Skip to main content

ic_agent/identity/
prime256v1.rs

1use crate::{agent::EnvelopeContent, export::Principal, Identity, Signature};
2
3#[cfg(feature = "pem")]
4use crate::identity::error::PemError;
5
6use p256::{
7    ecdsa::{self, signature::Signer, SigningKey, VerifyingKey},
8    pkcs8::{Document, EncodePublicKey},
9    SecretKey,
10};
11#[cfg(feature = "pem")]
12use std::path::Path;
13
14use super::Delegation;
15
16/// A cryptographic identity based on the Prime256v1 elliptic curve.
17///
18/// The caller will be represented via [`Principal::self_authenticating`], which contains the SHA-224 hash of the public key.
19#[derive(Clone, Debug)]
20pub struct Prime256v1Identity {
21    private_key: SigningKey,
22    _public_key: VerifyingKey,
23    der_encoded_public_key: Document,
24}
25
26impl Prime256v1Identity {
27    /// Creates an identity from a PEM file. Shorthand for calling `from_pem` with `std::fs::read`.
28    #[cfg(feature = "pem")]
29    pub fn from_pem_file<P: AsRef<Path>>(file_path: P) -> Result<Self, PemError> {
30        Self::from_pem(std::fs::read(file_path)?)
31    }
32
33    /// Creates an identity from a PEM-encoded private key (SEC1 or PKCS#8).
34    ///
35    /// Accepts keys in SEC1 ("EC PRIVATE KEY") or PKCS#8 ("PRIVATE KEY") format.
36    #[cfg(feature = "pem")]
37    pub fn from_pem<B: AsRef<[u8]>>(pem_contents: B) -> Result<Self, PemError> {
38        use pkcs8::{AssociatedOid, PrivateKeyInfo};
39        use sec1::{pem::PemLabel, EcPrivateKey};
40
41        const EC_PARAMETERS: &str = "EC PARAMETERS";
42        const PRIME256V1: &[u8] = b"\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07";
43
44        let contents = pem_contents.as_ref();
45
46        for pem in pem::parse_many(contents)? {
47            if pem.tag() == EC_PARAMETERS && pem.contents() != PRIME256V1 {
48                return Err(PemError::UnsupportedKeyCurve(
49                    "prime256v1".to_string(),
50                    pem.contents().to_vec(),
51                ));
52            }
53
54            if pem.tag() == EcPrivateKey::PEM_LABEL {
55                // SEC1 "EC PRIVATE KEY" format
56                let private_key = SecretKey::from_sec1_der(pem.contents())
57                    .map_err(|_| pkcs8::Error::KeyMalformed)?;
58                return Ok(Self::from_private_key(private_key));
59            }
60
61            if pem.tag() == PrivateKeyInfo::PEM_LABEL {
62                // PKCS#8 "PRIVATE KEY" format
63                let key_bytes = super::parse_ec_pkcs8_key_bytes(
64                    pem.contents(),
65                    p256::NistP256::OID,
66                    "prime256v1",
67                )?;
68                let private_key =
69                    SecretKey::from_sec1_der(&key_bytes).map_err(|_| pkcs8::Error::KeyMalformed)?;
70                return Ok(Self::from_private_key(private_key));
71            }
72        }
73        Err(pem::PemError::MissingData.into())
74    }
75
76    /// Creates an identity from a private key.
77    pub fn from_private_key(private_key: SecretKey) -> Self {
78        let public_key = private_key.public_key();
79        let der_encoded_public_key = public_key
80            .to_public_key_der()
81            .expect("Cannot DER encode prime256v1 public key.");
82        Self {
83            private_key: private_key.into(),
84            _public_key: public_key.into(),
85            der_encoded_public_key,
86        }
87    }
88}
89
90impl Identity for Prime256v1Identity {
91    fn sender(&self) -> Result<Principal, String> {
92        Ok(Principal::self_authenticating(
93            self.der_encoded_public_key.as_ref(),
94        ))
95    }
96
97    fn public_key(&self) -> Option<Vec<u8>> {
98        Some(self.der_encoded_public_key.as_ref().to_vec())
99    }
100
101    fn sign(&self, content: &EnvelopeContent) -> Result<Signature, String> {
102        self.sign_arbitrary(&content.to_request_id().signable())
103    }
104
105    fn sign_delegation(&self, content: &Delegation) -> Result<Signature, String> {
106        self.sign_arbitrary(&content.signable())
107    }
108
109    fn sign_arbitrary(&self, content: &[u8]) -> Result<Signature, String> {
110        let ecdsa_sig: ecdsa::Signature = self
111            .private_key
112            .try_sign(content)
113            .map_err(|err| format!("Cannot create prime256v1 signature: {err}"))?;
114        let r = ecdsa_sig.r().as_ref().to_bytes();
115        let s = ecdsa_sig.s().as_ref().to_bytes();
116        let mut bytes = [0u8; 64];
117        if r.len() > 32 || s.len() > 32 {
118            return Err("Cannot create prime256v1 signature: malformed signature.".to_string());
119        }
120        bytes[(32 - r.len())..32].clone_from_slice(&r);
121        bytes[32 + (32 - s.len())..].clone_from_slice(&s);
122        let signature = Some(bytes.to_vec());
123        let public_key = self.public_key();
124        Ok(Signature {
125            public_key,
126            signature,
127            delegations: None,
128        })
129    }
130}
131
132#[cfg(feature = "pem")]
133#[cfg(test)]
134mod test {
135    use super::*;
136    use candid::Encode;
137    use p256::{
138        ecdsa::{signature::Verifier, Signature},
139        elliptic_curve::PrimeField,
140        Scalar,
141    };
142
143    // WRONG_CURVE_IDENTITY_FILE is generated from the following command:
144    // > openssl ecparam -name secp160r2 -genkey
145    // it uses the secp160r2 curve instead of prime256v1 and should
146    // therefore be rejected by Prime256v1Identity when loading an identity
147    const WRONG_CURVE_IDENTITY_FILE: &str = "\
148-----BEGIN EC PARAMETERS-----
149BgUrgQQAHg==
150-----END EC PARAMETERS-----
151-----BEGIN EC PRIVATE KEY-----
152MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR
153oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ==
154-----END EC PRIVATE KEY-----
155";
156
157    // WRONG_CURVE_IDENTITY_FILE_NO_PARAMS is generated from the following command:
158    // > openssl ecparam -name secp160r2 -genkey -noout
159    // it uses the secp160r2 curve instead of prime256v1 and should
160    // therefore be rejected by Prime256v1Identity when loading an identity
161    const WRONG_CURVE_IDENTITY_FILE_NO_PARAMS: &str = "\
162-----BEGIN EC PRIVATE KEY-----
163MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR
164oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ==
165-----END EC PRIVATE KEY-----
166";
167
168    // IDENTITY_FILE was generated from the the following commands:
169    // > openssl ecparam -name prime256v1 -genkey -noout -out identity.pem
170    // > cat identity.pem
171    const IDENTITY_FILE: &str = "\
172-----BEGIN EC PRIVATE KEY-----
173MHcCAQEEIL1ybmbwx+uKYsscOZcv71MmKhrNqfPP0ke1unET5AY4oAoGCCqGSM49
174AwEHoUQDQgAEUbbZV4NerZTPWfbQ749/GNLu8TaH8BUS/I7/+ipsu+MPywfnBFIZ
175Sks4xGbA/ZbazsrMl4v446U5UIVxCGGaKw==
176-----END EC PRIVATE KEY-----
177";
178
179    // DER_ENCODED_PUBLIC_KEY was generated from the the following commands:
180    // > openssl ec -in identity.pem -pubout -outform DER -out public.der
181    // > hexdump -ve '1/1 "%.2x"' public.der
182    const DER_ENCODED_PUBLIC_KEY: &str = "3059301306072a8648ce3d020106082a8648ce3d0301070342000451b6d957835ead94cf59f6d0ef8f7f18d2eef13687f01512fc8efffa2a6cbbe30fcb07e70452194a4b38c466c0fd96dacecacc978bf8e3a53950857108619a2b";
183
184    #[test]
185    #[should_panic(expected = "UnsupportedKeyCurve")]
186    fn test_prime256v1_reject_wrong_curve() {
187        Prime256v1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE.as_bytes()).unwrap();
188    }
189
190    #[test]
191    #[should_panic(expected = "KeyMalformed")]
192    fn test_prime256v1_reject_wrong_curve_no_id() {
193        Prime256v1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE_NO_PARAMS.as_bytes()).unwrap();
194    }
195
196    // IDENTITY_FILE_PKCS8 is the same key as IDENTITY_FILE, converted to PKCS#8:
197    // > openssl pkcs8 -topk8 -nocrypt -in identity.pem -out identity_pkcs8.pem
198    const IDENTITY_FILE_PKCS8: &str = "\
199-----BEGIN PRIVATE KEY-----
200MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvXJuZvDH64piyxw5
201ly/vUyYqGs2p88/SR7W6cRPkBjihRANCAARRttlXg16tlM9Z9tDvj38Y0u7xNofw
202FRL8jv/6Kmy74w/LB+cEUhlKSzjEZsD9ltrOysyXi/jjpTlQhXEIYZor
203-----END PRIVATE KEY-----
204";
205
206    #[test]
207    fn test_prime256v1_public_key() {
208        // Create a prime256v1 identity from a PEM file.
209        let identity = Prime256v1Identity::from_pem(IDENTITY_FILE.as_bytes())
210            .expect("Cannot create prime256v1 identity from PEM file.");
211
212        // Assert the DER-encoded prime256v1 public key matches what we would expect.
213        assert!(DER_ENCODED_PUBLIC_KEY == hex::encode(identity.der_encoded_public_key));
214    }
215
216    #[test]
217    fn test_prime256v1_pkcs8_public_key() {
218        let identity = Prime256v1Identity::from_pem(IDENTITY_FILE_PKCS8.as_bytes())
219            .expect("Cannot create prime256v1 identity from PKCS#8 PEM file.");
220        assert_eq!(
221            DER_ENCODED_PUBLIC_KEY,
222            hex::encode(identity.der_encoded_public_key)
223        );
224    }
225
226    #[test]
227    #[should_panic(expected = "UnsupportedKeyCurve")]
228    fn test_prime256v1_pkcs8_reject_wrong_curve() {
229        // A PKCS#8 secp256k1 key must be rejected by Prime256v1Identity.
230        const SECP256K1_PKCS8: &str = "\
231-----BEGIN PRIVATE KEY-----
232MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgCDLudkRxUeRDhnUp2pvL
233xLDICLIoNCa1sQdMgz5Y14GhRANCAASA7zusnWjPN0y8nJlD4YAEOpTEYu+CcCdO
234VwidXc26G4+/g7dUbMwbN4E3d3bpxHEP31M+2by6jY67MqFKKroR
235-----END PRIVATE KEY-----
236";
237        Prime256v1Identity::from_pem(SECP256K1_PKCS8.as_bytes()).unwrap();
238    }
239
240    #[test]
241    fn test_prime256v1_signature() {
242        // Create a prime256v1 identity from a PEM file.
243        let identity = Prime256v1Identity::from_pem(IDENTITY_FILE.as_bytes())
244            .expect("Cannot create prime256v1 identity from PEM file.");
245
246        // Create a prime256v1 signature for a hello-world canister.
247        let message = EnvelopeContent::Call {
248            nonce: None,
249            ingress_expiry: 0,
250            sender: identity.sender().unwrap(),
251            canister_id: "bkyz2-fmaaa-aaaaa-qaaaq-cai".parse().unwrap(),
252            method_name: "greet".to_string(),
253            arg: Encode!(&"world").unwrap(),
254            sender_info: None,
255        };
256        let signature = identity
257            .sign(&message)
258            .expect("Cannot create prime256v1 signature.")
259            .signature
260            .expect("Cannot find prime256v1 signature bytes.");
261
262        // Import the prime256v1 signature.
263        let r: Scalar = Option::from(Scalar::from_repr(
264            <[u8; 32]>::try_from(&signature[0..32])
265                .expect("Cannot extract r component from prime256v1 signature bytes.")
266                .into(),
267        ))
268        .expect("Cannot extract r component from prime256v1 signature bytes.");
269        let s: Scalar = Option::from(Scalar::from_repr(
270            <[u8; 32]>::try_from(&signature[32..])
271                .expect("Cannot extract s component from prime256v1 signature bytes.")
272                .into(),
273        ))
274        .expect("Cannot extract s component from prime256v1 signature bytes.");
275        let ecdsa_sig = Signature::from_scalars(r, s)
276            .expect("Cannot create prime256v1 signature from r and s components.");
277
278        // Assert the prime256v1 signature is valid.
279        identity
280            ._public_key
281            .verify(&message.to_request_id().signable(), &ecdsa_sig)
282            .expect("Cannot verify prime256v1 signature.");
283    }
284}