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}