AWS SDK

AWS SDK

rev. 0f697fe024b95fa2edbb5bf2fc415edbd1a0c921 (ignoring whitespace)

Files changed:

tmp-codegen-diff/aws-sdk/sdk/aws-sdk-cloudfront-url-signer/src/sign.rs

@@ -0,1 +0,593 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
use crate::error::{ErrorKind, SigningError};
           7  +
use crate::key::PrivateKey;
           8  +
use crate::policy::Policy;
           9  +
use aws_smithy_types::DateTime;
          10  +
use std::borrow::Cow;
          11  +
use std::fmt;
          12  +
use std::time::Duration;
          13  +
          14  +
/// CloudFront-specific base64 encoding.
          15  +
/// Standard base64 with: `+` → `-`, `=` → `_`, `/` → `~`
          16  +
fn cloudfront_base64(data: &[u8]) -> String {
          17  +
    base64_simd::STANDARD
          18  +
        .encode_to_string(data)
          19  +
        .replace('+', "-")
          20  +
        .replace('=', "_")
          21  +
        .replace('/', "~")
          22  +
}
          23  +
          24  +
const COOKIE_POLICY: &str = "CloudFront-Policy";
          25  +
const COOKIE_SIGNATURE: &str = "CloudFront-Signature";
          26  +
const COOKIE_KEY_PAIR_ID: &str = "CloudFront-Key-Pair-Id";
          27  +
const COOKIE_EXPIRES: &str = "CloudFront-Expires";
          28  +
          29  +
#[derive(Debug, Clone)]
          30  +
enum Expiration {
          31  +
    DateTime(DateTime),
          32  +
    Duration(Duration),
          33  +
}
          34  +
          35  +
/// Request to sign a CloudFront URL or generate signed cookies.
          36  +
#[derive(Debug, Clone)]
          37  +
pub struct SigningRequest {
          38  +
    pub(crate) resource_url: String,
          39  +
    pub(crate) resource_pattern: Option<String>,
          40  +
    pub(crate) key_pair_id: String,
          41  +
    pub(crate) private_key: PrivateKey,
          42  +
    pub(crate) expiration: DateTime,
          43  +
    pub(crate) active_date: Option<DateTime>,
          44  +
    pub(crate) ip_range: Option<String>,
          45  +
}
          46  +
          47  +
impl SigningRequest {
          48  +
    /// Creates a new builder for constructing a signing request.
          49  +
    pub fn builder() -> SigningRequestBuilder {
          50  +
        SigningRequestBuilder::default()
          51  +
    }
          52  +
}
          53  +
          54  +
/// Builder for [`SigningRequest`].
          55  +
#[derive(Default, Debug)]
          56  +
pub struct SigningRequestBuilder {
          57  +
    resource_url: Option<String>,
          58  +
    resource_pattern: Option<String>,
          59  +
    key_pair_id: Option<String>,
          60  +
    private_key: Option<PrivateKey>,
          61  +
    expiration: Option<Expiration>,
          62  +
    active_date: Option<DateTime>,
          63  +
    ip_range: Option<String>,
          64  +
    time_source: Option<aws_smithy_async::time::SharedTimeSource>,
          65  +
}
          66  +
          67  +
impl SigningRequestBuilder {
          68  +
    /// Sets the CloudFront resource URL to sign.
          69  +
    pub fn resource_url(mut self, url: impl Into<String>) -> Self {
          70  +
        self.resource_url = Some(url.into());
          71  +
        self
          72  +
    }
          73  +
          74  +
    /// Sets a wildcard pattern for the policy's `Resource` field.
          75  +
    ///
          76  +
    /// Use this when you want the signed URL to grant access to multiple resources
          77  +
    /// matching a pattern. If not set, `resource_url` is used in the policy.
          78  +
    ///
          79  +
    /// Wildcards:
          80  +
    /// - `*` matches zero or more characters
          81  +
    /// - `?` matches exactly one character
          82  +
    ///
          83  +
    /// # Example
          84  +
    /// ```ignore
          85  +
    /// // Sign a specific URL but grant access to all files under /videos/
          86  +
    /// SigningRequest::builder()
          87  +
    ///     .resource_url("https://d111111abcdef8.cloudfront.net/videos/intro.mp4")
          88  +
    ///     .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*")
          89  +
    ///     // ...
          90  +
    /// ```
          91  +
    pub fn resource_pattern(mut self, pattern: impl Into<String>) -> Self {
          92  +
        self.resource_pattern = Some(pattern.into());
          93  +
        self
          94  +
    }
          95  +
          96  +
    /// Sets the CloudFront key pair ID.
          97  +
    pub fn key_pair_id(mut self, id: impl Into<String>) -> Self {
          98  +
        self.key_pair_id = Some(id.into());
          99  +
        self
         100  +
    }
         101  +
         102  +
    /// Sets the private key for signing.
         103  +
    pub fn private_key(mut self, key: PrivateKey) -> Self {
         104  +
        self.private_key = Some(key);
         105  +
        self
         106  +
    }
         107  +
         108  +
    /// Sets an absolute expiration time.
         109  +
    pub fn expires_at(mut self, time: DateTime) -> Self {
         110  +
        self.expiration = Some(Expiration::DateTime(time));
         111  +
        self
         112  +
    }
         113  +
         114  +
    /// Sets a relative expiration time from now.
         115  +
    pub fn expires_in(mut self, duration: Duration) -> Self {
         116  +
        self.expiration = Some(Expiration::Duration(duration));
         117  +
        self
         118  +
    }
         119  +
         120  +
    /// Sets an activation time (not-before date) for custom policy.
         121  +
    pub fn active_at(mut self, time: DateTime) -> Self {
         122  +
        self.active_date = Some(time);
         123  +
        self
         124  +
    }
         125  +
         126  +
    /// Sets an IP range restriction (CIDR notation) for custom policy.
         127  +
    pub fn ip_range(mut self, cidr: impl Into<String>) -> Self {
         128  +
        self.ip_range = Some(cidr.into());
         129  +
        self
         130  +
    }
         131  +
         132  +
    /// Builds the signing request.
         133  +
    pub fn build(self) -> Result<SigningRequest, SigningError> {
         134  +
        let resource_url = self
         135  +
            .resource_url
         136  +
            .ok_or_else(|| SigningError::invalid_input("resource_url is required"))?;
         137  +
         138  +
        let key_pair_id = self
         139  +
            .key_pair_id
         140  +
            .ok_or_else(|| SigningError::invalid_input("key_pair_id is required"))?;
         141  +
         142  +
        let private_key = self
         143  +
            .private_key
         144  +
            .ok_or_else(|| SigningError::invalid_input("private_key is required"))?;
         145  +
         146  +
        let expiration = self.expiration.ok_or_else(|| {
         147  +
            SigningError::invalid_input("expiration is required (use expires_at or expires_in)")
         148  +
        })?;
         149  +
         150  +
        let expiration = match expiration {
         151  +
            Expiration::DateTime(dt) => dt,
         152  +
            Expiration::Duration(dur) => {
         153  +
                let time_source = self.time_source.unwrap_or_default();
         154  +
                let now = DateTime::from(time_source.now());
         155  +
                DateTime::from_secs(now.secs() + dur.as_secs() as i64)
         156  +
            }
         157  +
        };
         158  +
         159  +
        // Validate that activeDate is before expirationDate
         160  +
        if let Some(active) = self.active_date {
         161  +
            if active.secs() >= expiration.secs() {
         162  +
                return Err(SigningError::invalid_input(
         163  +
                    "active_at must be before expiration",
         164  +
                ));
         165  +
            }
         166  +
        }
         167  +
         168  +
        Ok(SigningRequest {
         169  +
            resource_url,
         170  +
            resource_pattern: self.resource_pattern,
         171  +
            key_pair_id,
         172  +
            private_key,
         173  +
            expiration,
         174  +
            active_date: self.active_date,
         175  +
            ip_range: self.ip_range,
         176  +
        })
         177  +
    }
         178  +
}
         179  +
         180  +
