Skip to main content

max / everycycle

6.0 KB · 198 lines History Blame Raw
1 //! Per-tenant Ed25519 signing for appraisal reports.
2 //!
3 //! A `TenantKey` wraps an `ed25519_dalek::SigningKey` and an
4 //! operator-chosen identity string. Signing a payload produces a
5 //! `Signature` envelope (alg, tenant identity, base64 pubkey, base64
6 //! signature) ready to attach to a `CardReport` or `BoxReport`.
7 //!
8 //! The hash that links a card report to its box report is also computed
9 //! here: `box_payload_hash` returns the hex-encoded SHA-256 of the
10 //! canonical box-payload bytes.
11
12 use base64::{Engine, engine::general_purpose::STANDARD as B64};
13 use ed25519_dalek::{Signature as DalekSig, Signer, SigningKey, Verifier, VerifyingKey};
14 use everycycle_hal::{BoxReport, BoxReportPayload, CardReport, CardReportPayload, Signature};
15 use sha2::{Digest, Sha256};
16
17 use crate::canonical::{CanonicalError, canonical_json};
18
19 const ALG: &str = "ed25519";
20
21 #[derive(Debug)]
22 pub enum SignError {
23 Canonical(CanonicalError),
24 }
25
26 impl core::fmt::Display for SignError {
27 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
28 match self {
29 Self::Canonical(e) => write!(f, "{e}"),
30 }
31 }
32 }
33
34 impl std::error::Error for SignError {
35 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
36 match self {
37 Self::Canonical(e) => Some(e),
38 }
39 }
40 }
41
42 impl From<CanonicalError> for SignError {
43 fn from(e: CanonicalError) -> Self {
44 Self::Canonical(e)
45 }
46 }
47
48 #[derive(Debug)]
49 pub enum VerifyError {
50 Canonical(CanonicalError),
51 UnknownAlg(String),
52 BadPubkey,
53 BadSignature,
54 SignatureMismatch,
55 }
56
57 impl core::fmt::Display for VerifyError {
58 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59 match self {
60 Self::Canonical(e) => write!(f, "{e}"),
61 Self::UnknownAlg(a) => write!(f, "unknown signature algorithm: {a}"),
62 Self::BadPubkey => write!(f, "tenant public key did not decode"),
63 Self::BadSignature => write!(f, "signature did not decode"),
64 Self::SignatureMismatch => write!(f, "signature did not verify against payload"),
65 }
66 }
67 }
68
69 impl std::error::Error for VerifyError {
70 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
71 match self {
72 Self::Canonical(e) => Some(e),
73 _ => None,
74 }
75 }
76 }
77
78 impl From<CanonicalError> for VerifyError {
79 fn from(e: CanonicalError) -> Self {
80 Self::Canonical(e)
81 }
82 }
83
84 /// An operator's signing key plus the identity string it publishes
85 /// alongside the public key.
86 pub struct TenantKey {
87 pub identity: String,
88 signing: SigningKey,
89 }
90
91 impl TenantKey {
92 /// Generate a new keypair from OS entropy.
93 ///
94 /// # Panics
95 ///
96 /// Panics if the OS entropy source is unavailable.
97 #[must_use]
98 pub fn generate(identity: impl Into<String>) -> Self {
99 let mut seed = [0u8; 32];
100 getrandom::fill(&mut seed).expect("OS entropy source unavailable");
101 Self::from_seed(identity, seed)
102 }
103
104 /// Reconstruct a key from raw seed bytes. Used for fixture tests
105 /// and for restoring a persisted operator key.
106 #[must_use]
107 pub fn from_seed(identity: impl Into<String>, seed: [u8; 32]) -> Self {
108 Self {
109 identity: identity.into(),
110 signing: SigningKey::from_bytes(&seed),
111 }
112 }
113
114 #[must_use]
115 pub fn pubkey_b64(&self) -> String {
116 B64.encode(self.signing.verifying_key().to_bytes())
117 }
118
119 fn sign_bytes(&self, bytes: &[u8]) -> Signature {
120 let sig: DalekSig = self.signing.sign(bytes);
121 Signature {
122 alg: ALG.to_string(),
123 tenant_identity: self.identity.clone(),
124 tenant_pubkey: self.pubkey_b64(),
125 sig: B64.encode(sig.to_bytes()),
126 }
127 }
128 }
129
130 /// Hex-encoded SHA-256 of the canonical box-payload bytes. This is the
131 /// value a card-report payload carries in its `box_report` field.
132 pub fn box_payload_hash(payload: &BoxReportPayload) -> Result<String, CanonicalError> {
133 let bytes = canonical_json(payload)?;
134 let digest = Sha256::digest(bytes.as_bytes());
135 Ok(hex_lower(&digest))
136 }
137
138 pub fn sign_box_report(key: &TenantKey, payload: BoxReportPayload) -> Result<BoxReport, SignError> {
139 let bytes = canonical_json(&payload)?;
140 let signature = key.sign_bytes(bytes.as_bytes());
141 Ok(BoxReport { payload, signature })
142 }
143
144 pub fn sign_card_report(
145 key: &TenantKey,
146 payload: CardReportPayload,
147 ) -> Result<CardReport, SignError> {
148 let bytes = canonical_json(&payload)?;
149 let signature = key.sign_bytes(bytes.as_bytes());
150 Ok(CardReport { payload, signature })
151 }
152
153 pub fn verify_box_report(report: &BoxReport) -> Result<(), VerifyError> {
154 verify(&report.payload, &report.signature)
155 }
156
157 pub fn verify_card_report(report: &CardReport) -> Result<(), VerifyError> {
158 verify(&report.payload, &report.signature)
159 }
160
161 fn verify<T: serde::Serialize>(payload: &T, sig: &Signature) -> Result<(), VerifyError> {
162 if sig.alg != ALG {
163 return Err(VerifyError::UnknownAlg(sig.alg.clone()));
164 }
165 let pub_bytes = B64
166 .decode(sig.tenant_pubkey.as_bytes())
167 .map_err(|_| VerifyError::BadPubkey)?;
168 let pub_arr: [u8; 32] = pub_bytes
169 .as_slice()
170 .try_into()
171 .map_err(|_| VerifyError::BadPubkey)?;
172 let verifying = VerifyingKey::from_bytes(&pub_arr).map_err(|_| VerifyError::BadPubkey)?;
173
174 let sig_bytes = B64
175 .decode(sig.sig.as_bytes())
176 .map_err(|_| VerifyError::BadSignature)?;
177 let sig_arr: [u8; 64] = sig_bytes
178 .as_slice()
179 .try_into()
180 .map_err(|_| VerifyError::BadSignature)?;
181 let dalek_sig = DalekSig::from_bytes(&sig_arr);
182
183 let bytes = canonical_json(payload)?;
184 verifying
185 .verify(bytes.as_bytes(), &dalek_sig)
186 .map_err(|_| VerifyError::SignatureMismatch)
187 }
188
189 fn hex_lower(bytes: &[u8]) -> String {
190 const HEX: &[u8; 16] = b"0123456789abcdef";
191 let mut out = String::with_capacity(bytes.len() * 2);
192 for b in bytes {
193 out.push(HEX[(b >> 4) as usize] as char);
194 out.push(HEX[(b & 0x0f) as usize] as char);
195 }
196 out
197 }
198