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}