Skip to main content

matter_cert/
public_key.rs

1//! Matter EC public key (P-256, uncompressed point).
2//!
3//! M2.1 ships only the byte container; M2.2 adds signature verification
4//! via `ring`.
5
6use core::fmt;
7
8use crate::error::{Error, Result};
9
10/// A 65-byte uncompressed P-256 public key: `0x04 || X(32) || Y(32)`.
11#[derive(Clone, PartialEq, Eq, Hash)]
12pub struct PublicKey([u8; 65]);
13
14impl PublicKey {
15    /// Construct from raw bytes. Validates the `0x04` uncompressed-point
16    /// prefix; rejects compressed or hybrid encodings.
17    ///
18    /// # Errors
19    ///
20    /// Returns [`Error::BadPublicKeyPrefix`] if `bytes[0] != 0x04`.
21    pub fn new(bytes: [u8; 65]) -> Result<Self> {
22        if bytes[0] != 0x04 {
23            return Err(Error::BadPublicKeyPrefix);
24        }
25        Ok(Self(bytes))
26    }
27
28    /// Construct from a byte slice. Rejects wrong-length input.
29    ///
30    /// # Errors
31    ///
32    /// Returns [`Error::WrongPublicKeyLength`] if `slice.len() != 65`,
33    /// or [`Error::BadPublicKeyPrefix`] if the first byte is not `0x04`.
34    pub fn from_slice(slice: &[u8]) -> Result<Self> {
35        let bytes: [u8; 65] = slice
36            .try_into()
37            .map_err(|_| Error::WrongPublicKeyLength(slice.len()))?;
38        Self::new(bytes)
39    }
40
41    /// The raw 65-byte uncompressed point.
42    #[must_use]
43    pub fn as_bytes(&self) -> &[u8; 65] {
44        &self.0
45    }
46
47    /// Verify an ECDSA-P256-SHA256 signature against a message.
48    ///
49    /// The signature is the 64-byte raw `r || s` form; ring's
50    /// `ECDSA_P256_SHA256_FIXED` algorithm consumes exactly this layout
51    /// and computes the SHA-256 hash internally.
52    ///
53    /// # Errors
54    ///
55    /// Returns [`Error::SignatureVerificationFailed`] if the signature
56    /// is not a valid signature over `message` by the private key
57    /// corresponding to this public key. The error variant does NOT
58    /// distinguish between malformed signatures, wrong-key signatures,
59    /// and signatures over different messages — ring deliberately
60    /// keeps this opaque to avoid side-channel leaks.
61    pub fn verify(&self, message: &[u8], signature: &crate::signature::Signature) -> Result<()> {
62        use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_FIXED};
63        let key = UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, &self.0[..]);
64        key.verify(message, signature.as_bytes())
65            .map_err(|_| Error::SignatureVerificationFailed)
66    }
67}
68
69impl fmt::Debug for PublicKey {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(
72            f,
73            "PublicKey({:02x}{:02x}{:02x}{:02x}…)",
74            self.0[1], self.0[2], self.0[3], self.0[4]
75        )
76    }
77}
78
79#[cfg(test)]
80#[allow(clippy::unwrap_used)] // Test-code carve-out: see CLAUDE.md.
81mod tests {
82    use ring::rand::SystemRandom;
83    use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_FIXED_SIGNING};
84
85    use super::*;
86    use crate::signature::Signature;
87
88    /// Generate a fresh test keypair via ring. Returns the public key
89    /// (in our `PublicKey` newtype) and the key pair (for signing).
90    fn make_keypair() -> (PublicKey, EcdsaKeyPair) {
91        let rng = SystemRandom::new();
92        let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
93        let key_pair =
94            EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
95                .unwrap();
96        let our_pub = PublicKey::from_slice(key_pair.public_key().as_ref()).unwrap();
97        (our_pub, key_pair)
98    }
99
100    fn sign(key_pair: &EcdsaKeyPair, message: &[u8]) -> Signature {
101        let rng = SystemRandom::new();
102        let sig = key_pair.sign(&rng, message).unwrap();
103        Signature::from_slice(sig.as_ref()).unwrap()
104    }
105
106    #[test]
107    fn verify_accepts_correct_signature() {
108        let (pub_key, key_pair) = make_keypair();
109        let message = b"matter-cert phase 2 test message";
110        let sig = sign(&key_pair, message);
111        assert!(pub_key.verify(message, &sig).is_ok());
112    }
113
114    #[test]
115    fn verify_rejects_signature_from_different_key() {
116        let (_, key_a) = make_keypair();
117        let (pub_b, _) = make_keypair();
118        let message = b"signed by A, verified against B";
119        let sig = sign(&key_a, message);
120        let err = pub_b.verify(message, &sig).unwrap_err();
121        assert!(matches!(err, Error::SignatureVerificationFailed));
122    }
123
124    #[test]
125    fn verify_rejects_signature_for_different_message() {
126        let (pub_key, key_pair) = make_keypair();
127        let sig = sign(&key_pair, b"signed message");
128        let err = pub_key.verify(b"different message", &sig).unwrap_err();
129        assert!(matches!(err, Error::SignatureVerificationFailed));
130    }
131
132    #[test]
133    fn verify_rejects_tampered_signature() {
134        let (pub_key, key_pair) = make_keypair();
135        let message = b"this is the original message";
136        let mut sig = sign(&key_pair, message);
137        let mut raw = *sig.as_bytes();
138        raw[0] ^= 0x01;
139        sig = Signature::from_slice(&raw).unwrap();
140        let err = pub_key.verify(message, &sig).unwrap_err();
141        assert!(matches!(err, Error::SignatureVerificationFailed));
142    }
143
144    #[test]
145    fn new_rejects_non_0x04_prefix() {
146        let mut bytes = [0u8; 65];
147        bytes[0] = 0x02;
148        assert!(matches!(
149            PublicKey::new(bytes),
150            Err(Error::BadPublicKeyPrefix)
151        ));
152    }
153
154    #[test]
155    fn new_accepts_0x04_prefix() {
156        let mut bytes = [0u8; 65];
157        bytes[0] = 0x04;
158        bytes[1] = 0xAB;
159        let key = PublicKey::new(bytes).unwrap();
160        assert_eq!(key.as_bytes(), &bytes);
161    }
162
163    #[test]
164    fn from_slice_rejects_wrong_length() {
165        let short = [0x04u8; 10];
166        assert!(matches!(
167            PublicKey::from_slice(&short),
168            Err(Error::WrongPublicKeyLength(10))
169        ));
170    }
171
172    #[test]
173    fn debug_format_does_not_leak_full_key() {
174        let mut bytes = [0u8; 65];
175        bytes[0] = 0x04;
176        bytes[1] = 0xAB;
177        bytes[2] = 0xCD;
178        bytes[3] = 0xEF;
179        bytes[4] = 0x12;
180        let key = PublicKey::new(bytes).unwrap();
181        let s = format!("{key:?}");
182        assert!(s.contains("abcdef12"));
183        assert!(!s.contains("00000000"));
184    }
185}