ic_agent/identity/
secp256k1.rs

1use crate::{agent::EnvelopeContent, export::Principal, Identity, Signature};
2
3#[cfg(feature = "pem")]
4use crate::identity::error::PemError;
5
6use k256::{
7    ecdsa::{self, signature::Signer, SigningKey, VerifyingKey},
8    pkcs8::{Document, EncodePublicKey},
9    SecretKey,
10};
11#[cfg(feature = "pem")]
12use std::{fs::File, io, path::Path};
13
14use super::Delegation;
15
16/// A cryptographic identity based on the Secp256k1 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 Secp256k1Identity {
21    private_key: SigningKey,
22    _public_key: VerifyingKey,
23    der_encoded_public_key: Document,
24}
25
26impl Secp256k1Identity {
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(File::open(file_path)?)
31    }
32
33    /// Creates an identity from a PEM certificate.
34    #[cfg(feature = "pem")]
35    pub fn from_pem<R: io::Read>(pem_reader: R) -> Result<Self, PemError> {
36        use sec1::{pem::PemLabel, EcPrivateKey};
37
38        const EC_PARAMETERS: &str = "EC PARAMETERS";
39        const SECP256K1: &[u8] = b"\x06\x05\x2b\x81\x04\x00\x0a";
40
41        let contents = pem_reader.bytes().collect::<Result<Vec<u8>, io::Error>>()?;
42
43        for pem in pem::parse_many(contents)? {
44            if pem.tag() == EC_PARAMETERS && pem.contents() != SECP256K1 {
45                return Err(PemError::UnsupportedKeyCurve(
46                    "secp256k1".to_string(),
47                    pem.contents().to_vec(),
48                ));
49            }
50
51            if pem.tag() != EcPrivateKey::PEM_LABEL {
52                continue;
53            }
54            let private_key =
55                SecretKey::from_sec1_der(pem.contents()).map_err(|_| pkcs8::Error::KeyMalformed)?;
56            return Ok(Self::from_private_key(private_key));
57        }
58        Err(pem::PemError::MissingData.into())
59    }
60
61    /// Creates an identity from a private key.
62    pub fn from_private_key(private_key: SecretKey) -> Self {
63        let public_key = private_key.public_key();
64        let der_encoded_public_key = public_key
65            .to_public_key_der()
66            .expect("Cannot DER encode secp256k1 public key.");
67        Self {
68            private_key: private_key.into(),
69            _public_key: public_key.into(),
70            der_encoded_public_key,
71        }
72    }
73}
74
75impl Identity for Secp256k1Identity {
76    fn sender(&self) -> Result<Principal, String> {
77        Ok(Principal::self_authenticating(
78            self.der_encoded_public_key.as_ref(),
79        ))
80    }
81
82    fn public_key(&self) -> Option<Vec<u8>> {
83        Some(self.der_encoded_public_key.as_ref().to_vec())
84    }
85
86    fn sign(&self, content: &EnvelopeContent) -> Result<Signature, String> {
87        self.sign_arbitrary(&content.to_request_id().signable())
88    }
89
90    fn sign_delegation(&self, content: &Delegation) -> Result<Signature, String> {
91        self.sign_arbitrary(&content.signable())
92    }
93
94    fn sign_arbitrary(&self, content: &[u8]) -> Result<Signature, String> {
95        let ecdsa_sig: ecdsa::Signature = self
96            .private_key
97            .try_sign(content)
98            .map_err(|err| format!("Cannot create secp256k1 signature: {err}"))?;
99        let r = ecdsa_sig.r().as_ref().to_bytes();
100        let s = ecdsa_sig.s().as_ref().to_bytes();
101        let mut bytes = [0u8; 64];
102        if r.len() > 32 || s.len() > 32 {
103            return Err("Cannot create secp256k1 signature: malformed signature.".to_string());
104        }
105        bytes[(32 - r.len())..32].clone_from_slice(&r);
106        bytes[32 + (32 - s.len())..].clone_from_slice(&s);
107        let signature = Some(bytes.to_vec());
108        let public_key = self.public_key();
109        Ok(Signature {
110            public_key,
111            signature,
112            delegations: None,
113        })
114    }
115}
116
117#[cfg(feature = "pem")]
118#[cfg(test)]
119mod test {
120    use super::*;
121    use candid::Encode;
122    use k256::{
123        ecdsa::{signature::Verifier, Signature},
124        elliptic_curve::PrimeField,
125        FieldBytes, Scalar,
126    };
127
128    // WRONG_CURVE_IDENTITY_FILE is generated from the following command:
129    // > openssl ecparam -name secp160r2 -genkey
130    // it uses hte secp160r2 curve instead of secp256k1 and should
131    // therefore be rejected by Secp256k1Identity when loading an identity
132    const WRONG_CURVE_IDENTITY_FILE: &str = "-----BEGIN EC PARAMETERS-----
133BgUrgQQAHg==
134-----END EC PARAMETERS-----
135-----BEGIN EC PRIVATE KEY-----
136MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR
137oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ==
138-----END EC PRIVATE KEY-----
139";
140
141    // WRONG_CURVE_IDENTITY_FILE_NO_PARAMS is generated from the following command:
142    // > openssl ecparam -name secp160r2 -genkey -noout
143    // it uses hte secp160r2 curve instead of secp256k1 and should
144    // therefore be rejected by Secp256k1Identity when loading an identity
145    const WRONG_CURVE_IDENTITY_FILE_NO_PARAMS: &str = "-----BEGIN EC PRIVATE KEY-----
146MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR
147oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ==
148-----END EC PRIVATE KEY-----
149";
150
151    // IDENTITY_FILE was generated from the the following commands:
152    // > openssl ecparam -name secp256k1 -genkey -noout -out identity.pem
153    // > cat identity.pem
154    const IDENTITY_FILE: &str = "-----BEGIN EC PARAMETERS-----
155BgUrgQQACg==
156-----END EC PARAMETERS-----
157-----BEGIN EC PRIVATE KEY-----
158MHQCAQEEIAgy7nZEcVHkQ4Z1Kdqby8SwyAiyKDQmtbEHTIM+WNeBoAcGBSuBBAAK
159oUQDQgAEgO87rJ1ozzdMvJyZQ+GABDqUxGLvgnAnTlcInV3NuhuPv4O3VGzMGzeB
160N3d26cRxD99TPtm8uo2OuzKhSiq6EQ==
161-----END EC PRIVATE KEY-----
162";
163
164    // DER_ENCODED_PUBLIC_KEY was generated from the the following commands:
165    // > openssl ec -in identity.pem -pubout -outform DER -out public.der
166    // > hexdump -ve '1/1 "%.2x"' public.der
167    const DER_ENCODED_PUBLIC_KEY: &str = "3056301006072a8648ce3d020106052b8104000a0342000480ef3bac9d68cf374cbc9c9943e180043a94c462ef8270274e57089d5dcdba1b8fbf83b7546ccc1b3781377776e9c4710fdf533ed9bcba8d8ebb32a14a2aba11";
168
169    #[test]
170    #[should_panic(expected = "UnsupportedKeyCurve")]
171    fn test_secp256k1_reject_wrong_curve() {
172        Secp256k1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE.as_bytes()).unwrap();
173    }
174
175    #[test]
176    #[should_panic(expected = "KeyMalformed")]
177    fn test_secp256k1_reject_wrong_curve_no_id() {
178        Secp256k1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE_NO_PARAMS.as_bytes()).unwrap();
179    }
180
181    #[test]
182    fn test_secp256k1_public_key() {
183        // Create a secp256k1 identity from a PEM file.
184        let identity = Secp256k1Identity::from_pem(IDENTITY_FILE.as_bytes())
185            .expect("Cannot create secp256k1 identity from PEM file.");
186
187        // Assert the DER-encoded secp256k1 public key matches what we would expect.
188        assert!(DER_ENCODED_PUBLIC_KEY == hex::encode(identity.der_encoded_public_key));
189    }
190
191    #[test]
192    fn test_secp256k1_signature() {
193        // Create a secp256k1 identity from a PEM file.
194        let identity = Secp256k1Identity::from_pem(IDENTITY_FILE.as_bytes())
195            .expect("Cannot create secp256k1 identity from PEM file.");
196
197        // Create a secp256k1 signature for a hello-world canister.
198        let message = EnvelopeContent::Call {
199            nonce: None,
200            ingress_expiry: 0,
201            sender: identity.sender().unwrap(),
202            canister_id: "bkyz2-fmaaa-aaaaa-qaaaq-cai".parse().unwrap(),
203            method_name: "greet".to_string(),
204            arg: Encode!(&"world").unwrap(),
205        };
206        let signature = identity
207            .sign(&message)
208            .expect("Cannot create secp256k1 signature.")
209            .signature
210            .expect("Cannot find secp256k1 signature bytes.");
211
212        // Import the secp256k1 signature into OpenSSL.
213        let r: Scalar = Option::from(Scalar::from_repr(*FieldBytes::from_slice(
214            &signature[0..32],
215        )))
216        .expect("Cannot extract r component from secp256k1 signature bytes.");
217        let s: Scalar = Option::from(Scalar::from_repr(*FieldBytes::from_slice(&signature[32..])))
218            .expect("Cannot extract s component from secp256k1 signature bytes.");
219        let ecdsa_sig = Signature::from_scalars(r, s)
220            .expect("Cannot create secp256k1 signature from r and s components.");
221
222        // Assert the secp256k1 signature is valid.
223        identity
224            ._public_key
225            .verify(&message.to_request_id().signable(), &ecdsa_sig)
226            .expect("Cannot verify secp256k1 signature.");
227    }
228}