//! Per-tenant Ed25519 signing for appraisal reports. //! //! A `TenantKey` wraps an `ed25519_dalek::SigningKey` and an //! operator-chosen identity string. Signing a payload produces a //! `Signature` envelope (alg, tenant identity, base64 pubkey, base64 //! signature) ready to attach to a `CardReport` or `BoxReport`. //! //! The hash that links a card report to its box report is also computed //! here: `box_payload_hash` returns the hex-encoded SHA-256 of the //! canonical box-payload bytes. use base64::{Engine, engine::general_purpose::STANDARD as B64}; use ed25519_dalek::{Signature as DalekSig, Signer, SigningKey, Verifier, VerifyingKey}; use everycycle_hal::{BoxReport, BoxReportPayload, CardReport, CardReportPayload, Signature}; use sha2::{Digest, Sha256}; use crate::canonical::{CanonicalError, canonical_json}; const ALG: &str = "ed25519"; #[derive(Debug)] pub enum SignError { Canonical(CanonicalError), } impl core::fmt::Display for SignError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Canonical(e) => write!(f, "{e}"), } } } impl std::error::Error for SignError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Canonical(e) => Some(e), } } } impl From for SignError { fn from(e: CanonicalError) -> Self { Self::Canonical(e) } } #[derive(Debug)] pub enum VerifyError { Canonical(CanonicalError), UnknownAlg(String), BadPubkey, BadSignature, SignatureMismatch, } impl core::fmt::Display for VerifyError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Canonical(e) => write!(f, "{e}"), Self::UnknownAlg(a) => write!(f, "unknown signature algorithm: {a}"), Self::BadPubkey => write!(f, "tenant public key did not decode"), Self::BadSignature => write!(f, "signature did not decode"), Self::SignatureMismatch => write!(f, "signature did not verify against payload"), } } } impl std::error::Error for VerifyError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Canonical(e) => Some(e), _ => None, } } } impl From for VerifyError { fn from(e: CanonicalError) -> Self { Self::Canonical(e) } } /// An operator's signing key plus the identity string it publishes /// alongside the public key. pub struct TenantKey { pub identity: String, signing: SigningKey, } impl TenantKey { /// Generate a new keypair from OS entropy. /// /// # Panics /// /// Panics if the OS entropy source is unavailable. #[must_use] pub fn generate(identity: impl Into) -> Self { let mut seed = [0u8; 32]; getrandom::fill(&mut seed).expect("OS entropy source unavailable"); Self::from_seed(identity, seed) } /// Reconstruct a key from raw seed bytes. Used for fixture tests /// and for restoring a persisted operator key. #[must_use] pub fn from_seed(identity: impl Into, seed: [u8; 32]) -> Self { Self { identity: identity.into(), signing: SigningKey::from_bytes(&seed), } } #[must_use] pub fn pubkey_b64(&self) -> String { B64.encode(self.signing.verifying_key().to_bytes()) } fn sign_bytes(&self, bytes: &[u8]) -> Signature { let sig: DalekSig = self.signing.sign(bytes); Signature { alg: ALG.to_string(), tenant_identity: self.identity.clone(), tenant_pubkey: self.pubkey_b64(), sig: B64.encode(sig.to_bytes()), } } } /// Hex-encoded SHA-256 of the canonical box-payload bytes. This is the /// value a card-report payload carries in its `box_report` field. pub fn box_payload_hash(payload: &BoxReportPayload) -> Result { let bytes = canonical_json(payload)?; let digest = Sha256::digest(bytes.as_bytes()); Ok(hex_lower(&digest)) } pub fn sign_box_report(key: &TenantKey, payload: BoxReportPayload) -> Result { let bytes = canonical_json(&payload)?; let signature = key.sign_bytes(bytes.as_bytes()); Ok(BoxReport { payload, signature }) } pub fn sign_card_report( key: &TenantKey, payload: CardReportPayload, ) -> Result { let bytes = canonical_json(&payload)?; let signature = key.sign_bytes(bytes.as_bytes()); Ok(CardReport { payload, signature }) } pub fn verify_box_report(report: &BoxReport) -> Result<(), VerifyError> { verify(&report.payload, &report.signature) } pub fn verify_card_report(report: &CardReport) -> Result<(), VerifyError> { verify(&report.payload, &report.signature) } fn verify(payload: &T, sig: &Signature) -> Result<(), VerifyError> { if sig.alg != ALG { return Err(VerifyError::UnknownAlg(sig.alg.clone())); } let pub_bytes = B64 .decode(sig.tenant_pubkey.as_bytes()) .map_err(|_| VerifyError::BadPubkey)?; let pub_arr: [u8; 32] = pub_bytes .as_slice() .try_into() .map_err(|_| VerifyError::BadPubkey)?; let verifying = VerifyingKey::from_bytes(&pub_arr).map_err(|_| VerifyError::BadPubkey)?; let sig_bytes = B64 .decode(sig.sig.as_bytes()) .map_err(|_| VerifyError::BadSignature)?; let sig_arr: [u8; 64] = sig_bytes .as_slice() .try_into() .map_err(|_| VerifyError::BadSignature)?; let dalek_sig = DalekSig::from_bytes(&sig_arr); let bytes = canonical_json(payload)?; verifying .verify(bytes.as_bytes(), &dalek_sig) .map_err(|_| VerifyError::SignatureMismatch) } fn hex_lower(bytes: &[u8]) -> String { const HEX: &[u8; 16] = b"0123456789abcdef"; let mut out = String::with_capacity(bytes.len() * 2); for b in bytes { out.push(HEX[(b >> 4) as usize] as char); out.push(HEX[(b & 0x0f) as usize] as char); } out }