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}