Skip to main content

matter_crypto/case/
signer.rs

1//! Pluggable ECDSA-P256-SHA256 signer for CASE.
2//!
3//! M4.1: `SignerError`, `CaseSigner` trait, and `RingSigner` (the in-tree
4//! production-default implementation).
5//!
6//! # Signing algorithm
7//!
8//! `RingSigner::sign_p256_sha256` uses the `p256` crate's ECDSA implementation,
9//! which follows RFC 6979 deterministic nonce generation. This is the same
10//! algorithm used by `@noble/curves` (JavaScript) and enables byte-for-byte
11//! reproducible test vectors captured by `cargo xtask capture-case`.
12//!
13//! `ring`'s `EcdsaKeyPair::sign` uses a hedged variant that mixes in random
14//! bytes, making signatures non-deterministic. We still use `ring` for key
15//! generation and public-key parsing (where it excels), but switch to `p256`
16//! for the signing step itself so that CASE byte-parity tests are possible.
17
18use matter_cert::PublicKey;
19use p256::ecdsa::{signature::Signer as EcdsaSigner, Signature, SigningKey};
20use p256::pkcs8::DecodePrivateKey;
21
22use crate::error::{Error, Result};
23
24/// Errors returned by a [`CaseSigner`] implementation.
25///
26/// This type is intentionally `#[non_exhaustive]` so that future backends
27/// (HSMs, software key stores, OS keychain) can add variants without breaking
28/// callers that only handle the existing arms.
29#[derive(Debug, thiserror::Error)]
30#[non_exhaustive]
31pub enum SignerError {
32    /// Signer hardware is not available (e.g., HSM disconnected).
33    #[error("signer hardware unavailable")]
34    Unavailable,
35    /// Signer explicitly rejected the operation (e.g., policy violation).
36    #[error("signer rejected the operation: {0}")]
37    Rejected(&'static str),
38    /// Internal signer error (e.g., ring returned an opaque failure).
39    #[error("internal signer error")]
40    Internal,
41}
42
43/// Pluggable ECDSA-P256-SHA256 signer for CASE.
44///
45/// The in-tree concrete implementation is [`RingSigner`], which wraps
46/// a P-256 private key and produces RFC 6979 deterministic signatures.
47/// Alternative backends (HSM, OS keychain, software key store) can implement
48/// this trait without touching core CASE code.
49pub trait CaseSigner: Send + Sync + std::fmt::Debug {
50    /// Sign `message` with the NOC's private ECDSA-P256 key.
51    /// Returns raw 64-byte r||s signature (Matter wire format).
52    ///
53    /// # Errors
54    ///
55    /// Returns [`SignerError`] if the signing operation fails (hardware
56    /// unavailable, policy rejection, or internal error).
57    fn sign_p256_sha256(&self, message: &[u8]) -> std::result::Result<[u8; 64], SignerError>;
58
59    /// The 65-byte SEC1-uncompressed P-256 public key matching the NOC.
60    fn public_key(&self) -> &PublicKey;
61}
62
63// ── RingSigner ────────────────────────────────────────────────────────────────
64
65use ring::rand::SystemRandom;
66use ring::signature::{EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING};
67
68/// [`CaseSigner`] backed by the `p256` crate's RFC 6979 deterministic ECDSA.
69///
70/// The name `RingSigner` is kept for API stability (this type was introduced in
71/// M4.1). Key generation still uses `ring` (`[Self::generate]`), but signing
72/// uses `p256::ecdsa::SigningKey` (RFC 6979 deterministic) and the public key
73/// is derived from the same `p256` signing key. This enables byte-for-byte
74/// test-vector parity with `@noble/curves` (JavaScript) as captured by
75/// `cargo xtask capture-case`.
76///
77/// Use [`Self::from_pkcs8`] to load existing keys (e.g., from a fabric
78/// store), or [`Self::generate`] in tests to mint a fresh keypair.
79pub struct RingSigner {
80    /// RFC 6979 deterministic signing key (p256 crate).
81    signing_key: SigningKey,
82    public_key: PublicKey,
83}
84
85impl std::fmt::Debug for RingSigner {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.debug_struct("RingSigner")
88            .field("public_key", &self.public_key)
89            .field("signing_key", &"<p256::ecdsa::SigningKey>")
90            .finish_non_exhaustive()
91    }
92}
93
94impl RingSigner {
95    /// Construct from PKCS#8 v1 encoded private key bytes.
96    ///
97    /// Parses the PKCS#8 DER bytes using `p256::ecdsa::SigningKey`, which
98    /// also derives the matching public key. The 65-byte SEC1-uncompressed
99    /// public key is extracted from the `p256` verifying key.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`Error::SigningFailed`] with [`SignerError::Internal`] if the
104    /// bytes are not a valid PKCS#8-encoded P-256 key.
105    pub fn from_pkcs8(pkcs8_bytes: &[u8]) -> Result<Self> {
106        let signing_key = SigningKey::from_pkcs8_der(pkcs8_bytes)
107            .map_err(|_| Error::SigningFailed(SignerError::Internal))?;
108
109        // Derive the 65-byte SEC1-uncompressed public key from the signing key.
110        let verifying_key = signing_key.verifying_key();
111        let encoded = verifying_key.to_encoded_point(false); // false = uncompressed
112        let encoded_bytes = encoded.as_bytes();
113        if encoded_bytes.len() != 65 {
114            return Err(Error::SigningFailed(SignerError::Internal));
115        }
116        let mut pk_bytes = [0u8; 65];
117        pk_bytes.copy_from_slice(encoded_bytes);
118        let public_key =
119            PublicKey::new(pk_bytes).map_err(|_| Error::SigningFailed(SignerError::Internal))?;
120
121        Ok(Self {
122            signing_key,
123            public_key,
124        })
125    }
126
127    /// Generate a fresh ECDSA-P256 keypair. Returns the signer plus the
128    /// PKCS#8 bytes so the caller can persist them.
129    ///
130    /// Key generation uses `ring`'s `EcdsaKeyPair::generate_pkcs8` for its
131    /// well-audited RNG plumbing. The resulting PKCS#8 is then loaded via
132    /// `from_pkcs8` so that signing uses the deterministic `p256` path.
133    ///
134    /// # Errors
135    ///
136    /// Returns [`Error::SigningFailed`] with [`SignerError::Internal`] if the
137    /// OS RNG or key-generation step fails (extremely unlikely in practice).
138    pub fn generate() -> Result<(Self, Vec<u8>)> {
139        let rng = SystemRandom::new();
140        let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
141            .map_err(|_| Error::SigningFailed(SignerError::Internal))?;
142        let pkcs8_vec = pkcs8.as_ref().to_vec();
143        let signer = Self::from_pkcs8(&pkcs8_vec)?;
144        Ok((signer, pkcs8_vec))
145    }
146}
147
148impl CaseSigner for RingSigner {
149    /// Sign `message` using RFC 6979 deterministic ECDSA-P256-SHA256.
150    ///
151    /// Uses `p256::ecdsa::SigningKey::sign` which follows RFC 6979 for nonce
152    /// generation (same algorithm as `@noble/curves` in JavaScript), enabling
153    /// byte-for-byte reproducible test vectors captured by
154    /// `cargo xtask capture-case`.
155    ///
156    /// The signature is low-s normalized (`s ≤ n/2`), matching `@noble/curves`
157    /// behavior. Both `s` and `n - s` are valid ECDSA signatures; this ensures
158    /// the Rust output is byte-identical with matter.js.
159    ///
160    /// The returned signature is in IEEE P1363 compact format (raw r||s, 64 bytes),
161    /// which is the Matter wire format for ECDSA signatures.
162    ///
163    /// # Errors
164    ///
165    /// Returns [`SignerError::Internal`] on signing failure (not expected for
166    /// valid keys and any message length).
167    fn sign_p256_sha256(&self, message: &[u8]) -> std::result::Result<[u8; 64], SignerError> {
168        // `Signer::sign` hashes `message` with SHA-256 internally (the `sha256`
169        // feature of the `p256` crate wires this up via `ecdsa::hazmat::SignPrimitive`
170        // + `DigestSigner<sha2::Sha256, _>`). The returned `Signature` is in the
171        // compact (IEEE P1363) format: 32-byte big-endian r || 32-byte big-endian s.
172        let sig: Signature = self.signing_key.sign(message);
173        // Apply low-s normalization (s → n - s when s > n/2).
174        //
175        // `@noble/curves` always produces low-s signatures (s ≤ n/2).
176        // `p256::ecdsa::SigningKey::sign` uses RFC 6979 for nonce generation but
177        // does NOT guarantee low-s — it may produce s > n/2 for some keys and
178        // messages. Both forms are mathematically equivalent and valid per ECDSA,
179        // but byte-parity tests require the same representation as matter.js.
180        //
181        // `Signature::normalize_s()` returns `Some(normalized)` when s > n/2
182        // (and flips s to n - s), or `None` when s is already low.
183        let sig = sig.normalize_s().unwrap_or(sig);
184        // `Signature::to_bytes()` returns the compact 64-byte r||s GenericArray.
185        Ok(sig.to_bytes().into())
186    }
187
188    fn public_key(&self) -> &PublicKey {
189        &self.public_key
190    }
191}
192
193#[cfg(test)]
194#[allow(clippy::unwrap_used, clippy::expect_used)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn generate_and_round_trip_through_pkcs8() {
200        let (signer1, pkcs8) = RingSigner::generate().unwrap();
201        let signer2 = RingSigner::from_pkcs8(&pkcs8).unwrap();
202        assert_eq!(
203            signer1.public_key().as_bytes(),
204            signer2.public_key().as_bytes()
205        );
206    }
207
208    #[test]
209    fn signing_produces_64_byte_signature() {
210        let (signer, _) = RingSigner::generate().unwrap();
211        let sig = signer.sign_p256_sha256(b"hello world").unwrap();
212        assert_eq!(sig.len(), 64);
213    }
214
215    #[test]
216    fn signing_same_message_twice_is_deterministic() {
217        // p256's ECDSA uses RFC 6979 deterministic nonce generation, so signing
218        // the same message twice produces identical signatures. This property is
219        // required for byte-parity test vectors captured by `cargo xtask capture-case`.
220        let (signer, _) = RingSigner::generate().unwrap();
221        let a = signer.sign_p256_sha256(b"same").unwrap();
222        let b = signer.sign_p256_sha256(b"same").unwrap();
223        assert_eq!(a, b, "RFC 6979 signing must be deterministic");
224    }
225
226    #[test]
227    fn signing_produces_verifiable_signature() {
228        use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_FIXED};
229        let (signer, _) = RingSigner::generate().unwrap();
230        let msg = b"verify me";
231        let sig = signer.sign_p256_sha256(msg).unwrap();
232        let pk = UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, signer.public_key().as_bytes());
233        pk.verify(msg, &sig).expect("signature must verify");
234    }
235
236    #[test]
237    fn pkcs8_from_invalid_bytes_returns_error() {
238        let err = RingSigner::from_pkcs8(b"garbage").unwrap_err();
239        assert!(matches!(err, Error::SigningFailed(SignerError::Internal)));
240    }
241
242    #[test]
243    fn signing_produces_low_s_signature() {
244        // Signatures must have s ≤ n/2 (low-s normalized) to match @noble/curves output.
245        // P-256 curve order n split into two u128 halves (big-endian):
246        // n = 0xFFFFFFFF_00000000_FFFFFFFF_FFFFFFFF_BCE6FAAD_A7179E84_F3B9CAC2_FC632551
247        let n_hi = 0xFFFF_FFFF_0000_0000_FFFF_FFFF_FFFF_FFFFu128;
248        let n_lo = 0xBCE6_FAAD_A717_9E84_F3B9_CAC2_FC63_2551u128;
249
250        let (signer, _) = RingSigner::generate().unwrap();
251        // Sign several messages to exercise different (r, s) pairs.
252        for msg in &[b"alpha" as &[u8], b"beta", b"gamma", b"delta", b"epsilon"] {
253            let sig = signer.sign_p256_sha256(msg).unwrap();
254            let s_hi = u128::from_be_bytes(sig[32..48].try_into().unwrap());
255            let s_lo = u128::from_be_bytes(sig[48..64].try_into().unwrap());
256            // n/2 = (n - 1) / 2; s ≤ n/2 iff 2*s ≤ n. Perform a 256-bit comparison.
257            // n is odd so n/2 = 0x7FFF...DE73... We check s_hi, then s_lo.
258            let half_hi = (n_hi >> 1) | ((n_lo >> 127) << 127); // carry bit from lo to hi
259            let half_lo = n_lo >> 1;
260            let is_low_s = s_hi < half_hi || (s_hi == half_hi && s_lo <= half_lo);
261            assert!(
262                is_low_s,
263                "signature s must be ≤ n/2 (low-s) for message {msg:?}: s = {s_hi:016x}{s_lo:016x}",
264            );
265        }
266    }
267}