Skip to main content

warg_crypto/signing/
private_key.rs

1use super::{PublicKey, Signature, SignatureAlgorithm, SignatureAlgorithmParseError};
2use base64::{engine::general_purpose::STANDARD, Engine};
3use p256;
4use secrecy::{zeroize::Zeroizing, ExposeSecret, Secret, SecretString, SecretVec, Zeroize};
5use signature::Signer;
6use thiserror::Error;
7
8pub use signature::Error as SignatureError;
9
10/// Represents a private key
11pub struct PrivateKey(Secret<PrivateKeyInner>);
12
13pub enum PrivateKeyInner {
14    EcdsaP256(p256::ecdsa::SigningKey),
15}
16
17impl PrivateKey {
18    /// Decode a key from the given string in `<algo>:<base64 data>` form.
19    pub fn decode(s: impl Into<SecretString>) -> Result<Self, PrivateKeyParseError> {
20        let s = s.into();
21
22        let Some((algo, b64_data)) = s.expose_secret().split_once(':') else {
23            return Err(PrivateKeyParseError::MissingColon);
24        };
25
26        let algo = algo.parse::<SignatureAlgorithm>()?;
27        let bytes: SecretVec<u8> = STANDARD.decode(b64_data)?.into();
28
29        let key = match algo {
30            SignatureAlgorithm::EcdsaP256 => PrivateKeyInner::EcdsaP256(
31                p256::ecdsa::SigningKey::from_slice(bytes.expose_secret())?,
32            ),
33        };
34
35        Ok(PrivateKey(Secret::from(key)))
36    }
37
38    /// Encode the key as a string in `<algo>:<base64 data>` form.
39    pub fn encode(&self) -> Zeroizing<String> {
40        Zeroizing::new(format!(
41            "{algo}:{b64}",
42            algo = self.signature_algorithm(),
43            b64 = STANDARD.encode(self.bytes())
44        ))
45    }
46
47    /// Get the signature algorithm used for by this key
48    pub fn signature_algorithm(&self) -> SignatureAlgorithm {
49        match self.0.expose_secret() {
50            PrivateKeyInner::EcdsaP256(_) => SignatureAlgorithm::EcdsaP256,
51        }
52    }
53
54    /// Get the keys representation as bytes (not including an algorithm specifier)
55    pub fn bytes(&self) -> Vec<u8> {
56        match self.0.expose_secret() {
57            PrivateKeyInner::EcdsaP256(key) => key.to_bytes().to_vec(),
58        }
59    }
60
61    /// Sign a given message with this key
62    pub fn sign(&self, msg: &[u8]) -> Result<Signature, SignatureError> {
63        match self.0.expose_secret() {
64            PrivateKeyInner::EcdsaP256(key) => Ok(Signature::P256(key.try_sign(msg)?)),
65        }
66    }
67
68    pub fn public_key(&self) -> PublicKey {
69        match self.0.expose_secret() {
70            PrivateKeyInner::EcdsaP256(key) => {
71                PublicKey::EcdsaP256(p256::ecdsa::VerifyingKey::from(key))
72            }
73        }
74    }
75}
76
77// Note: FromStr isn't used because it makes it too easy to leave behind an
78// unzeroized copy of the sensitive encoded key.
79impl TryFrom<String> for PrivateKey {
80    type Error = PrivateKeyParseError;
81
82    fn try_from(key: String) -> Result<Self, PrivateKeyParseError> {
83        let key = Zeroizing::new(key);
84
85        let Some((algo, b64_data)) = key.split_once(':') else {
86            return Err(PrivateKeyParseError::MissingColon);
87        };
88
89        let algo = algo.parse::<SignatureAlgorithm>()?;
90        let bytes = STANDARD.decode(b64_data)?;
91
92        let key = match algo {
93            SignatureAlgorithm::EcdsaP256 => PrivateKeyInner::EcdsaP256(
94                p256::ecdsa::SigningKey::from_bytes(bytes.as_slice().into())?,
95            ),
96        };
97
98        Ok(PrivateKey(Secret::from(key)))
99    }
100}
101
102#[derive(Error, Debug)]
103pub enum PrivateKeyParseError {
104    #[error("expected algorithm followed by colon")]
105    MissingColon,
106
107    #[error("unable to parse signature algorithm")]
108    SignatureAlgorithmParseError(#[from] SignatureAlgorithmParseError),
109
110    #[error("base64 decode failed")]
111    Base64DecodeError(#[from] base64::DecodeError),
112
113    #[error("private key could not be constructed from bytes")]
114    SignatureError(#[from] SignatureError),
115}
116
117impl Zeroize for PrivateKeyInner {
118    fn zeroize(&mut self) {
119        match self {
120            PrivateKeyInner::EcdsaP256(sk) => {
121                // SigningKey zeroizes on Drop:
122                // https://github.com/RustCrypto/signatures/blob/a97a358f9e00773c4a04ca54816fb539506f89e6/ecdsa/src/sign.rs#L118
123                let mostly_zero = p256::ecdsa::SigningKey::from(
124                    p256::NonZeroScalar::new(p256::Scalar::ONE).unwrap(),
125                );
126                drop(std::mem::replace(sk, mostly_zero));
127            }
128        }
129    }
130}
131
132impl From<p256::ecdsa::SigningKey> for PrivateKey {
133    fn from(key: p256::ecdsa::SigningKey) -> Self {
134        PrivateKey(Secret::from(PrivateKeyInner::EcdsaP256(key)))
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use pretty_assertions::assert_eq;
142
143    #[test]
144    fn test_roundtrip() {
145        let key_str = "ecdsa-p256:I+UlDo0HxyBBFeelhPPWmD+LnklOpqZDkrFP5VduASk=";
146        let key = PrivateKey::decode(key_str.to_string()).unwrap();
147        assert_eq!(key_str, &*key.encode());
148    }
149}