Skip to main content

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}