Skip to main content

aae/
signing.rs

1//! Audit-event signatures (SPEC §3.5, v0.7).
2//!
3//! The signer signs the event's **attestation bytes** —
4//! `canonicalize(view sans signature sans this_event_hash) || prev_event_hash`
5//! — which are exactly the bytes an unsigned event would hash. The signature
6//! is then attached and `this_event_hash` is computed over the view
7//! *including* it. Two properties follow: the chain commits to the signature
8//! (stripping it is hash-detectable without keys), and the signature commits
9//! to the event's content *and* its chain position (prev hash is inside the
10//! signed bytes).
11//!
12//! Consequence: signatures are applied at append time or never.
13//!
14//! v0.x permits exactly one algorithm, `ed25519` — deterministic, so signed
15//! fixtures are byte-reproducible and vector-testable.
16
17use crate::hashchain::{canonicalize, event_value_for_hashing, ChainError};
18use base64::engine::general_purpose::URL_SAFE_NO_PAD;
19use base64::Engine as _;
20use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey};
21use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
22
23/// The only signature algorithm permitted in v0.x.
24pub const SIGNATURE_ALG: &str = "ed25519";
25
26/// Errors from signing and signature verification.
27#[derive(Debug, thiserror::Error)]
28pub enum SignatureError {
29    /// The event carries no signature where one was required.
30    #[error("event carries no signature (event_id={event_id})")]
31    Missing {
32        /// Event id of the offending event.
33        event_id: String,
34    },
35    /// The signature declares an algorithm this verifier does not permit.
36    #[error("signature algorithm {alg:?} not permitted in v0.x (ed25519 only)")]
37    UnpermittedAlgorithm {
38        /// The offending algorithm name.
39        alg: String,
40    },
41    /// No trusted public key matches the signature's `key_id`.
42    #[error("unknown signing key {key_id:?}")]
43    UnknownKey {
44        /// The unknown key id.
45        key_id: String,
46    },
47    /// A key or signature value could not be parsed.
48    #[error("malformed key or signature: {0}")]
49    Malformed(String),
50    /// A signature was present but did not verify (content, position, or the
51    /// signature itself was altered).
52    #[error("signature did not verify (event_id={event_id})")]
53    Invalid {
54        /// Event id of the offending event.
55        event_id: String,
56    },
57    /// Canonicalization failed.
58    #[error(transparent)]
59    Chain(#[from] ChainError),
60}
61
62/// An Ed25519 event signer.
63pub struct Ed25519EventSigner {
64    key_id: String,
65    key: SigningKey,
66}
67
68impl Ed25519EventSigner {
69    /// Build from a PKCS#8 PEM private key.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`SignatureError::Malformed`] if the PEM is not an Ed25519
74    /// PKCS#8 private key.
75    pub fn from_pem(key_id: &str, private_key_pem: &str) -> Result<Self, SignatureError> {
76        let key = SigningKey::from_pkcs8_pem(private_key_pem)
77            .map_err(|e| SignatureError::Malformed(e.to_string()))?;
78        Ok(Self {
79            key_id: key_id.to_string(),
80            key,
81        })
82    }
83
84    /// The key id this signer stamps into signatures.
85    #[must_use]
86    pub fn key_id(&self) -> &str {
87        &self.key_id
88    }
89
90    /// Sign raw attestation bytes.
91    #[must_use]
92    pub fn sign(&self, data: &[u8]) -> String {
93        URL_SAFE_NO_PAD.encode(self.key.sign(data).to_bytes())
94    }
95}
96
97/// The bytes a signature covers: `canonical(view sans signature sans
98/// this_event_hash) || prev_event_hash`.
99///
100/// # Errors
101///
102/// Returns [`ChainError`] if the value cannot be canonicalized.
103pub fn attestation_bytes(
104    event: &serde_json::Value,
105    prev_event_hash: Option<&str>,
106) -> Result<Vec<u8>, ChainError> {
107    let mut view = event_value_for_hashing(event);
108    if let Some(obj) = view.as_object_mut() {
109        obj.remove("signature");
110    }
111    let mut bytes = canonicalize(&view)?;
112    bytes.extend(prev_event_hash.unwrap_or("").as_bytes());
113    Ok(bytes)
114}
115
116/// Verify a single signed event against a `key_id -> PEM public key` map.
117///
118/// Returns `Ok(true)` on a valid signature and `Ok(false)` when a present
119/// signature does not verify. Structural problems (missing signature, unknown
120/// key, unpermitted algorithm) are *errors*, not `false`: operators must be
121/// able to tell "forged" from "misconfigured".
122///
123/// # Errors
124///
125/// See [`SignatureError`].
126pub fn verify_event_signature<S: std::hash::BuildHasher>(
127    event: &serde_json::Value,
128    trusted_keys: &std::collections::HashMap<String, String, S>,
129) -> Result<bool, SignatureError> {
130    let event_id = event
131        .get("event_id")
132        .and_then(|v| v.as_str())
133        .unwrap_or("<unknown>")
134        .to_string();
135    let sig = event
136        .get("signature")
137        .filter(|v| !v.is_null())
138        .ok_or_else(|| SignatureError::Missing {
139            event_id: event_id.clone(),
140        })?;
141
142    let alg = sig.get("alg").and_then(|v| v.as_str()).unwrap_or("");
143    if alg != SIGNATURE_ALG {
144        return Err(SignatureError::UnpermittedAlgorithm {
145            alg: alg.to_string(),
146        });
147    }
148    let key_id = sig.get("key_id").and_then(|v| v.as_str()).unwrap_or("");
149    let pem = trusted_keys
150        .get(key_id)
151        .ok_or_else(|| SignatureError::UnknownKey {
152            key_id: key_id.to_string(),
153        })?;
154    let verifying = VerifyingKey::from_public_key_pem(pem)
155        .map_err(|e| SignatureError::Malformed(e.to_string()))?;
156
157    let raw = URL_SAFE_NO_PAD
158        .decode(sig.get("value").and_then(|v| v.as_str()).unwrap_or(""))
159        .map_err(|e| SignatureError::Malformed(e.to_string()))?;
160    let signature =
161        Signature::from_slice(&raw).map_err(|e| SignatureError::Malformed(e.to_string()))?;
162
163    let prev = event.get("prev_event_hash").and_then(|v| v.as_str());
164    let data = attestation_bytes(event, prev)?;
165    Ok(verifying.verify(&data, &signature).is_ok())
166}
167
168/// Verify signatures across a chain. Returns how many events carried valid
169/// signatures. Unsigned events are permitted unless `require_all`; a
170/// present-but-invalid signature always errors.
171///
172/// # Errors
173///
174/// See [`SignatureError`].
175pub fn verify_chain_signatures<S: std::hash::BuildHasher>(
176    events: &[serde_json::Value],
177    trusted_keys: &std::collections::HashMap<String, String, S>,
178    require_all: bool,
179) -> Result<usize, SignatureError> {
180    let mut verified = 0;
181    for ev in events {
182        let has_sig = ev.get("signature").is_some_and(|v| !v.is_null());
183        if !has_sig {
184            if require_all {
185                return Err(SignatureError::Missing {
186                    event_id: ev
187                        .get("event_id")
188                        .and_then(|v| v.as_str())
189                        .unwrap_or("<unknown>")
190                        .to_string(),
191                });
192            }
193            continue;
194        }
195        if !verify_event_signature(ev, trusted_keys)? {
196            return Err(SignatureError::Invalid {
197                event_id: ev
198                    .get("event_id")
199                    .and_then(|v| v.as_str())
200                    .unwrap_or("<unknown>")
201                    .to_string(),
202            });
203        }
204        verified += 1;
205    }
206    Ok(verified)
207}