/// A signed CloudFront URL.
         181  +
#[derive(Debug, Clone)]
         182  +
pub struct SignedUrl {
         183  +
    url: url::Url,
         184  +
}
         185  +
         186  +
impl SignedUrl {
         187  +
    pub(crate) fn new(url: String) -> Result<Self, SigningError> {
         188  +
        let url = url::Url::parse(&url).map_err(|e| {
         189  +
            SigningError::new(
         190  +
                ErrorKind::InvalidInput,
         191  +
                Some(Box::new(e)),
         192  +
                Some("failed to parse URL".into()),
         193  +
            )
         194  +
        })?;
         195  +
        Ok(Self { url })
         196  +
    }
         197  +
         198  +
    /// Returns the complete signed URL as a string.
         199  +
    pub fn as_str(&self) -> &str {
         200  +
        self.url.as_str()
         201  +
    }
         202  +
         203  +
    /// Returns a reference to the parsed URL.
         204  +
    pub fn as_url(&self) -> &url::Url {
         205  +
        &self.url
         206  +
    }
         207  +
         208  +
    /// Consumes self and returns the parsed URL.
         209  +
    pub fn into_url(self) -> url::Url {
         210  +
        self.url
         211  +
    }
         212  +
}
         213  +
         214  +
impl fmt::Display for SignedUrl {
         215  +
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         216  +
        write!(f, "{}", self.url)
         217  +
    }
         218  +
}
         219  +
         220  +
impl AsRef<str> for SignedUrl {
         221  +
    fn as_ref(&self) -> &str {
         222  +
        self.url.as_str()
         223  +
    }
         224  +
}
         225  +
         226  +
impl AsRef<url::Url> for SignedUrl {
         227  +
    fn as_ref(&self) -> &url::Url {
         228  +
        &self.url
         229  +
    }
         230  +
}
         231  +
         232  +
#[cfg(feature = "http-1x")]
         233  +
impl TryFrom<SignedUrl> for http::Request<()> {
         234  +
    type Error = http::Error;
         235  +
         236  +
    fn try_from(signed_url: SignedUrl) -> Result<Self, Self::Error> {
         237  +
        http::Request::builder()
         238  +
            .uri(signed_url.url.as_str())
         239  +
            .body(())
         240  +
    }
         241  +
}
         242  +
         243  +
#[cfg(feature = "http-1x")]
         244  +
impl TryFrom<&SignedUrl> for http::Request<()> {
         245  +
    type Error = http::Error;
         246  +
         247  +
    fn try_from(signed_url: &SignedUrl) -> Result<Self, Self::Error> {
         248  +
        http::Request::builder()
         249  +
            .uri(signed_url.url.as_str())
         250  +
            .body(())
         251  +
    }
         252  +
}
         253  +
         254  +
/// Signed cookies for CloudFront.
         255  +
#[derive(Debug, Clone)]
         256  +
