quantum_shield/rotate.rs
1//! Key-rotation attestations (`QSR2`).
2//!
3//! When a keypair is rotated, the **old** keypair signs the **new** public
4//! bundle, producing a [`RotationAttestation`] that gives verifiers a
5//! cryptographic old → new link. Because it reuses the hybrid
6//! [`sign`](crate::sign)/[`verify`](crate::verify), the attestation is itself
7//! Ed25519 + ML-DSA — forging it requires breaking both.
8//!
9//! The signed message is `old_key_id (16) || epoch (u64) || new_public (QSP2)`
10//! under the [`ROTATION_CONTEXT`] domain. The old bundle is **not** on the
11//! wire: the verifier supplies the key it already trusts, so an attestation
12//! can only be checked against an explicit trust anchor.
13//!
14//! ## Rollback protection is the caller's responsibility
15//!
16//! A [`RotationAttestation`] carries no clock and does not expire: every
17//! attestation `old` ever produced verifies forever under `old`. If a key is
18//! rotated more than once (`old→new1`, later `old→new2`), a captured `old→new1`
19//! would otherwise let an attacker roll a verifier back to the superseded
20//! `new1`. To prevent this, each attestation binds a caller-chosen monotonic
21//! `epoch`; a verifier **must** remember the highest epoch it has accepted for
22//! a given `old` and reject any attestation with a lower-or-equal epoch. The
23//! library cannot enforce this itself (it holds no state), so it exposes the
24//! epoch via [`RotationAttestation::epoch`].
25//!
26//! ```
27//! use quantum_shield::{HybridCrypto, verify_rotation};
28//! # fn run() -> quantum_shield::Result<()> {
29//! let old = HybridCrypto::generate()?;
30//! let new = HybridCrypto::generate()?;
31//!
32//! let attestation = old.attest_rotation(new.public_keys(), 1)?;
33//! // A peer who trusts `old` learns the authentic new key:
34//! let trusted_new = verify_rotation(old.public_keys(), &attestation)?;
35//! assert_eq!(trusted_new, new.public_keys());
36//! // ...and rejects it if its epoch does not advance past the last accepted one:
37//! assert!(attestation.epoch() > 0);
38//! # Ok(()) }
39//! ```
40
41use crate::constants::*;
42use crate::error::{Error, Result};
43use crate::keys::{KeyPair, PublicKeyBundle};
44use crate::sign;
45use crate::types::HybridSignature;
46use crate::wire::{read_header, write_header};
47use alloc::vec::Vec;
48
49/// A signed statement that one keypair authorizes a successor public bundle at
50/// a given epoch.
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct RotationAttestation {
53 epoch: u64,
54 new_public: PublicKeyBundle,
55 signature: HybridSignature,
56}
57
58impl RotationAttestation {
59 /// The attested successor public bundle. Only trustworthy once the
60 /// attestation has passed [`verify_rotation`].
61 pub fn new_public(&self) -> &PublicKeyBundle {
62 &self.new_public
63 }
64
65 /// The caller-chosen monotonic epoch bound into the signature. Verifiers
66 /// must reject an attestation whose epoch does not advance past the last
67 /// one they accepted for this signer (see the module docs on rollback).
68 pub fn epoch(&self) -> u64 {
69 self.epoch
70 }
71
72 /// Serialize to the `QSR2` wire format:
73 /// `header[6] || epoch (u64_be, 8) || new_public (QSP2, 4230) || signature (QSS2, 4697)`.
74 pub fn to_bytes(&self) -> Vec<u8> {
75 let mut out = Vec::with_capacity(HEADER_LEN + 8 + PUBLIC_BUNDLE_LEN + SIGNATURE_LEN);
76 write_header(&mut out, MAGIC_ROTATION);
77 out.extend_from_slice(&self.epoch.to_be_bytes());
78 out.extend_from_slice(&self.new_public.to_bytes());
79 out.extend_from_slice(&self.signature.to_bytes());
80 out
81 }
82
83 /// Parse a `QSR2` attestation.
84 ///
85 /// # Errors
86 ///
87 /// [`Error::InvalidSignature`] on malformed input; version/suite errors for
88 /// other formats. The signature is not checked here — call
89 /// [`verify_rotation`].
90 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
91 let rest = read_header(bytes, MAGIC_ROTATION, Error::InvalidSignature)?;
92 if rest.len() != 8 + PUBLIC_BUNDLE_LEN + SIGNATURE_LEN {
93 return Err(Error::InvalidSignature);
94 }
95 let (epoch_bytes, rest) = rest.split_at(8);
96 let epoch =
97 u64::from_be_bytes(epoch_bytes.try_into().expect("split_at guarantees 8 bytes"));
98 let (bundle_bytes, sig_bytes) = rest.split_at(PUBLIC_BUNDLE_LEN);
99 let new_public = PublicKeyBundle::from_bytes(bundle_bytes)?;
100 let signature = HybridSignature::from_bytes(sig_bytes)?;
101 Ok(Self {
102 epoch,
103 new_public,
104 signature,
105 })
106 }
107}
108
109/// The message signed by an attestation: `old_key_id || epoch || new_public`.
110fn rotation_message(old: &PublicKeyBundle, epoch: u64, new_public: &PublicKeyBundle) -> Vec<u8> {
111 let mut msg = Vec::with_capacity(KEY_ID_LEN + 8 + PUBLIC_BUNDLE_LEN);
112 msg.extend_from_slice(old.key_id().as_bytes());
113 msg.extend_from_slice(&epoch.to_be_bytes());
114 msg.extend_from_slice(&new_public.to_bytes());
115 msg
116}
117
118/// Produce an attestation that `old` authorizes `new_public` as its successor
119/// at `epoch`. Callers should use a strictly increasing `epoch` per signer.
120pub(crate) fn attest_rotation(
121 old: &KeyPair,
122 new_public: &PublicKeyBundle,
123 epoch: u64,
124) -> Result<RotationAttestation> {
125 let message = rotation_message(old.public_keys(), epoch, new_public);
126 let signature = sign::sign(old, &message, ROTATION_CONTEXT)?;
127 Ok(RotationAttestation {
128 epoch,
129 new_public: new_public.clone(),
130 signature,
131 })
132}
133
134/// Verify a rotation attestation against the trusted `old` public bundle.
135///
136/// On success returns the authenticated successor bundle.
137///
138/// # Errors
139///
140/// [`Error::VerificationFailed`] if the attestation was not signed by `old`
141/// over its embedded successor.
142pub fn verify_rotation<'a>(
143 old: &PublicKeyBundle,
144 attestation: &'a RotationAttestation,
145) -> Result<&'a PublicKeyBundle> {
146 let message = rotation_message(old, attestation.epoch, &attestation.new_public);
147 sign::verify(&message, ROTATION_CONTEXT, &attestation.signature, old)?;
148 Ok(&attestation.new_public)
149}