klieo-provenance 3.5.0

Append-only Merkle hash chain with signed evidence bundles for agent and audit provenance.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
//! Verifier port + concrete `Ed25519Verifier` and `StaticKeyResolver`.
//!
//! The `klieo-provenance` crate ships a [`Signer`](crate::Signer) port for
//! producing signed [`EvidenceBundle`](crate::EvidenceBundle)s, but until
//! 0.1.3 the verification half was missing — every downstream consumer
//! re-implemented canonical payload reconstruction, algorithm dispatch,
//! and chain Merkle verification. Worse, the bundle's
//! [`SignatureBytes::public_key_hex`](crate::SignatureBytes) was the only
//! key reference available, inviting a JWT `alg=none`-style forgery (a
//! malicious signer ships a bundle with their OWN public key and a valid
//! signature against it).
//!
//! This module closes both gaps:
//!
//! - [`Verifier`] decouples algorithm from key material. Implementations
//!   advertise the algorithm tag they support and verify a single
//!   `(public_key, payload, signature)` triple.
//! - [`KeyResolver`] maps a `key_id` to a trusted public key. The
//!   verifier NEVER reads `public_key_hex` from the bundle — trust comes
//!   exclusively from the resolver.
//! - [`EvidenceBundle::verify`](crate::EvidenceBundle::verify) composes
//!   them: rebuild the canonical payload, look up the trusted key,
//!   delegate the algorithm-specific check to the verifier, then re-walk
//!   the Merkle chain.
//!
//! [`Ed25519Verifier`] + [`StaticKeyResolver`] cover the common case:
//! ed25519-dalek-backed signatures, key material loaded from a config
//! file at boot.

use std::collections::HashMap;

use ed25519_dalek::{Signature, VerifyingKey};
use thiserror::Error;

/// Failure cases for evidence-bundle verification.
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum VerifyError {
    /// The bundle's `algorithm` tag is not in the verifier's allowlist.
    /// CWE-327 / CWE-347 defence: rejects bundles claiming `"none"` or
    /// an unknown algorithm before any cryptographic work happens.
    #[error("algorithm {0:?} not in verifier allowlist")]
    UnsupportedAlgorithm(String),
    /// The key_id named in the bundle is not in the trust store.
    #[error("unknown key_id: {0}")]
    UnknownKeyId(String),
    /// The signature did not validate against the trusted public key.
    #[error("signature mismatch")]
    SignatureMismatch,
    /// The chain failed Merkle re-verification (a tampered or reordered
    /// entry).
    #[error("merkle chain broken: {0}")]
    Chain(String),
    /// Payload reconstruction or hex decoding failed.
    #[error("payload reconstruction failed: {0}")]
    Payload(String),
    /// The public key returned by the resolver is malformed (wrong
    /// length, bad encoding, etc.).
    #[error("invalid trusted key: {0}")]
    InvalidKey(String),
}

/// Trust store: given a bundle's `key_id`, returns the trusted public
/// key bytes. The verifier NEVER reads `public_key_hex` from the bundle;
/// trust derives exclusively from this resolver.
pub trait KeyResolver: Send + Sync {
    /// Return the trusted public key (raw bytes) registered for
    /// `key_id`, or `None` if the key is unknown.
    fn resolve(&self, key_id: &str) -> Option<Vec<u8>>;
}

/// Algorithm-specific signature checker.
///
/// One implementation per signature algorithm the deployment trusts.
/// Wire `Ed25519Verifier` for production; bring your own for HSM /
/// FIPS-mode / multi-algorithm setups.
pub trait Verifier: Send + Sync {
    /// Algorithm tag this verifier accepts. Bundles whose
    /// `signature.algorithm` does not match this string are rejected
    /// before any cryptographic work runs. Common values:
    /// `"ed25519"`.
    fn algorithm(&self) -> &str;

