Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_crypto/src/credential.rs

15.1 KiB, 53 runs

created by r1870400018:11560, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Generic signed credentials: "issuer attests that this public key is bound
2//! to this subject for this time range".
3//!
4//! A [`SignedCredential`] is a self-contained, typed record carrying a
5//! signature over a canonical byte encoding of its fields. It is
6//! agnostic about what issuers and subjects mean semantically -- a
7//! caller is free to decide that a "subject" is a device, a peer, a
8//! user, a delegated agent, or anything else with an identifier. The
9//! credential format is just "issuer says this binding holds from A to
10//! B".
11//!
12//! # Design points
13//!
14//! - *Issuer and subject IDs are opaque bytes*. Applications hash whatever
15//! they consider stable (a public key, a name, a URL) into the ID
16//! space of their choice. This module does not impose a hash.
17//! - *Signature scheme is named, not typed*. The scheme's registered
18//! name (see [`SignatureScheme`] and its `Debug` impl) is stored in
19//! the credential so a verifier can reconstruct the right algorithm
20//! from the serialised bytes alone.
21//! - *Self-signed is a special case*. When `issuer_id == subject_id`,
22//! the credential asserts that the holder of the bound secret key
23//! has declared their own identity. Useful for bootstrap: the very
24//! first credential in a system cannot be signed by anyone else.
25//! - *Validity window is inclusive on the lower bound and exclusive on
26//! the upper*. `0` as `valid_to` is a sentinel for "no expiry".
27//! - *No at-rest encryption*. A credential's purpose is to be shown;
28//! it carries only public data plus a signature. If you want to
29//! protect the credential's existence (not its contents), encrypt
30//! it at a different layer.
31//!
32//! # Canonical byte encoding
33//!
34//! The signed bytes are produced by [`SignedCredential::signed_bytes`]
35//! and consist of, in order:
36//!
37//! ```text
38//! [u8 version = 1]
39//! [u32 LE scheme_len][scheme_bytes]
40//! [u32 LE subject_id_len][subject_id]
41//! [u32 LE subject_pk_len][subject_pk]
42//! [u32 LE issuer_id_len][issuer_id]
43//! [u64 LE valid_from]
44//! [u64 LE valid_to]
45//! ```
46//!
47//! The encoding is length-prefixed rather than delimiter-based so
48//! there is no ambiguity for callers that put arbitrary bytes in the
49//! id fields. The leading version byte lets a future schema change
50//! surface as a verify-fails-loudly rather than a quietly-different
51//! hash.
52//!
53//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
54//! Anthropic Claude
55
56use crate::sign::SignatureScheme;
57
58use oxedyne_fe2o3_core::prelude::*;
59use oxedyne_fe2o3_iop_crypto::{
60 keys::KeyManager,
61 sign::Signer,
62};
63use oxedyne_fe2o3_jdat::prelude::*;
64
65use std::{
66 str::FromStr,
67 time::{
68 SystemTime,
69 UNIX_EPOCH,
70 },
71};
72
73
74// Bump the version if the field set or the layout changes in a way that would
75// alter the signed bytes.
76pub const CREDENTIAL_VERSION: u8 = 1;
77
78
79/// A signed attestation that `issuer_id` vouches for `subject_pk` being bound
80/// to `subject_id` for a stated time range.
81#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct SignedCredential {
83 // Both identifiers are opaque: a hash of a name, a hash of a key, an
84 // assigned serial, whatever the caller decides. They are equal in a
85 // self-signed credential.
86 pub subject_id: Vec<u8>,
87 // Zero length is legal and binds an identifier alone, with no key.
88 pub subject_pk: Vec<u8>,
89 pub issuer_id: Vec<u8>,
90 pub scheme: String, // SignatureScheme's Debug string
91 pub valid_from: u64, // seconds since epoch, inclusive; 0 for no start
92 pub valid_to: u64, // seconds since epoch, exclusive; 0 for no expiry
93 pub sig: Vec<u8>,
94}
95
96impl SignedCredential {
97
98 /// A self-signed credential: the holder of the secret key declares who it
99 /// is, and vouches for nothing else. `subject_scheme` must carry both keys.
100 pub fn self_sign(
101 subject_id: Vec<u8>,
102 subject_scheme: &SignatureScheme,
103 valid_from: u64,
104 valid_to: u64,
105 )
106 -> Outcome<Self>
107 {
108 let subject_pk = res!(res!(subject_scheme.get_public_key()).ok_or_else(|| err!(
109 "self_sign requires the signature scheme to carry a public key.";
110 Missing, Configuration)))
111 .to_vec();
112 Self::sign(
113 subject_id.clone(),
114 subject_pk,
115 subject_id,
116 subject_scheme,
117 valid_from,
118 valid_to,
119 )
120 }
121
122 /// A third-party credential: the issuer attests that `subject_pk` belongs
123 /// to `subject_id`.
124 ///
125 /// The subject's public key is passed in rather than derived, so the issuer
126 /// never needs the subject's secret key. `issuer_scheme` must carry both of
127 /// the issuer's.
128 pub fn sign(
129 subject_id: Vec<u8>,
130 subject_pk: Vec<u8>,
131 issuer_id: Vec<u8>,
132 issuer_scheme: &SignatureScheme,
133 valid_from: u64,
134 valid_to: u64,
135 )
136 -> Outcome<Self>
137 {
138 if valid_to != 0 && valid_to <= valid_from {
139 return Err(err!(
140 "Credential validity window is empty: valid_from {}, \
141 valid_to {}.", valid_from, valid_to;
142 Invalid, Input, Size));
143 }
144 let scheme = fmt!("{:?}", issuer_scheme);
145 let mut cred = Self {
146 subject_id,
147 subject_pk,
148 issuer_id,
149 scheme,
150 valid_from,
151 valid_to,
152 sig: Vec::new(),
153 };
154 let bytes = cred.signed_bytes();
155 cred.sig = res!(issuer_scheme.sign(&bytes));
156 Ok(cred)
157 }
158
159 /// The canonical encoding the signature covers; the module header gives the
160 /// layout.
161 pub fn signed_bytes(&self) -> Vec<u8> {
162 let scheme_bytes = self.scheme.as_bytes();
163 let cap = 1
164 + 4 + scheme_bytes.len()
165 + 4 + self.subject_id.len()
166 + 4 + self.subject_pk.len()
167 + 4 + self.issuer_id.len()
168 + 8 + 8;
169 let mut out = Vec::with_capacity(cap);
170 out.push(CREDENTIAL_VERSION);
171 out.extend_from_slice(&(scheme_bytes.len() as u32).to_le_bytes());
172 out.extend_from_slice(scheme_bytes);
173 out.extend_from_slice(&(self.subject_id.len() as u32).to_le_bytes());
174 out.extend_from_slice(&self.subject_id);
175 out.extend_from_slice(&(self.subject_pk.len() as u32).to_le_bytes());
176 out.extend_from_slice(&self.subject_pk);
177 out.extend_from_slice(&(self.issuer_id.len() as u32).to_le_bytes());
178 out.extend_from_slice(&self.issuer_id);
179 out.extend_from_slice(&self.valid_from.to_le_bytes());
180 out.extend_from_slice(&self.valid_to.to_le_bytes());
181 out
182 }
183
184 /// Verifies the signature and that the credential is in its window now.
185 ///
186 /// A bad signature, an expired window and an unrecognised scheme each carry
187 /// their own error tag, so a caller can tell them apart.
188 pub fn verify(&self, issuer_pk: &[u8]) -> Outcome<()> {
189 let now = SystemTime::now()
190 .duration_since(UNIX_EPOCH)
191 .map(|d| d.as_secs())
192 .unwrap_or(0);
193 self.verify_at(issuer_pk, now)
194 }
195
196 /// As [`Self::verify`], against a supplied `now` in seconds.
197 pub fn verify_at(&self, issuer_pk: &[u8], now: u64) -> Outcome<()> {
198 // Validity window check first -- cheap, fails fast on
199 // expired credentials without wasting a signature verify.
200 if self.valid_from != 0 && now < self.valid_from {
201 return Err(err!(
202 "Credential not yet valid: now = {}, valid_from = {}.",
203 now, self.valid_from;
204 Invalid, Security, Order));
205 }
206 if self.valid_to != 0 && now >= self.valid_to {
207 return Err(err!(
208 "Credential expired: now = {}, valid_to = {}.",
209 now, self.valid_to;
210 Invalid, Security, Order));
211 }
212 // Reconstruct the scheme from its stored name and clone it
213 // with the supplied public key so we can call verify.
214 let scheme = res!(SignatureScheme::from_str(&self.scheme));
215 let scheme = res!(scheme.clone_with_keys(Some(issuer_pk), None));
216 let bytes = self.signed_bytes();
217 let ok = res!(scheme.verify(&bytes, &self.sig));
218 if !ok {
219 return Err(err!(
220 "Credential signature did not verify under the supplied \
221 issuer public key (scheme: {}).", self.scheme;
222 Invalid, Security, Mismatch));
223 }
224 Ok(())
225 }
226
227 /// Is this credential self-signed?
228 pub fn is_self_signed(&self) -> bool {
229 self.issuer_id == self.subject_id
230 }
231}
232
233
234impl ToDat for SignedCredential {
235 fn to_dat(&self) -> Outcome<Dat> {
236 let mut m = DaticleMap::new();
237 m.insert(dat!("subject_id"), Dat::bytdat(self.subject_id.clone()));
238 m.insert(dat!("subject_pk"), Dat::bytdat(self.subject_pk.clone()));
239 m.insert(dat!("issuer_id"), Dat::bytdat(self.issuer_id.clone()));
240 m.insert(dat!("scheme"), dat!(self.scheme.clone()));
241 m.insert(dat!("valid_from"), dat!(self.valid_from));
242 m.insert(dat!("valid_to"), dat!(self.valid_to));
243 m.insert(dat!("sig"), Dat::bytdat(self.sig.clone()));
244 Ok(Dat::Map(m))
245 }
246}
247
248impl FromDat for SignedCredential {
249 fn from_dat(mut dat: Dat) -> Outcome<Self> {
250 let subject_id = try_extract_dat!(
251 res!(dat.map_remove_must(&dat!("subject_id"))),
252 BU8, BU16, BU32, BU64,
253 );
254 let subject_pk = try_extract_dat!(
255 res!(dat.map_remove_must(&dat!("subject_pk"))),
256 BU8, BU16, BU32, BU64,
257 );
258 let issuer_id = try_extract_dat!(
259 res!(dat.map_remove_must(&dat!("issuer_id"))),
260 BU8, BU16, BU32, BU64,
261 );
262 let scheme = try_extract_dat!(
263 res!(dat.map_remove_must(&dat!("scheme"))),
264 Str,
265 );
266 let valid_from = match res!(dat.map_remove_must(&dat!("valid_from"))) {
267 Dat::U64(n) => n,
268 Dat::U32(n) => n as u64,
269 other => return Err(err!(
270 "SignedCredential 'valid_from' must be u64, got {:?}.",
271 other.kind();
272 Invalid, Input, Mismatch)),
273 };
274 let valid_to = match res!(dat.map_remove_must(&dat!("valid_to"))) {
275 Dat::U64(n) => n,
276 Dat::U32(n) => n as u64,
277 other => return Err(err!(
278 "SignedCredential 'valid_to' must be u64, got {:?}.",
279 other.kind();
280 Invalid, Input, Mismatch)),
281 };
282 let sig = try_extract_dat!(
283 res!(dat.map_remove_must(&dat!("sig"))),
284 BU8, BU16, BU32, BU64,
285 );
286 Ok(Self {
287 subject_id,
288 subject_pk,
289 issuer_id,
290 scheme,
291 valid_from,
292 valid_to,
293 sig,
294 })
295 }
296}
297
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 fn ed25519_scheme() -> SignatureScheme {
304 SignatureScheme::new_ed25519()
305 }
306
307 fn issuer_pk(scheme: &SignatureScheme) -> Vec<u8> {
308 scheme.get_public_key().unwrap().unwrap().to_vec()
309 }
310
311 #[test]
312 fn self_sign_round_trip_verifies() -> Outcome<()> {
313 let scheme = ed25519_scheme();
314 let pk = issuer_pk(&scheme);
315 let cred = res!(SignedCredential::self_sign(
316 vec![0x42; 32],
317 &scheme,
318 0,
319 0,
320 ));
321 assert!(cred.is_self_signed());
322 res!(cred.verify(&pk));
323 Ok(())
324 }
325
326 #[test]
327 fn third_party_sign_round_trip_verifies() -> Outcome<()> {
328 let issuer = ed25519_scheme();
329 let subject = ed25519_scheme();
330 let subject_pk = issuer_pk(&subject);
331 let issuer_pk_bytes = issuer_pk(&issuer);
332 let cred = res!(SignedCredential::sign(
333 vec![0x01; 16],
334 subject_pk,
335 vec![0x02; 16],
336 &issuer,
337 0,
338 0,
339 ));
340 assert!(!cred.is_self_signed());
341 res!(cred.verify(&issuer_pk_bytes));
342 Ok(())
343 }
344
345 #[test]
346 fn tampered_field_fails_verify() -> Outcome<()> {
347 let scheme = ed25519_scheme();
348 let pk = issuer_pk(&scheme);
349 let mut cred = res!(SignedCredential::self_sign(
350 vec![0x55; 32], &scheme, 0, 0,
351 ));
352 // Flip a bit in subject_pk after signing.
353 if !cred.subject_pk.is_empty() {
354 cred.subject_pk[0] ^= 0x01;
355 }
356 assert!(cred.verify(&pk).is_err());
357 Ok(())
358 }
359
360 #[test]
361 fn wrong_issuer_pk_fails_verify() -> Outcome<()> {
362 let scheme = ed25519_scheme();
363 let other = ed25519_scheme();
364 let cred = res!(SignedCredential::self_sign(
365 vec![0x66; 32], &scheme, 0, 0,
366 ));
367 let wrong_pk = issuer_pk(&other);
368 assert!(cred.verify(&wrong_pk).is_err());
369 Ok(())
370 }
371
372 #[test]
373 fn validity_window_not_yet_valid() -> Outcome<()> {
374 let scheme = ed25519_scheme();
375 let pk = issuer_pk(&scheme);
376 let cred = res!(SignedCredential::self_sign(
377 vec![0x77; 32], &scheme, 2_000_000_000, 0,
378 ));
379 // "Now" before valid_from.
380 assert!(cred.verify_at(&pk, 1_000_000_000).is_err());
381 // "Now" at or after valid_from passes.
382 res!(cred.verify_at(&pk, 2_000_000_000));
383 Ok(())
384 }
385
386 #[test]
387 fn validity_window_expired() -> Outcome<()> {
388 let scheme = ed25519_scheme();
389 let pk = issuer_pk(&scheme);
390 let cred = res!(SignedCredential::self_sign(
391 vec![0x88; 32], &scheme, 0, 2_000_000_000,
392 ));
393 // "Now" before valid_to passes.
394 res!(cred.verify_at(&pk, 1_999_999_999));
395 // "Now" at or after valid_to fails (upper bound is exclusive).
396 assert!(cred.verify_at(&pk, 2_000_000_000).is_err());
397 Ok(())
398 }
399
400 #[test]
401 fn empty_validity_window_rejected_at_sign_time() -> Outcome<()> {
402 let scheme = ed25519_scheme();
403 // valid_to <= valid_from (and != 0) is nonsense.
404 assert!(SignedCredential::self_sign(
405 vec![0x99; 32], &scheme, 100, 50,
406 ).is_err());
407 assert!(SignedCredential::self_sign(
408 vec![0x99; 32], &scheme, 100, 100,
409 ).is_err());
410 Ok(())
411 }
412
413 #[test]
414 fn jdat_round_trip_preserves_signature() -> Outcome<()> {
415 let scheme = ed25519_scheme();
416 let pk = issuer_pk(&scheme);
417 let cred = res!(SignedCredential::self_sign(
418 vec![0xab; 32], &scheme, 0, 0,
419 ));
420 let dat = res!(cred.to_dat());
421 let back = res!(SignedCredential::from_dat(dat));
422 assert_eq!(back, cred);
423 res!(back.verify(&pk));
424 Ok(())
425 }
426
427 #[test]
428 fn version_byte_in_signed_bytes() {
429 let cred = SignedCredential {
430 subject_id: vec![0x01],
431 subject_pk: vec![0x02],
432 issuer_id: vec![0x03],
433 scheme: "Ed25519".to_string(),
434 valid_from: 0,
435 valid_to: 0,
436 sig: Vec::new(),
437 };
438 let bytes = cred.signed_bytes();
439 assert_eq!(bytes[0], CREDENTIAL_VERSION);
440 }
441}