aws_sdk_cloudfront_url_signer/
key.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::error::SigningError;
7use rsa::pkcs1::DecodeRsaPrivateKey;
8use rsa::RsaPrivateKey;
9use sha1::{Digest, Sha1};
10
11use p256::ecdsa::signature::SignatureEncoding;
12#[cfg(feature = "rt-tokio")]
13use std::path::Path;
14
15/// Private key for signing CloudFront URLs and cookies.
16#[derive(Debug, Clone)]
17pub enum PrivateKey {
18    /// RSA private key.
19    Rsa(Box<RsaPrivateKey>),
20    /// ECDSA P-256 private key.
21    Ecdsa(p256::ecdsa::SigningKey),
22}
23
24impl PrivateKey {
25    /// Loads a private key from PEM-encoded bytes.
26    ///
27    /// Supports RSA keys in PKCS#1 or PKCS#8 format, and ECDSA P-256 keys in PKCS#8 format.
28    pub fn from_pem(bytes: &[u8]) -> Result<Self, SigningError> {
29        let pem_str = std::str::from_utf8(bytes).map_err(SigningError::invalid_key)?;
30
31        // Detect key type from PEM header
32        if pem_str.contains("BEGIN RSA PRIVATE KEY") {
33            // PKCS#1 RSA format
34            let key = RsaPrivateKey::from_pkcs1_pem(pem_str).map_err(SigningError::invalid_key)?;
35            return Ok(PrivateKey::Rsa(Box::new(key)));
36        }
37
38        if pem_str.contains("BEGIN PRIVATE KEY") {
39            // PKCS#8 format - could be RSA or ECDSA
40            // Try ECDSA first (P-256)
41            if let Ok(key) = p256::ecdsa::SigningKey::from_pkcs8_pem(pem_str) {
42                return Ok(PrivateKey::Ecdsa(key));
43            }
44
45            // Try RSA
46            use p256::pkcs8::DecodePrivateKey;
47            let key = RsaPrivateKey::from_pkcs8_pem(pem_str).map_err(SigningError::invalid_key)?;
48            return Ok(PrivateKey::Rsa(Box::new(key)));
49        }
50
51        Err(SigningError::invalid_key(
52            "Unsupported key format. Expected RSA (PKCS#1 or PKCS#8) or ECDSA P-256 (PKCS#8)",
53        ))
54    }
55
56    /// Loads a private key from a PEM file asynchronously.
57    ///
58    /// Requires the `rt-tokio` feature.
59    #[cfg(feature = "rt-tokio")]
60    #[cfg_attr(docsrs, doc(cfg(feature = "rt-tokio")))]
61    pub async fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, SigningError> {
62        let bytes = tokio::fs::read(path.as_ref())
63            .await
64            .map_err(SigningError::invalid_key)?;
65
66        Self::from_pem(&bytes)
67    }
68
69    pub(crate) fn sign(&self, message: &[u8]) -> Result<Vec<u8>, SigningError> {
70        match self {
71            PrivateKey::Rsa(key) => {
72                let mut hasher = Sha1::new();
73                hasher.update(message);
74                let digest = hasher.finalize();
75
76                let signature = key
77                    .sign(rsa::Pkcs1v15Sign::new::<Sha1>(), &digest)
78                    .map_err(SigningError::signing_failure)?;
79
80                Ok(signature)
81            }
82            PrivateKey::Ecdsa(key) => {
83                use p256::ecdsa::signature::DigestSigner;
84
85                // CloudFront uses SHA1 for all signature types
86                let mut hasher = Sha1::new();
87                hasher.update(message);
88
89                let (signature, _recovery_id): (p256::ecdsa::Signature, _) = key
90                    .try_sign_digest(hasher)
91                    .map_err(SigningError::signing_failure)?;
92
93                // CloudFront expects DER-encoded signatures
94                Ok(signature.to_der().to_vec())
95            }
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    const TEST_RSA_KEY_PEM: &[u8] = b"-----BEGIN RSA PRIVATE KEY-----
105MIIBPAIBAAJBANW8WjQksUoX/7nwOfRDNt1XQpLCueHoXSt91MASMOSAqpbzZvXO
106g2hW2gCFUIFUPCByMXPoeRe6iUZ5JtjepssCAwEAAQJBALR7ybwQY/lKTLKJrZab
107D4BXCCt/7ZFbMxnftsC+W7UHef4S4qFW8oOOLeYfmyGZK1h44rXf2AIp4PndKUID
1081zECIQD1suunYw5U22Pa0+2dFThp1VMXdVbPuf/5k3HT2/hSeQIhAN6yX0aT/N6G
109gb1XlBKw6GQvhcM0fXmP+bVXV+RtzFJjAiAP+2Z2yeu5u1egeV6gdCvqPnUcNobC
110FmA/NMcXt9xMSQIhALEMMJEFAInNeAIXSYKeoPNdkMPDzGnD3CueuCLEZCevAiEA
111j+KnJ7pJkTvOzFwE8RfNLli9jf6/OhyYaLL4et7Ng5k=
112-----END RSA PRIVATE KEY-----";
113
114    const TEST_ECDSA_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
115MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg4//aTM1/HqiVWagy
11601cAx3EaegJ0Y5KLRoTtub8T8EWhRANCAARV/wa477wYpyWB5LCrCdS5M9bEAvD+
117VORtjoydSpheKlsa+gE4PcFG88G2gE1Lilb8f6wEq/Lz+5kFa2S8gZmb
118-----END PRIVATE KEY-----";
119
120    #[test]
121    fn test_from_pem_invalid() {
122        let result = PrivateKey::from_pem(b"invalid pem data");
123        assert!(result.is_err());
124    }
125
126    #[test]
127    fn test_rsa_key_parsing() {
128        let key = PrivateKey::from_pem(TEST_RSA_KEY_PEM).expect("valid RSA key");
129        assert!(matches!(key, PrivateKey::Rsa(_)));
130    }
131
132    #[test]
133    fn test_ecdsa_key_parsing() {
134        let key = PrivateKey::from_pem(TEST_ECDSA_KEY_PEM).expect("valid ECDSA key");
135        assert!(matches!(key, PrivateKey::Ecdsa(_)));
136    }
137
138    #[test]
139    fn test_rsa_sign() {
140        let key = PrivateKey::from_pem(TEST_RSA_KEY_PEM).expect("valid test key");
141        let message = b"test message";
142        let signature = key.sign(message).expect("signing should succeed");
143        assert!(!signature.is_empty());
144    }
145
146    #[test]
147    fn test_ecdsa_sign() {
148        let key = PrivateKey::from_pem(TEST_ECDSA_KEY_PEM).expect("valid test key");
149        let message = b"test message";
150        let signature = key.sign(message).expect("signing should succeed");
151        assert!(!signature.is_empty());
152    }
153}