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        // `to_sec1_point` is the p256 0.14 name for 0.13's `to_encoded_point`;
111        // same SEC1 encoding, same `false = uncompressed` flag.
112        let encoded = signing_key.verifying_key().to_sec1_point(false);
113        let encoded_bytes = encoded.as_bytes();
114        if encoded_bytes.len() != 65 {
115            return Err(Error::SigningFailed(SignerError::Internal));
116        }
117        let mut pk_bytes = [0u8; 65];
118        pk_bytes.copy_from_slice(encoded_bytes);
119        let public_key =
120            PublicKey::new(pk_bytes).map_err(|_| Error::SigningFailed(SignerError::Internal))?;
121
122        Ok(Self {
123            signing_key,
124            public_key,
125        })
126    }
127
128    /// Generate a fresh ECDSA-P256 keypair. Returns the signer plus the
129    /// PKCS#8 bytes so the caller can persist them.
130    ///
131    /// Key generation uses `ring`'s `EcdsaKeyPair::generate_pkcs8` for its
132    /// well-audited RNG plumbing. The resulting PKCS#8 is then loaded via
133    /// `from_pkcs8` so that signing uses the deterministic `p256` path.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`Error::SigningFailed`] with [`SignerError::Internal`] if the
138    /// OS RNG or key-generation step fails (extremely unlikely in practice).
139    pub fn generate() -> Result<(Self, Vec<u8>)> {
140        let rng = SystemRandom::new();
141        let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
142            .map_err(|_| Error::SigningFailed(SignerError::Internal))?;
143        let pkcs8_vec = pkcs8.as_ref().to_vec();
144        let signer = Self::from_pkcs8(&pkcs8_vec)?;
145        Ok((signer, pkcs8_vec))
146    }
147}
148
149impl CaseSigner for RingSigner {
150    /// Sign `message` using RFC 6979 deterministic ECDSA-P256-SHA256.
151    ///
152    /// Uses `p256::ecdsa::SigningKey::sign` which follows RFC 6979 for nonce
153    /// generation (same algorithm as `@noble/curves` in JavaScript), enabling
154    /// byte-for-byte reproducible test vectors captured by
155    /// `cargo xtask capture-case`.
156    ///
157    /// The signature is low-s normalized (`s ≤ n/2`), matching `@noble/curves`
158    /// behavior. Both `s` and `n - s` are valid ECDSA signatures; this ensures
159    /// the Rust output is byte-identical with matter.js.
160    ///
161    /// The returned signature is in IEEE P1363 compact format (raw r||s, 64 bytes),
162    /// which is the Matter wire format for ECDSA signatures.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`SignerError::Internal`] on signing failure (not expected for
167    /// valid keys and any message length).
168    fn sign_p256_sha256(&self, message: &[u8]) -> std::result::Result<[u8; 64], SignerError> {
169        // `Signer::sign` hashes `message` with SHA-256 internally (the `sha256`
170        // feature of the `p256` crate wires this up via `ecdsa::hazmat::SignPrimitive`
171        // + `DigestSigner<sha2::Sha256, _>`). The returned `Signature` is in the
172        // compact (IEEE P1363) format: 32-byte big-endian r || 32-byte big-endian s.
173        let sig: Signature = self.signing_key.sign(message);
174        // Apply low-s normalization (s → n - s when s > n/2).
175        //
176        // `@noble/curves` always produces low-s signatures (s ≤ n/2).
177        // `p256::ecdsa::SigningKey::sign` uses RFC 6979 for nonce generation but
178        // does NOT guarantee low-s — it may produce s > n/2 for some keys and
179        // messages. Both forms are mathematically equivalent and valid per ECDSA,
180        // but byte-parity tests require the same representation as matter.js.
181        //
182        // `Signature::normalize_s()` flips s to n - s when s > n/2 and returns
183        // the signature unchanged when s is already low. (In p256 0.13 it
184        // returned `Option<Signature>` — `Some` only on the flip — so the call
185        // site read `sig.normalize_s().unwrap_or(sig)`. p256 0.14 returns
186        // `Signature` directly, folding the "already low" case in. Same result
187        // in both branches.)
188        let sig = sig.normalize_s();
189        // `Signature::to_bytes()` returns the compact 64-byte r||s byte array.
190        Ok(sig.to_bytes().into())
191    }
192
193    fn public_key(&self) -> &PublicKey {
194        &self.public_key
195    }
196}
197
198#[cfg(test)]
199#[allow(clippy::unwrap_used, clippy::expect_used)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn generate_and_round_trip_through_pkcs8() {
205        let (signer1, pkcs8) = RingSigner::generate().unwrap();
206        let signer2 = RingSigner::from_pkcs8(&pkcs8).unwrap();
207        assert_eq!(
208            signer1.public_key().as_bytes(),
209            signer2.public_key().as_bytes()
210        );
211    }
212
213    #[test]
214    fn signing_produces_64_byte_signature() {
215        let (signer, _) = RingSigner::generate().unwrap();
216        let sig = signer.sign_p256_sha256(b"hello world").unwrap();
217        assert_eq!(sig.len(), 64);
218    }
219
220    #[test]
221    fn signing_same_message_twice_is_deterministic() {
222        // p256's ECDSA uses RFC 6979 deterministic nonce generation, so signing
223        // the same message twice produces identical signatures. This property is
224        // required for byte-parity test vectors captured by `cargo xtask capture-case`.
225        let (signer, _) = RingSigner::generate().unwrap();
226        let a = signer.sign_p256_sha256(b"same").unwrap();
227        let b = signer.sign_p256_sha256(b"same").unwrap();
228        assert_eq!(a, b, "RFC 6979 signing must be deterministic");
229    }
230
231    #[test]
232    fn signing_produces_verifiable_signature() {
233        use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_FIXED};
234        let (signer, _) = RingSigner::generate().unwrap();
235        let msg = b"verify me";
236        let sig = signer.sign_p256_sha256(msg).unwrap();
237        let pk = UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, signer.public_key().as_bytes());
238        pk.verify(msg, &sig).expect("signature must verify");
239    }
240
241    #[test]
242    fn pkcs8_from_invalid_bytes_returns_error() {
243        let err = RingSigner::from_pkcs8(b"garbage").unwrap_err();
244        assert!(matches!(err, Error::SigningFailed(SignerError::Internal)));
245    }
246
247    #[test]
248    fn signing_produces_low_s_signature() {
249        // Signatures must have s ≤ n/2 (low-s normalized) to match @noble/curves output.
250        // P-256 curve order n split into two u128 halves (big-endian):
251        // n = 0xFFFFFFFF_00000000_FFFFFFFF_FFFFFFFF_BCE6FAAD_A7179E84_F3B9CAC2_FC632551
252        let n_hi = 0xFFFF_FFFF_0000_0000_FFFF_FFFF_FFFF_FFFFu128;
253        let n_lo = 0xBCE6_FAAD_A717_9E84_F3B9_CAC2_FC63_2551u128;
254
255        let (signer, _) = RingSigner::generate().unwrap();
256        // Sign several messages to exercise different (r, s) pairs.
257        for msg in &[b"alpha" as &[u8], b"beta", b"gamma", b"delta", b"epsilon"] {
258            let sig = signer.sign_p256_sha256(msg).unwrap();
259            let s_hi = u128::from_be_bytes(sig[32..48].try_into().unwrap());
260            let s_lo = u128::from_be_bytes(sig[48..64].try_into().unwrap());
261            // n/2 = (n - 1) / 2; s ≤ n/2 iff 2*s ≤ n. Perform a 256-bit comparison.
262            // n is odd so n/2 = 0x7FFF...DE73... We check s_hi, then s_lo.
263            let half_hi = (n_hi >> 1) | ((n_lo >> 127) << 127); // carry bit from lo to hi
264            let half_lo = n_lo >> 1;
265            let is_low_s = s_hi < half_hi || (s_hi == half_hi && s_lo <= half_lo);
266            assert!(
267                is_low_s,
268                "signature s must be ≤ n/2 (low-s) for message {msg:?}: s = {s_hi:016x}{s_lo:016x}",
269            );
270        }
271    }
272}