    /// Verify `signature` against `payload` using `public_key`.
    /// Implementations must use constant-time comparison and reject
    /// malformed key / signature lengths with `VerifyError::InvalidKey`
    /// or `VerifyError::SignatureMismatch` respectively.
    fn verify(
        &self,
        public_key: &[u8],
        payload: &[u8],
        signature: &[u8],
    ) -> Result<(), VerifyError>;
}

/// Ed25519 implementation backed by `ed25519-dalek`. Algorithm tag is
/// the literal string `"ed25519"`.
pub struct Ed25519Verifier;

impl Ed25519Verifier {
    /// Construct a new ed25519 verifier.
    pub fn new() -> Self {
        Self
    }
}

impl Default for Ed25519Verifier {
    fn default() -> Self {
        Self::new()
    }
}

impl Verifier for Ed25519Verifier {
    fn algorithm(&self) -> &str {
        "ed25519"
    }

    fn verify(
        &self,
        public_key: &[u8],
        payload: &[u8],
        signature: &[u8],
    ) -> Result<(), VerifyError> {
        let key_bytes: [u8; 32] = public_key.try_into().map_err(|_| {
            VerifyError::InvalidKey(format!(
                "ed25519 public key must be 32 bytes, got {}",
                public_key.len()
            ))
        })?;
        let verifying = VerifyingKey::from_bytes(&key_bytes)
            .map_err(|e| VerifyError::InvalidKey(e.to_string()))?;
        let sig_bytes: [u8; 64] = signature
            .try_into()
            .map_err(|_| VerifyError::SignatureMismatch)?;
        let sig = Signature::from_bytes(&sig_bytes);
        verifying
            .verify_strict(payload, &sig)
            .map_err(|_| VerifyError::SignatureMismatch)
    }
}

/// ECDSA-P256 implementation backed by the `p256` crate. Algorithm
/// tag is the literal string `"ecdsa-p256"`. Compatible with the
/// SignatureBytes shape emitted by KMS-rooted signers
/// (`examples/aws-kms-signer/`, see ADR-009).
///
/// The verifier expects:
/// - `public_key` as an SPKI-encoded DER blob (this is what AWS KMS
///   `GetPublicKey` returns natively).
/// - `signature` as the DER-encoded ECDSA signature pair (what AWS
///   KMS `Sign` returns natively when `SigningAlgorithm =
///   ECDSA_SHA_256`).
///
/// Gated by the `ecdsa-p256` cargo feature.
///
/// ## CWE-347 — malleability note
///
/// Standard ECDSA accepts both halves of the `(r, s)` / `(r, n-s)`
/// malleability pair. AWS KMS emits either form depending on the
/// internal nonce, so this verifier accepts both. Consumers MUST
/// NOT key bundle deduplication, replay-defence, or any uniqueness
/// invariant on `SignatureBytes::signature_hex` — two distinct hex
/// strings can validly authenticate the same payload+key. Use the
/// canonical payload hash or the bundle id as the dedup key
/// instead.
#[cfg(feature = "ecdsa-p256")]
pub struct EcdsaP256Verifier;

#[cfg(feature = "ecdsa-p256")]
impl EcdsaP256Verifier {
    /// Construct a new ECDSA-P256 verifier.
    pub fn new() -> Self {
        Self
    }
}

#[cfg(feature = "ecdsa-p256")]
impl Default for EcdsaP256Verifier {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(feature = "ecdsa-p256")]
impl Verifier for EcdsaP256Verifier {
    fn algorithm(&self) -> &str {
        "ecdsa-p256"
    }

    fn verify(
        &self,
        public_key: &[u8],
        payload: &[u8],
        signature: &[u8],
    ) -> Result<(), VerifyError> {
        use p256::ecdsa::signature::Verifier as _;
        use p256::ecdsa::{DerSignature, VerifyingKey};
        use p256::pkcs8::DecodePublicKey;

        let verifying = VerifyingKey::from_public_key_der(public_key)
            .map_err(|e| VerifyError::InvalidKey(format!("p256 SPKI decode: {e}")))?;
        // Malformed signature bytes are a SignatureMismatch per the
        // Verifier trait contract (sibling Ed25519Verifier does the
        // same on wrong-length sigs); InvalidKey is reserved for the
        // trust-store key-bytes branch above.
        let sig =
            DerSignature::from_bytes(signature).map_err(|_| VerifyError::SignatureMismatch)?;
        verifying
            .verify(payload, &sig)
            .map_err(|_| VerifyError::SignatureMismatch)
    }
}