pub struct SignedCookies {
         257  +
    cookies: Vec<(Cow<'static, str>, String)>,
         258  +
}
         259  +
         260  +
impl SignedCookies {
         261  +
    pub(crate) fn new(cookies: Vec<(Cow<'static, str>, String)>) -> Self {
         262  +
        Self { cookies }
         263  +
    }
         264  +
         265  +
    /// Returns all cookies as name-value pairs.
         266  +
    pub fn cookies(&self) -> &[(Cow<'static, str>, String)] {
         267  +
        &self.cookies
         268  +
    }
         269  +
         270  +
    /// Gets a specific cookie value by name.
         271  +
    pub fn get(&self, name: &str) -> Option<&str> {
         272  +
        self.cookies
         273  +
            .iter()
         274  +
            .find(|(n, _)| n == name)
         275  +
            .map(|(_, v)| v.as_str())
         276  +
    }
         277  +
         278  +
    /// Returns an iterator over cookies.
         279  +
    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
         280  +
        self.cookies.iter().map(|(n, v)| (n.as_ref(), v.as_str()))
         281  +
    }
         282  +
}
         283  +
         284  +
// Internal signing implementation
         285  +
impl SigningRequest {
         286  +
    /// Returns true if this request should use a canned policy.
         287  +
    /// Canned policy is used only when there's no resource_pattern, active_date, or ip_range.
         288  +
    fn use_canned_policy(&self) -> bool {
         289  +
        self.resource_pattern.is_none() && self.active_date.is_none() && self.ip_range.is_none()
         290  +
    }
         291  +
         292  +
    pub(crate) fn sign_url(&self) -> Result<SignedUrl, SigningError> {
         293  +
        let policy = self.build_policy()?;
         294  +
        let policy_json = policy.to_json();
         295  +
        let signature = self.private_key.sign(policy_json.as_bytes())?;
         296  +
        let signature_b64 = cloudfront_base64(&signature);
         297  +
         298  +
        let separator = if self.resource_url.contains('?') {
         299  +
            "&"
         300  +
        } else {
         301  +
            "?"
         302  +
        };
         303  +
         304  +
        let signed_url = if self.use_canned_policy() {
         305  +
            format!(
         306  +
                "{}{}Expires={}&Signature={}&Key-Pair-Id={}",
         307  +
                self.resource_url,
         308  +
                separator,
         309  +
                self.expiration.secs(),
         310  +
                signature_b64,
         311  +
                self.key_pair_id
         312  +
            )
         313  +
        } else {
         314  +
            let policy_b64 = policy.to_cloudfront_base64();
         315  +
            format!(
         316  +
                "{}{}Policy={}&Signature={}&Key-Pair-Id={}",
         317  +
                self.resource_url, separator, policy_b64, signature_b64, self.key_pair_id
         318  +
            )
         319  +
        };
         320  +
         321  +
        SignedUrl::new(signed_url)
         322  +
    }
         323  +
         324  +
    pub(crate) fn sign_cookies(&self) -> Result<SignedCookies, SigningError> {
         325  +
        let policy = self.build_policy()?;
         326  +
        let policy_json = policy.to_json();
         327  +
        let signature = self.private_key.sign(policy_json.as_bytes())?;
         328  +
        let signature_b64 = cloudfront_base64(&signature);
         329  +
         330  +
        let cookies = if self.use_canned_policy() {
         331  +
            vec![
         332  +
                (
         333  +
                    Cow::Borrowed(COOKIE_EXPIRES),
         334  +
                    self.expiration.secs().to_string(),
         335  +
                ),
         336  +
                (Cow::Borrowed(COOKIE_SIGNATURE), signature_b64),
         337  +
                (Cow::Borrowed(COOKIE_KEY_PAIR_ID), self.key_pair_id.clone()),
         338  +
            ]
         339  +
        } else {
         340  +
            let policy_b64 = policy.to_cloudfront_base64();
         341  +
            vec![
         342  +
                (Cow::Borrowed(COOKIE_POLICY), policy_b64),
         343  +
                (Cow::Borrowed(COOKIE_SIGNATURE), signature_b64),
         344  +
                (Cow::Borrowed(COOKIE_KEY_PAIR_ID), self.key_pair_id.clone()),
         345  +
            ]
         346  +
        };
         347  +
         348  +
        Ok(SignedCookies::new(cookies))
         349  +
    }
         350  +
         351  +
    fn build_policy(&self) -> Result<Policy, SigningError> {
         352  +
        // Use resource_pattern for policy if set, otherwise use resource_url
         353  +
        let policy_resource = self.resource_pattern.as_ref().unwrap_or(&self.resource_url);
         354  +
         355  +
        let mut builder = Policy::builder()
         356  +
            .resource(policy_resource)
         357  +
            .expires_at(self.expiration);
         358  +
         359  +
        if let Some(active) = self.active_date {
         360  +
            builder = builder.starts_at(active);
         361  +
        }
         362  +
        if let Some(ref ip) = self.ip_range {
         363  +
            builder = builder.ip_range(ip);
         364  +
        }
         365  +
         366  +
        builder.build()
         367  +
    }
         368  +
}
         369  +
         370  +
#[cfg(test)]
         371  +
mod tests {
         372  +
    use super::*;
         373  +
         374  +
    const TEST_RSA_KEY: &[u8] = b"-----BEGIN RSA PRIVATE KEY-----
         375  +
MIIBPAIBAAJBANW8WjQksUoX/7nwOfRDNt1XQpLCueHoXSt91MASMOSAqpbzZvXO
         376  +
g2hW2gCFUIFUPCByMXPoeRe6iUZ5JtjepssCAwEAAQJBALR7ybwQY/lKTLKJrZab
         377  +
D4BXCCt/7ZFbMxnftsC+W7UHef4S4qFW8oOOLeYfmyGZK1h44rXf2AIp4PndKUID
         378  +
1zECIQD1suunYw5U22Pa0+2dFThp1VMXdVbPuf/5k3HT2/hSeQIhAN6yX0aT/N6G
         379  +
gb1XlBKw6GQvhcM0fXmP+bVXV+RtzFJjAiAP+2Z2yeu5u1egeV6gdCvqPnUcNobC
         380  +
FmA/NMcXt9xMSQIhALEMMJEFAInNeAIXSYKeoPNdkMPDzGnD3CueuCLEZCevAiEA
         381  +
j+KnJ7pJkTvOzFwE8RfNLli9jf6/OhyYaLL4et7Ng5k=
         382  +
-----END RSA PRIVATE KEY-----";
         383  +
         384  +
    #[test]
         385  +
    fn test_sign_url_canned_policy() {
         386  +
        let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
         387  +
        let request = SigningRequest::builder()
         388  +
            .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg")
         389  +
            .key_pair_id("APKAEXAMPLE")
         390  +
            .private_key(key)
         391  +
            .expires_at(DateTime::from_secs(1767290400))
         392  +
            .build()
         393  +
            .unwrap();
         394  +
         395  +
        let signed_url = request.sign_url().unwrap();
         396  +
        let url = signed_url.as_str();
         397  +
         398  +
        assert!(url.contains("Expires=1767290400"));
         399  +
        assert!(url.contains("Signature="));
         400  +
        assert!(url.contains("Key-Pair-Id=APKAEXAMPLE"));
         401  +
        assert!(!url.contains("Policy="));
         402  +
    }
         403  +
         404  +
    #[test]
         405  +
    fn test_sign_url_custom_policy() {
         406  +
        let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
         407  +
        let request = SigningRequest::builder()
         408  +
            .resource_url("https://d111111abcdef8.cloudfront.net/*")
         409  +
            .key_pair_id("APKAEXAMPLE")
         410  +
            .private_key(key)
         411  +
            .expires_at(DateTime::from_secs(1767290400))
         412  +
            .active_at(DateTime::from_secs(1767200000))
         413  +
            .build()
         414  +
            .unwrap();
         415  +
         416  +
        let signed_url = request.sign_url().unwrap();
         417  +
        let url = signed_url.as_str();
         418  +
         419  +
        assert!(url.contains("Policy="));
         420  +
        assert!(url.contains("Signature="));
         421  +
        assert!(url.contains("Key-Pair-Id=APKAEXAMPLE"));
         422  +
        assert!(!url.contains("Expires="));
         423  +
    }
         424  +
         425  +
    #[test]
         426  +
    fn test_sign_url_with_existing_params() {
         427  +
        let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
         428  +
        let request = SigningRequest::builder()
         429  +
            .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg?size=large")
         430  +
            .key_pair_id("APKAEXAMPLE")
         431  +
            .private_key(key)
         432  +
            .expires_at(DateTime::from_secs(1767290400))
         433  +
            .build()
         434  +
            .unwrap();
         435  +
         436  +
        let signed_url = request.sign_url().unwrap();
         437  +
        let url = signed_url.as_str();
         438  +
         439  +
        assert!(url.contains("size=large"));
         440  +
        assert!(url.contains("&Expires="));
         441  +
    }
         442  +
         443  +
    #[test]
         444  +
    fn test_sign_cookies_canned_policy() {
         445  +
        let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
         446  +
        let request = SigningRequest::builder()
         447  +
            .resource_url("https://d111111abcdef8.cloudfront.net/*")
         448  +
            .key_pair_id("APKAEXAMPLE")
         449  +
            .private_key(key)
         450  +
            .expires_at(DateTime::from_secs(1767290400))
         451  +
            .build()
         452  +
            .unwrap();
         453  +
         454  +
        let cookies = request.sign_cookies().unwrap();
         455  +
         456  +
        assert_eq!(cookies.get("CloudFront-Expires"), Some("1767290400"));
         457  +
        assert!(cookies.get("CloudFront-Signature").is_some());
         458  +
        assert_eq!(cookies.get("CloudFront-Key-Pair-Id"), Some("APKAEXAMPLE"));
         459  +
        assert!(cookies.get("CloudFront-Policy").is_none());
         460  +
    }
         461  +
         462  +
    #[test]
         463  +
    fn test_sign_cookies_custom_policy() {
         464  +
        let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
         465  +
        let request = SigningRequest::builder()
         466  +
            .resource_url("https://d111111abcdef8.cloudfront.net/*")
         467  +
            .key_pair_id("APKAEXAMPLE")
         468  +
            .private_key(key)
         469  +
            .expires_at(DateTime::from_secs(1767290400))
         470  +
            .ip_range("192.0.2.0/24")
         471  +
            .build()
         472  +
            .unwrap();
         473  +
         474  +
        let cookies = request.sign_cookies().unwrap();
         475  +
         476  +
        assert!(cookies.get("CloudFront-Policy").is_some());
         477  +
        assert!(cookies.get("CloudFront-Signature").is_some());
         478  +
        assert_eq!(cookies.get("CloudFront-Key-Pair-Id"), Some("APKAEXAMPLE"));
         479  +
        assert!(cookies.get("CloudFront-Expires").is_none());
         480  +
    }
         481  +
         482  +
    #[test]
         483  +
    fn test_sign_url_with_resource_pattern() {
         484  +
        let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
         485  +
        let request = SigningRequest::builder()
         486  +
            .resource_url("https://d111111abcdef8.cloudfront.net/videos/intro.mp4")
         487  +
            .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*")
         488  +
            .key_pair_id("APKAEXAMPLE")
         489  +
            .private_key(key)
         490  +
            .expires_at(DateTime::from_secs(1767290400))
         491  +
            .build()
         492  +
            .unwrap();
         493  +
         494  +
        let signed_url = request.sign_url().unwrap();
         495  +
        let url = signed_url.as_str();
         496  +
         497  +
        // Should use custom policy format (Policy param) because resource_pattern is set
         498  +
        assert!(url.contains("Policy="));
         499  +
        assert!(url.contains("Signature="));
         500  +
        assert!(url.contains("Key-Pair-Id=APKAEXAMPLE"));
         501  +
        assert!(!url.contains("Expires="));
         502  +
        // The actual URL should be the resource_url, not the pattern
         503  +
        assert!(url.starts_with("https://d111111abcdef8.cloudfront.net/videos/intro.mp4?"));
         504  +
    }
         505  +
         506  +
    #[test]
         507  +
    fn test_sign_cookies_with_resource_pattern() {
         508  +
        let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
         509  +
        let request = SigningRequest::builder()
         510  +
            .resource_url("https://d111111abcdef8.cloudfront.net/videos/*")
         511  +
            .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*")
         512  +
            .key_pair_id("APKAEXAMPLE")
         513  +
            .private_key(key)
         514  +
            .expires_at(DateTime::from_secs(1767290400))
         515  +
            .build()
         516  +
            .unwrap();
         517  +
         518  +
        let cookies = request.sign_cookies().unwrap();
         519  +
         520  +
        // Should use custom policy format because resource_pattern is set
         521  +
        assert!(cookies.get("CloudFront-Policy").is_some());
         522  +
        assert!(cookies.get("CloudFront-Signature").is_some());
         523  +
        assert_eq!(cookies.get("CloudFront-Key-Pair-Id"), Some("APKAEXAMPLE"));
         524  +
        assert!(cookies.get("CloudFront-Expires").is_none());
         525  +
    }
         526  +
         527  +
    #[test]
         528  +
    fn test_signed_url_accessors() {
         529  +
        let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
         530  +
        let request = SigningRequest::builder()
         531  +
            .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg")
         532  +
            .key_pair_id("APKAEXAMPLE")
         533  +
            .private_key(key)
         534  +
            .expires_at(DateTime::from_secs(1767290400))
         535  +
            .build()
         536  +
            .unwrap();
         537  +
         538  +
        let signed_url = request.sign_url().unwrap();
         539  +
         540  +
        // Test as_str()
         541  +
        let url_str = signed_url.as_str();
         542  +
        assert!(url_str.starts_with("https://d111111abcdef8.cloudfront.net/image.jpg"));
         543  +
        assert!(url_str.contains("Expires="));
         544  +
         545  +
        // Test as_url()
         546  +
        let url_ref = signed_url.as_url();
         547  +
        assert_eq!(url_ref.scheme(), "https");
         548  +
        assert_eq!(url_ref.host_str(), Some("d111111abcdef8.cloudfront.net"));
         549  +
        assert_eq!(url_ref.path(), "/image.jpg");
         550  +
         551  +
        // Test Display
         552  +
        let displayed = format!("{}", signed_url);
         553  +
        assert_eq!(displayed, url_str);
         554  +
         555  +
        // Test AsRef<str>
         556  +
        let as_ref_str: &str = signed_url.as_ref();
         557  +
        assert_eq!(as_ref_str, url_str);
         558  +
         559  +
        // Test AsRef<url::Url>
         560  +
        let as_ref_url: &url::Url = signed_url.as_ref();
         561  +
        assert_eq!(as_ref_url, url_ref);
         562  +
         563  +
        // Test into_url()
         564  +
        let url = signed_url.into_url();
         565  +
        assert_eq!(url.scheme(), "https");
         566  +
        assert_eq!(url.host_str(), Some("d111111abcdef8.cloudfront.net"));
         567  +
    }
         568  +
         569  +
    #[cfg(feature = "http-1x")]
         570  +
    #[test]
         571  +
    fn test_signed_url_to_http_request() {
         572  +
        let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
         573  +
        let request = SigningRequest::builder()
         574  +
            .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg")
         575  +
            .key_pair_id("APKAEXAMPLE")
         576  +
            .private_key(key)
         577  +
            .expires_at(DateTime::from_secs(1767290400))
         578  +
            .build()
         579  +
            .unwrap();
         580  +
         581  +
        let signed_url = request.sign_url().unwrap();
         582  +
         583  +
        // Test TryFrom<SignedUrl>
         584  +
        let http_req: http::Request<()> = signed_url.clone().try_into().unwrap();
         585  +
        assert_eq!(http_req.method(), http::Method::GET);
         586  +
        assert!(http_req.uri().to_string().contains("Expires="));
         587  +
         588  +
        // Test TryFrom<&SignedUrl>
         589  +
        let http_req: http::Request<()> = (&signed_url).try_into().unwrap();
         590  +
        assert_eq!(http_req.method(), http::Method::GET);
         591  +
        assert!(http_req.uri().to_string().contains("Expires="));
         592  +
    }
         593  +
}

tmp-codegen-diff/aws-sdk/sdk/aws-sdk-cloudfront-url-signer/tests/integration_test.rs

@@ -0,1 +0,83 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
use aws_sdk_cloudfront_url_signer::{sign_cookies, sign_url, PrivateKey, SigningRequest};
           7  +
use aws_smithy_types::DateTime;
           8  +
           9  +
const TEST_RSA_KEY: &[u8] = b"-----BEGIN RSA PRIVATE KEY-----
          10  +
MIIBPAIBAAJBANW8WjQksUoX/7nwOfRDNt1XQpLCueHoXSt91MASMOSAqpbzZvXO
          11  +
g2hW2gCFUIFUPCByMXPoeRe6iUZ5JtjepssCAwEAAQJBALR7ybwQY/lKTLKJrZab
          12  +
D4BXCCt/7ZFbMxnftsC+W7UHef4S4qFW8oOOLeYfmyGZK1h44rXf2AIp4PndKUID
          13  +
1zECIQD1suunYw5U22Pa0+2dFThp1VMXdVbPuf/5k3HT2/hSeQIhAN6yX0aT/N6G
          14  +
gb1XlBKw6GQvhcM0fXmP+bVXV+RtzFJjAiAP+2Z2yeu5u1egeV6gdCvqPnUcNobC
          15  +
FmA/NMcXt9xMSQIhALEMMJEFAInNeAIXSYKeoPNdkMPDzGnD3CueuCLEZCevAiEA
          16  +
j+KnJ7pJkTvOzFwE8RfNLli9jf6/OhyYaLL4et7Ng5k=
          17  +
-----END RSA PRIVATE KEY-----";
          18  +
          19  +
const TEST_ECDSA_KEY: &[u8] = b"-----BEGIN PRIVATE KEY-----
          20  +
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg4//aTM1/HqiVWagy
          21  +
01cAx3EaegJ0Y5KLRoTtub8T8EWhRANCAARV/wa477wYpyWB5LCrCdS5M9bEAvD+
          22  +
VORtjoydSpheKlsa+gE4PcFG88G2gE1Lilb8f6wEq/Lz+5kFa2S8gZmb
          23  +
-----END PRIVATE KEY-----";
          24  +
          25  +
#[test]
          26  +
fn test_sign_url_with_rsa_key() {
          27  +
    let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
          28  +
    let request = SigningRequest::builder()
          29  +
        .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg")
          30  +
        .key_pair_id("APKAEXAMPLE")
          31  +
        .private_key(key)
          32  +
        .expires_at(DateTime::from_secs(1767290400))
          33  +
        .build()
          34  +
        .unwrap();
          35  +
          36  +
    let signed_url = sign_url(request).unwrap();
          37  +
    assert!(signed_url.as_str().contains("Signature="));
          38  +
}
          39  +
          40  +
#[test]
          41  +
fn test_sign_url_with_ecdsa_key() {
          42  +
    let key = PrivateKey::from_pem(TEST_ECDSA_KEY).unwrap();
          43  +
    let request = SigningRequest::builder()
          44  +
        .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg")
          45  +
        .key_pair_id("APKAEXAMPLE")
          46  +
        .private_key(key)
          47  +
        .expires_at(DateTime::from_secs(1767290400))
          48  +
        .build()
          49  +
        .unwrap();
          50  +
          51  +
    let signed_url = sign_url(request).unwrap();
          52  +
    assert!(signed_url.as_str().contains("Signature="));
          53  +
}
          54  +
          55  +
#[test]
          56  +
fn test_sign_cookies_with_rsa_key() {
          57  +
    let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
          58  +
    let request = SigningRequest::builder()
          59  +
        .resource_url("https://d111111abcdef8.cloudfront.net/*")
          60  +
        .key_pair_id("APKAEXAMPLE")
          61  +
        .private_key(key)
          62  +
        .expires_at(DateTime::from_secs(1767290400))
          63  +
        .build()
          64  +
        .unwrap();
          65  +
          66  +
    let cookies = sign_cookies(request).unwrap();
          67  +
    assert!(cookies.get("CloudFront-Signature").is_some());
          68  +
}
          69  +
          70  +
#[test]
          71  +
fn test_sign_cookies_with_ecdsa_key() {
          72  +
    let key = PrivateKey::from_pem(TEST_ECDSA_KEY).unwrap();
          73  +
    let request = SigningRequest::builder()
          74  +
        .resource_url("https://d111111abcdef8.cloudfront.net/*")
          75  +
        .key_pair_id("APKAEXAMPLE")
          76  +
        .private_key(key)
          77  +
        .expires_at(DateTime::from_secs(1767290400))
          78  +
        .build()
          79  +
        .unwrap();
          80  +
          81  +
    let cookies = sign_cookies(request).unwrap();
          82  +
    assert!(cookies.get("CloudFront-Signature").is_some());
          83  +
}

tmp-codegen-diff/aws-sdk/sdk/aws-sdk-cloudfront-url-signer/tests/sep_test_cases.rs

@@ -0,1 +0,213 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
use aws_sdk_cloudfront_url_signer::{sign_cookies, sign_url, PrivateKey, SigningRequest};
           7  +
use aws_smithy_types::DateTime;
           8  +
use serde::Deserialize;
           9  +
use std::collections::HashMap;
          10  +
          11  +
#[derive(Debug, Deserialize)]
          12  +
struct TestCase {
          13  +
    id: String,
          14  +
    documentation: String,
          15  +
    input: TestInput,
          16  +
    expected: TestExpected,
          17  +
}
          18  +
          19  +
#[derive(Debug, Deserialize)]
          20  +
#[serde(rename_all = "camelCase")]
          21  +
struct TestInput {
          22  +
    resource_url: String,
          23  +
    key_pair_id: String,
          24  +
    private_key_file: String,
          25  +
    expiration_date: Option<i64>,
          26  +
    active_date: Option<i64>,
          27  +
    ip_range: Option<String>,
          28  +
    resource_url_pattern: Option<String>,
          29  +
}
          30  +
          31  +
#[derive(Debug, Deserialize)]
          32  +
#[serde(rename_all = "camelCase")]
          33  +
struct TestExpected {
          34  +
    #[allow(dead_code)]
          35  +
    policy_json: Option<String>,
          36  +
    query_params: Option<HashMap<String, String>>,
          37  +
    cookies: Option<HashMap<String, String>>,
          38  +
    signature: Option<String>,
          39  +
    signature_algorithm: Option<String>,
          40  +
    error: Option<bool>,
          41  +
    error_contains: Option<Vec<String>>,
          42  +
}
          43  +
          44  +
#[derive(Debug, Deserialize)]
          45  +
struct TestCases {
          46  +
    #[serde(flatten)]
          47  +
    #[allow(dead_code)]
          48  +
    cases: Vec<TestCase>,
          49  +
}
          50  +
          51  +
fn load_test_cases() -> Vec<TestCase> {
          52  +
    let json = include_str!("test-cases.json");
          53  +
    serde_json::from_str(json).expect("Failed to parse test cases")
          54  +
}
          55  +
          56  +
fn parse_url_query_params(url: &str) -> HashMap<String, String> {
          57  +
    url.split('?')
          58  +
        .nth(1)
          59  +
        .map(|query| {
          60  +
            query
          61  +
                .split('&')
          62  +
                .filter_map(|pair| {
          63  +
                    let mut parts = pair.split('=');
          64  +
                    Some((parts.next()?.to_string(), parts.next()?.to_string()))
          65  +
                })
          66  +
                .collect()
          67  +
        })
          68  +
        .unwrap_or_default()
          69  +
}
          70  +
          71  +
#[test]
          72  +
fn test_sep_test_cases() {
          73  +
    let test_cases = load_test_cases();
          74  +
          75  +
    for test_case in test_cases {
          76  +
        println!(
          77  +
            "\nRunning test: {} - {}",
          78  +
            test_case.id, test_case.documentation
          79  +
        );
          80  +
          81  +
        // Load private key
          82  +
        let key_path = format!("tests/{}", test_case.input.private_key_file);
          83  +
        let key_bytes = std::fs::read(&key_path)
          84  +
            .unwrap_or_else(|_| panic!("Failed to read key file: {key_path}"));
          85  +
        let private_key = PrivateKey::from_pem(&key_bytes)
          86  +
            .unwrap_or_else(|_| panic!("Failed to parse private key for test {}", test_case.id));
          87  +
          88  +
        // Build signing request
          89  +
        let mut builder = SigningRequest::builder()
          90  +
            .resource_url(&test_case.input.resource_url)
          91  +
            .key_pair_id(&test_case.input.key_pair_id)
          92  +
            .private_key(private_key);
          93  +
          94  +
        if let Some(exp) = test_case.input.expiration_date {
          95  +
            builder = builder.expires_at(DateTime::from_secs(exp));
          96  +
        }
          97  +
          98  +
        if let Some(active) = test_case.input.active_date {
          99  +
            builder = builder.active_at(DateTime::from_secs(active));
         100  +
        }
         101  +
         102  +
        if let Some(ip) = &test_case.input.ip_range {
         103  +
            builder = builder.ip_range(ip);
         104  +
        }
         105  +
         106  +
        if let Some(pattern) = &test_case.input.resource_url_pattern {
         107  +
            builder = builder.resource_pattern(pattern);
         108  +
        }
         109  +
         110  +
        // Handle error cases
         111  +
        if test_case.expected.error == Some(true) {
         112  +
            let result = builder.build();
         113  +
            assert!(
         114  +
                result.is_err(),
         115  +
                "Test {} expected error but succeeded",
         116  +
                test_case.id
         117  +
            );
         118  +
         119  +
            if let Some(error_contains) = &test_case.expected.error_contains {
         120  +
                let error_msg = result.unwrap_err().to_string().to_lowercase();
         121  +
                for expected_text in error_contains {
         122  +
                    assert!(
         123  +
                        error_msg.contains(&expected_text.to_lowercase()),
         124  +
                        "Test {} error message '{}' does not contain '{}'",
         125  +
                        test_case.id,
         126  +
                        error_msg,
         127  +
                        expected_text
         128  +
                    );
         129  +
                }
         130  +
            }
         131  +
            continue;
         132  +
        }
         133  +
         134  +
        let request = builder
         135  +
            .build()
         136  +
            .unwrap_or_else(|e| panic!("Failed to build request for test {}: {}", test_case.id, e));
         137  +
         138  +
        // Determine if this is a URL or cookie test
         139  +
        let is_cookie_test = test_case.expected.cookies.is_some();
         140  +
         141  +
        if is_cookie_test {
         142  +
            // Test signed cookies
         143  +
            let cookies = sign_cookies(request).unwrap_or_else(|e| {
         144  +
                panic!("Failed to sign cookies for test {}: {}", test_case.id, e)
         145  +
            });
         146  +
         147  +
            let cookie_map: HashMap<String, String> = cookies
         148  +
                .iter()
         149  +
                .map(|(k, v)| (k.to_string(), v.to_string()))
         150  +
                .collect();
         151  +
         152  +
            // Verify expected cookies
         153  +
            if let Some(expected_cookies) = &test_case.expected.cookies {
         154  +
                for (key, expected_value) in expected_cookies {
         155  +
                    let actual_value = cookie_map
         156  +
                        .get(key)
         157  +
                        .unwrap_or_else(|| panic!("Test {} missing cookie: {}", test_case.id, key));
         158  +
         159  +
                    assert_eq!(
         160  +
                        actual_value, expected_value,
         161  +
                        "Test {} cookie {} mismatch",
         162  +
                        test_case.id, key
         163  +
                    );
         164  +
                }
         165  +
            }
         166  +
        } else {
         167  +
            // Test signed URL
         168  +
            let signed_url = sign_url(request)
         169  +
                .unwrap_or_else(|e| panic!("Failed to sign URL for test {}: {}", test_case.id, e));
         170  +
         171  +
            let query_params = parse_url_query_params(signed_url.as_str());
         172  +
         173  +
            // Verify expected query parameters
         174  +
            if let Some(expected_params) = &test_case.expected.query_params {
         175  +
                for (key, expected_value) in expected_params {
         176  +
                    let actual_value = query_params.get(key).unwrap_or_else(|| {
         177  +
                        panic!("Test {} missing query param: {}", test_case.id, key)
         178  +
                    });
         179  +
         180  +
                    assert_eq!(
         181  +
                        actual_value, expected_value,
         182  +
                        "Test {} query param {} mismatch",
         183  +
                        test_case.id, key
         184  +
                    );
         185  +
                }
         186  +
            }
         187  +
         188  +
            // Verify signature if provided (skip for ECDSA as it may not be deterministic)
         189  +
            if let Some(expected_signature) = &test_case.expected.signature {
         190  +
                if test_case.expected.signature_algorithm.as_deref() != Some("ECDSA-SHA1") {
         191  +
                    let actual_signature = query_params.get("Signature").unwrap_or_else(|| {
         192  +
                        panic!("Test {} missing Signature query param", test_case.id)
         193  +
                    });
         194  +
         195  +
                    assert_eq!(
         196  +
                        actual_signature, expected_signature,
         197  +
                        "Test {} signature mismatch",
         198  +
                        test_case.id
         199  +
                    );
         200  +
                } else {
         201  +
                    // For ECDSA, just verify signature is present
         202  +
                    assert!(
         203  +
                        query_params.contains_key("Signature"),
         204  +
                        "Test {} missing Signature query param",
         205  +
                        test_case.id
         206  +
                    );
         207  +
                }
         208  +
            }
         209  +
        }
         210  +
         211  +
        println!("✓ Test {} passed", test_case.id);
         212  +
    }
         213  +
}

tmp-codegen-diff/aws-sdk/sdk/aws-sdk-cloudfront-url-signer/tests/test-cases.json

@@ -0,1 +0,184 @@
           1  +
[
           2  +
  {
           3  +
    "id": "canned-policy-url-basic",
           4  +
    "documentation": "Sign a URL with canned policy (expiration only)",
           5  +
    "input": {
           6  +
      "resourceUrl": "https://d111111abcdef8.cloudfront.net/image.jpg",
           7  +
      "keyPairId": "K1TESTKEY",
           8  +
      "privateKeyFile": "test-rsa-key.pem",
           9  +
      "expirationDate": 1767290400
          10  +
    },
          11  +
    "expected": {
          12  +
      "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/image.jpg\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400}}}]}",
          13  +
      "queryParams": {
          14  +
        "Expires": "1767290400",
          15  +
        "Key-Pair-Id": "K1TESTKEY"
          16  +
      },
          17  +
      "signature": "iONoMLnhiCy9q1~WB9GkR2DiHz18I85i3o6kZ64REf-fCSOg-AyXEZiq7fJuS~DT-kbZXjVpgIQqI4sCTcBW9XpO6dyJ5sh8Igk3V~OVncS9acGVnI~ZhHBWiGhU8GmkEMAhn6R2RGO-wKGClrXdJGEUE26XoALdHUzHbmU6AGI_",
          18  +
      "signatureAlgorithm": "RSA-SHA1"
          19  +
    }
          20  +
  },
          21  +
  {
          22  +
    "id": "canned-policy-url-existing-params",
          23  +
    "documentation": "Sign a URL that already has query parameters",
          24  +
    "input": {
          25  +
      "resourceUrl": "https://d111111abcdef8.cloudfront.net/image.jpg?size=large",
          26  +
      "keyPairId": "K1TESTKEY",
          27  +
      "privateKeyFile": "test-rsa-key.pem",
          28  +
      "expirationDate": 1767290400
          29  +
    },
          30  +
    "expected": {
          31  +
      "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/image.jpg?size=large\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400}}}]}",
          32  +
      "queryParams": {
          33  +
        "size": "large",
          34  +
        "Expires": "1767290400",
          35  +
        "Key-Pair-Id": "K1TESTKEY"
          36  +
      },
          37  +
      "signature": "kx5dxV5cSW50EuCMtYi9JI0D5H97if~zNqwKxZtS7FPc3NV5oi~zCb45ti8ve-YDYw45hzN4lceF6zhW~STVWa7r7fC9wi5XBG1T6ZT2b9R0wMWmGxsy9uVGXFn6JqcjQMsq09MnWrPezK0qFQ2X6pV1nGQqLQZ2sgO~FSlEoCo_",
          38  +
      "signatureAlgorithm": "RSA-SHA1"
          39  +
    }
          40  +
  },
          41  +
  {
          42  +
    "id": "custom-policy-url-with-active-date",
          43  +
    "documentation": "Sign a URL with custom policy including activation date",
          44  +
    "input": {
          45  +
      "resourceUrl": "https://d111111abcdef8.cloudfront.net/video.mp4",
          46  +
      "keyPairId": "K1TESTKEY",
          47  +
      "privateKeyFile": "test-rsa-key.pem",
          48  +
      "expirationDate": 1767290400,
          49  +
      "activeDate": 1767200000
          50  +
    },
          51  +
    "expected": {
          52  +
      "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/video.mp4\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400},\"DateGreaterThan\":{\"AWS:EpochTime\":1767200000}}}]}",
          53  +
      "queryParams": {
          54  +
        "Key-Pair-Id": "K1TESTKEY"
          55  +
      },
          56  +
      "signature": "aF0WSOV6dkNn-D0HJ~-EwLG69-4QZZ9dv1PgAZEsER~WXkaNa1pBM2aTVXrskBWVHUU6hc1nD6ZKYgBS5Sb9JluFVT0MbQWR~DrtUGu8ugCsnfzUe6ov38nFPBwQICY~jnoQZEXxO2ZkQrluOO4~jWOpqAGrCw5tkdHAnb9gHYQ_",
          57  +
      "signatureAlgorithm": "RSA-SHA1"
          58  +
    }
          59  +
  },
          60  +
  {
          61  +
    "id": "custom-policy-url-with-ip-range",
          62  +
    "documentation": "Sign a URL with custom policy including IP restriction",
          63  +
    "input": {
          64  +
      "resourceUrl": "https://d111111abcdef8.cloudfront.net/document.pdf",
          65  +
      "keyPairId": "K1TESTKEY",
          66  +
      "privateKeyFile": "test-rsa-key.pem",
          67  +
      "expirationDate": 1767290400,
          68  +
      "ipRange": "192.168.0.0/24"
          69  +
    },
          70  +
    "expected": {
          71  +
      "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/document.pdf\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400},\"IpAddress\":{\"AWS:SourceIp\":\"192.168.0.0/24\"}}}]}",
          72  +
      "queryParams": {
          73  +
        "Key-Pair-Id": "K1TESTKEY"
          74  +
      },
          75  +
      "signature": "fq~vePR~kmc5kWDveZOJ3j6PSYRsHSSanN1shBBpj2rQwvVQPU8e9MRwUDizOYHLnuAXQG89Xm7kXL03HIX0uzELcY2Q6Bw68BcGQHq-nyC2U6h0Uv277pg~FIwatR84y1FZiswIVmZwfPmpkvR0X~1skuooCPGiztAXREe-hsg_",
          76  +
      "signatureAlgorithm": "RSA-SHA1"
          77  +
    }
          78  +
  },
          79  +
  {
          80  +
    "id": "custom-policy-url-with-wildcard",
          81  +
    "documentation": "Sign a URL with wildcard resource pattern",
          82  +
    "input": {
          83  +
      "resourceUrl": "https://d111111abcdef8.cloudfront.net/images/photo1.jpg",
          84  +
      "resourceUrlPattern": "https://d111111abcdef8.cloudfront.net/images/*",
          85  +
      "keyPairId": "K1TESTKEY",
          86  +
      "privateKeyFile": "test-rsa-key.pem",
          87  +
      "expirationDate": 1767290400
          88  +
    },
          89  +
    "expected": {
          90  +
      "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/images/*\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400}}}]}",
          91  +
      "queryParams": {
          92  +
        "Key-Pair-Id": "K1TESTKEY"
          93  +
      },
          94  +
      "signature": "nP~GtuBVUSEUDMXW1ZTGTx~BUQfjl7FsWBQBKwnTNhHlBAeYlzdbMGMphViPQbjawdYt1Z~iBHDr0ctooQyhEdsXwZPQKmRJQm3yIxmbvnuCzB2qdMDVGRKH8l3PnfAJErrgUVIzb6m3KUdtIDvSkLV~Mb0tyQJwZ6QE4BdlEmQ_",
          95  +
      "signatureAlgorithm": "RSA-SHA1"
          96  +
    }
          97  +
  },
          98  +
  {
          99  +
    "id": "canned-policy-cookies",
         100  +
    "documentation": "Generate signed cookies with canned policy",
         101  +
    "input": {
         102  +
      "resourceUrl": "https://d111111abcdef8.cloudfront.net/*",
         103  +
      "keyPairId": "K1TESTKEY",
         104  +
      "privateKeyFile": "test-rsa-key.pem",
         105  +
      "expirationDate": 1767290400
         106  +
    },
         107  +
    "expected": {
         108  +
      "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/*\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400}}}]}",
         109  +
      "cookies": {
         110  +
        "CloudFront-Expires": "1767290400",
         111  +
        "CloudFront-Key-Pair-Id": "K1TESTKEY",
         112  +
        "CloudFront-Signature": "e0yIAbSak8fgGDJfX0bwVT0uDrzdr6fV3kzHuZOGYxeitO8H-tvFuce7~aOGFNReoi6LNhYpvr0wP99~2KKbrbetwLmIdUnTToOabNamMDJSGZgVK31XPWmuxq~TGUB-4v338iQhpP5qk8rkUe3meE5VXlchIb4vvnzph83X~Z8_"
         113  +
      },
         114  +
      "signatureAlgorithm": "RSA-SHA1"
         115  +
    }
         116  +
  },
         117  +
  {
         118  +
    "id": "custom-policy-cookies",
         119  +
    "documentation": "Generate signed cookies with custom policy",
         120  +
    "input": {
         121  +
      "resourceUrl": "https://d111111abcdef8.cloudfront.net/*",
         122  +
      "keyPairId": "K1TESTKEY",
         123  +
      "privateKeyFile": "test-rsa-key.pem",
         124  +
      "expirationDate": 1767290400,
         125  +
      "ipRange": "10.0.0.0/8"
         126  +
    },
         127  +
    "expected": {
         128  +
      "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/*\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400},\"IpAddress\":{\"AWS:SourceIp\":\"10.0.0.0/8\"}}}]}",
         129  +
      "cookies": {
         130  +
        "CloudFront-Key-Pair-Id": "K1TESTKEY",
         131  +
        "CloudFront-Policy": "eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9kMTExMTExYWJjZGVmOC5jbG91ZGZyb250Lm5ldC8qIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNzY3MjkwNDAwfSwiSXBBZGRyZXNzIjp7IkFXUzpTb3VyY2VJcCI6IjEwLjAuMC4wLzgifX19XX0_",
         132  +
        "CloudFront-Signature": "r28Jnd0t9aq7cu0k9jGWl4L0YsRxgueZtGRw5oEEspU9-eIPGM~ZGMQh36~5HpKC5c67cZjDgJcsqrCacmTHMZZx613gbeYAsx2-hEatU8URiuNHnVp4hPV3HqtbuZ6Din9iEZUpOBYVg6DWGEFJRCQ7SPouBhhJdYDZZOPHGpA_"
         133  +
      },
         134  +
      "signatureAlgorithm": "RSA-SHA1"
         135  +
    }
         136  +
  },
         137  +
  {
         138  +
    "id": "ecdsa-canned-policy-url",
         139  +
    "documentation": "Sign a URL with ECDSA key and canned policy",
         140  +
    "input": {
         141  +
      "resourceUrl": "https://d111111abcdef8.cloudfront.net/video.mp4",
         142  +
      "keyPairId": "K2ECDSATEST",
         143  +
      "privateKeyFile": "test-ecdsa-key.pem",
         144  +
      "expirationDate": 1767290400
         145  +
    },
         146  +
    "expected": {
         147  +
      "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/video.mp4\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400}}}]}",
         148  +
      "queryParams": {
         149  +
        "Expires": "1767290400",
         150  +
        "Key-Pair-Id": "K2ECDSATEST"
         151  +
      },
         152  +
      "signature": "MEQCIDS3JTjLLIRne5G3fDjf6MwgCmckmYVlJhqGVMl0Q4reAiBjXw1FMf9j03wqAHeE4LfRPjOVkp-jWhmDfHVwd7kkpA__",
         153  +
      "signatureAlgorithm": "ECDSA-SHA1"
         154  +
    }
         155  +
  },
         156  +
  {
         157  +
    "id": "error-missing-expiration",
         158  +
    "documentation": "Error when expiration date is not provided",
         159  +
    "input": {
         160  +
      "resourceUrl": "https://d111111abcdef8.cloudfront.net/file.txt",
         161  +
      "keyPairId": "K1TESTKEY",
         162  +
      "privateKeyFile": "test-rsa-key.pem"
         163  +
    },
         164  +
    "expected": {
         165  +
      "error": true,
         166  +
      "errorContains": ["expiration", "required"]
         167  +
    }
         168  +
  },
         169  +
  {
         170  +
    "id": "error-active-after-expiration",
         171  +
    "documentation": "Error when activeDate is after expirationDate",
         172  +
    "input": {
         173  +
      "resourceUrl": "https://d111111abcdef8.cloudfront.net/file.txt",
         174  +
      "keyPairId": "K1TESTKEY",
         175  +
      "privateKeyFile": "test-rsa-key.pem",
         176  +
      "expirationDate": 1767200000,
         177  +
      "activeDate": 1767290400
         178  +
    },
         179  +
    "expected": {
         180  +
      "error": true,
         181  +
      "errorContains": ["active", "before", "expiration"]
         182  +
    }
         183  +
  }
         184  +
]