cloudfront_policy_signer/
lib.rs

1/*
2MIT License
3
4Copyright (c) 2020 Martin Karlsen Jensen
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
24
25use log::error;
26use openssl::base64::encode_block;
27use openssl::hash::MessageDigest;
28use openssl::pkey::{PKey, Private};
29use openssl::rsa;
30use openssl::sign::Signer;
31use std::io::Error as SysIOError;
32use std::{fmt, fs};
33
34/// Enumeration of all possible errors returned by the crate
35#[derive(Debug)]
36pub enum Error {
37    /// We received an IO error from the operating system. Refer to std::io::Error for more information
38    IOError(SysIOError),
39    /// The private key was in an unsupported format or somehow malformed. It only accepts keys in PEM-encoded PKCS#1
40    PrivateKeyParseError,
41    /// The key could not be converted from a `openssl::rsa::Rsa<openssl::pkey::Private>` to a `PKey<Private>`
42    PrivateKeyConvertError,
43    /// The policy could not be signed. Refer to the error printed out in the logs
44    CouldNotSign,
45    /// Blanket error for all errors from OpenSSL that should not occur, but can due to it being written in unsafe C.
46    Unknown,
47}
48
49impl fmt::Display for Error {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            Error::IOError(e) => {
53                write!(f, "System IO error: {}", e)
54            }
55            Error::PrivateKeyParseError => {
56                write!(f, "The private key was in an unsupported format or somehow malformed. It only accepts keys in PEM-encoded PKCS#1")
57            }
58            Error::PrivateKeyConvertError => {
59                write!(f, "The key could not be converted from a openssl::rsa::Rsa<openssl::pkey::Private> to a PKey<Private>. Refer to log output")
60            }
61            Error::CouldNotSign => {
62                write!(f, "The policy could not be signed. Refer log output")
63            }
64            Error::Unknown => {
65                write!(f, "Unkown error occurred")
66            }
67        }
68    }
69}
70
71/// Returns a canned policy with the specified constraints as an vector of bytes
72///
73/// # Arguments
74/// * `resource` - The protected resource eg. https://example.cloudfront.net/flowerpot.png
75/// * `expiry` - The time the resource link should expire at
76///
77///
78fn generate_canned_policy(resource: &str, expiry: u64) -> Vec<u8> {
79    format!("{{\"Statement\":[{{\"Resource\":\"{}\",\"Condition\":{{\"DateLessThan\":{{\"AWS:EpochTime\":{}}}}}}}]}}", resource, expiry).into_bytes()
80}
81
82/// Reads the contents of a file into memory and returns it as a vector of bytes
83///
84/// # Arguments
85/// * `file` - A file containing an RSA private key usually either retrieved in the AWS interface or generated by OpenSSL. The file must be in PEM-encoded PKCS#1
86///
87/// # Note
88///
89/// See the [CloudFront Documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html#private-content-creating-cloudfront-key-pairs) about creating these keypairs
90///
91///
92fn read_rsa_private_key(file: &str) -> Result<Vec<u8>, Error> {
93    fs::read(&file).map_err(|e| {
94        error!("Could not read private key from file due to {}", e);
95        Error::IOError(e)
96    })
97}
98
99/// Parses the read bytes into an representation of a RSA private key appropriate for OpenSSL
100///
101/// # Arguments
102/// * `key` - An array of bytes containing a RSA private key part
103///
104fn parse_rsa_private_key(key: &[u8]) -> Result<PKey<Private>, Error> {
105    rsa::Rsa::private_key_from_pem(&key)
106        .map_err(|e| {
107            error!("Could not parse RSA private key due to {}", e);
108            Error::PrivateKeyParseError
109        })
110        .and_then(|private_key| {
111            PKey::from_rsa(private_key).map_err(|e| {
112                error!("Could not convert RSA private key due to {}", e);
113                Error::PrivateKeyConvertError
114            })
115        })
116}
117
118/// Signs the canned policy and returns it as a vector of bytes
119///
120/// # Arguments
121/// * `policy` - An array of bytes containing the properly formatted policy
122/// * `private_key` - The representation of the RSA private key part
123///
124///
125fn sign_canned_policy(policy: &[u8], private_key: &PKey<Private>) -> Result<Vec<u8>, Error> {
126    Signer::new(MessageDigest::sha1(), &private_key)
127        .map_err(|e| {
128            error!("Could not create signer due to {}", e);
129            Error::Unknown
130        })
131        .and_then(|mut signer| {
132            signer
133                .update(&policy)
134                .map_err(|e| {
135                    error!("Could not update signer due to {}", e);
136                    Error::Unknown
137                })
138                .and_then(|_| {
139                    signer.sign_to_vec().map_err(|e| {
140                        error!("Could not sign due to {}", e);
141                        Error::CouldNotSign
142                    })
143                })
144        })
145}
146
147/// Base64 encode an array of data and use that to create an URL safe string
148///
149/// # Arguments
150/// * `bytes` - An array of bytes to be encoded
151///
152///
153fn encode_signature_url_safe(bytes: &[u8]) -> String {
154    encode_block(&bytes)
155        .replace("+", "-")
156        .replace("=", "_")
157        .replace("/", "~")
158}
159
160/// Signs a canned policy with the specified path and expiration date and returns it in an URL safe format appropriate for AWS.
161///
162///
163/// See [CloudFront Documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-canned-policy.html) for more details
164///
165/// # Arguments
166/// * `resource` - The protected resource eg. https://example.cloudfront.net/flowerpot.png
167/// * `expiry` - Absolute time that the link expires, given in the form of a unix timestamp in UTC
168/// * `private_key_location` - Path where the private key file can be found
169///
170///
171/// # Example
172/// ```
173/// use cloudfront_url_signer;
174///
175///let resource = "https://example.cloudfront.net/flowerpot.png";
176///let expiry = 1579532331;
177///let certificate_location = "examples/key.pem";
178///let key_pair_id = "APKAIEXAMPLE";
179///let signature = cloudfront_url_signer::create_canned_policy_signature(resource, expiry, certificate_location).unwrap();
180///
181///println!("Signed URL is {}", format!("{}?Expires={}&Signature={}&Key-Pair-Id={}", resource, expiry, signature, key_pair_id));
182/// ```
183///
184pub fn create_canned_policy_signature(
185    resource: &str,
186    expiry: u64,
187    private_key_location: &str,
188) -> Result<String, Error> {
189    let key = read_file_to_private_key(private_key_location)?;
190    let signed_policy = sign_canned_policy(&generate_canned_policy(resource, expiry), &key)?;
191
192    Ok(encode_signature_url_safe(&signed_policy))
193}
194
195/// Reads a .pem file and tries to transform it into a PKey<Private>
196///
197/// # Arguments
198/// * `private_key_location` - Path where the private key file can be found
199fn read_file_to_private_key(private_key_location: &str) -> Result<PKey<Private>, Error> {
200    let key = read_rsa_private_key(private_key_location)?;
201
202    parse_rsa_private_key(&key)
203}
204
205/// Struct to create a URL from CloudFront with a cached private key
206/// Unliked method `create_canned_policy_signature`, calling `create_canned_policy_signature_url`
207/// in an instance of this struct will not read the private key file every time it is invoked.
208pub struct CloudFrontCannedPolicySigner {
209    private_key: PKey<Private>,
210    key_pair_id: String
211}
212
213impl CloudFrontCannedPolicySigner {
214    /// Constructs a new instance of `CloudFrontCannedPolicySigner`
215    /// # Arguments
216    /// * `private_key_location` - Path where the private key file can be found
217    /// * `key_pair_id` - The key pair ID from AWS CloudFront
218    pub fn new<T: ToString>(private_key_location: &str, key_pair_id: T) -> Result<CloudFrontCannedPolicySigner, Error> {
219        Ok(Self {
220            private_key: read_file_to_private_key(private_key_location)?,
221            key_pair_id: key_pair_id.to_string()
222        })
223    }
224
225    /// Creates a URL to CloudFront which can be used to download the object
226    pub fn create_canned_policy_signature_url(
227        &self,
228        resource: &str,
229        expiry: u64,
230    ) -> Result<String, Error> {
231        let signed_policy =
232            sign_canned_policy(&generate_canned_policy(resource, expiry), &self.private_key)?;
233        let signature = encode_signature_url_safe(&signed_policy);
234        let url = format!(
235            "{}?Expires={}&Signature={}&Key-Pair-Id={}",
236            resource, expiry, signature, self.key_pair_id
237        );
238
239        Ok(url)
240    }
241}