/// In-process key resolver backed by a static `HashMap`.
///
/// Load trusted keys at boot from a config file / KMS / environment
/// once; then look them up by `key_id` at verify time. For rotation,
/// hold multiple `(key_id, key_bytes)` pairs simultaneously and remove
/// retired ones on a schedule that exceeds the longest-lived bundle
/// retention period.
#[derive(Default)]
pub struct StaticKeyResolver {
    keys: HashMap<String, Vec<u8>>,
}

impl StaticKeyResolver {
    /// Build an empty resolver.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a `(key_id, public_key_bytes)` pair to the trust store.
    /// Returns `self` for chained construction.
    pub fn with(mut self, key_id: impl Into<String>, public_key: Vec<u8>) -> Self {
        self.keys.insert(key_id.into(), public_key);
        self
    }

    /// Add a key after construction.
    pub fn insert(&mut self, key_id: impl Into<String>, public_key: Vec<u8>) {
        self.keys.insert(key_id.into(), public_key);
    }
}

impl KeyResolver for StaticKeyResolver {
    fn resolve(&self, key_id: &str) -> Option<Vec<u8>> {
        self.keys.get(key_id).cloned()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ed25519_dalek::{Signer as DalekSigner, SigningKey};
    use rand::rngs::OsRng;

    #[cfg(feature = "ecdsa-p256")]
    mod ecdsa_p256 {
        use super::super::{EcdsaP256Verifier, Verifier, VerifyError};
        use p256::ecdsa::signature::Signer as _;
        use p256::ecdsa::{DerSignature, SigningKey};
        use p256::pkcs8::EncodePublicKey;

        fn fresh_signer_and_pub_der() -> (SigningKey, Vec<u8>) {
            let signing = SigningKey::random(&mut rand::rngs::OsRng);
            let pub_der = signing
                .verifying_key()
                .to_public_key_der()
                .unwrap()
                .as_bytes()
                .to_vec();
            (signing, pub_der)
        }

        #[test]
        fn ecdsa_verifier_advertises_algorithm_tag() {
            assert_eq!(EcdsaP256Verifier::new().algorithm(), "ecdsa-p256");
        }

        #[test]
        fn ecdsa_verifier_round_trips_a_real_signature() {
            let (signing, pub_der) = fresh_signer_and_pub_der();
            let payload = b"hello provenance";
            let sig: DerSignature = signing.sign(payload);
            EcdsaP256Verifier::new()
                .verify(&pub_der, payload, sig.as_bytes())
                .expect("valid ECDSA-P256 signature must verify");
        }

        #[test]
        fn ecdsa_verifier_rejects_tampered_payload() {
            let (signing, pub_der) = fresh_signer_and_pub_der();
            let sig: DerSignature = signing.sign(b"original");
            let err = EcdsaP256Verifier::new()
                .verify(&pub_der, b"tampered", sig.as_bytes())
                .unwrap_err();
            assert!(matches!(err, VerifyError::SignatureMismatch));
        }

        #[test]
        fn ecdsa_verifier_rejects_malformed_public_key() {
            let err = EcdsaP256Verifier::new()
                .verify(&[0u8; 8], b"payload", &[0u8; 70])
                .unwrap_err();
            assert!(matches!(err, VerifyError::InvalidKey(_)));
        }

        #[test]
        fn ecdsa_verifier_rejects_malformed_signature() {
            let (_, pub_der) = fresh_signer_and_pub_der();
            // Malformed signature bytes must be SignatureMismatch (per
            // Verifier trait contract); InvalidKey is reserved for the
            // trust-store key-bytes branch.
            let err = EcdsaP256Verifier::new()
                .verify(&pub_der, b"payload", &[0u8; 4])
                .unwrap_err();
            assert!(
                matches!(err, VerifyError::SignatureMismatch),
                "expected SignatureMismatch, got {err:?}"
            );
        }

        /// CWE-347 awareness — the verifier accepts BOTH halves of
        /// the ECDSA malleability pair because AWS KMS emits either
        /// form. Both must verify under the standard ECDSA equation.
        /// The mitigation against malleable-sig replay/dedup is
        /// documented at the type level: consumers MUST key
        /// uniqueness on the canonical payload hash, not on
        /// `signature_hex`.
        #[test]
        fn ecdsa_verifier_accepts_both_low_s_and_high_s_signatures() {
            use p256::ecdsa::{DerSignature, Signature};
            let (signing, pub_der) = fresh_signer_and_pub_der();
            let payload = b"malleability-acceptance";

            let sig_low: DerSignature = signing.sign(payload);
            let sig_low_decoded: Signature = sig_low.clone().try_into().expect("decode");
            let (r, s) = sig_low_decoded.split_scalars();
            let high_s = -*s.as_ref();
            let high_sig = Signature::from_scalars(*r.as_ref(), high_s).expect("high-S construct");
            let high_der: DerSignature = high_sig.into();

            EcdsaP256Verifier::new()
                .verify(&pub_der, payload, sig_low.as_bytes())
                .expect("low-S form must verify");
            EcdsaP256Verifier::new()
                .verify(&pub_der, payload, high_der.as_bytes())
                .expect("high-S form must verify (documented CWE-347 acceptance)");
        }
    }

    #[test]
    fn ed25519_verifier_round_trips_a_real_signature() {
        let mut csprng = OsRng;
        let signing = SigningKey::generate(&mut csprng);
        let verifying = signing.verifying_key();
        let payload = b"hello provenance";
        let sig = signing.sign(payload);

        let verifier = Ed25519Verifier::new();
        verifier
            .verify(verifying.as_bytes(), payload, &sig.to_bytes())
            .expect("valid signature must verify");
    }

    #[test]
    fn ed25519_verifier_rejects_tampered_payload() {
        let mut csprng = OsRng;
        let signing = SigningKey::generate(&mut csprng);
        let verifying = signing.verifying_key();
        let payload = b"original";
        let sig = signing.sign(payload);

        let verifier = Ed25519Verifier::new();
        let err = verifier
            .verify(verifying.as_bytes(), b"tampered", &sig.to_bytes())
            .unwrap_err();
        assert!(matches!(err, VerifyError::SignatureMismatch));
    }

    #[test]
    fn ed25519_verifier_rejects_wrong_key_length() {
        let verifier = Ed25519Verifier::new();
        let err = verifier
            .verify(&[0u8; 16], b"payload", &[0u8; 64])
            .unwrap_err();
        assert!(matches!(err, VerifyError::InvalidKey(_)));
    }

    #[test]
    fn ed25519_verifier_rejects_wrong_signature_length() {
        let mut csprng = OsRng;
        let key = SigningKey::generate(&mut csprng).verifying_key();
        let verifier = Ed25519Verifier::new();
        let err = verifier
            .verify(key.as_bytes(), b"payload", &[0u8; 32])
            .unwrap_err();
        assert!(matches!(err, VerifyError::SignatureMismatch));
    }

    #[test]
    fn static_key_resolver_returns_inserted_key() {
        let r = StaticKeyResolver::new().with("k1", vec![1, 2, 3]);
        assert_eq!(r.resolve("k1"), Some(vec![1, 2, 3]));
        assert!(r.resolve("unknown").is_none());
    }

    #[test]
    fn ed25519_verifier_advertises_algorithm_tag() {
        assert_eq!(Ed25519Verifier::new().algorithm(), "ed25519");
    }
}