AWS SDK

AWS SDK

rev. bd1596815d3b904d2fec7cce76050bed976c9d6b (ignoring whitespace)

Files changed:

tmp-codegen-diff/aws-sdk/sdk/aws-sdk-cloudfront-url-signer/Cargo.toml

@@ -0,1 +0,62 @@
           1  +
# Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT.
           2  +
[package]
           3  +
name = "aws-sdk-cloudfront-url-signer"
           4  +
version = "0.1.0"
           5  +
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
           6  +
description = "CloudFront URL and cookie signing utilities for AWS SDK for Rust"
           7  +
edition = "2021"
           8  +
license = "Apache-2.0"
           9  +
repository = "https://github.com/smithy-lang/smithy-rs"
          10  +
[package.metadata.docs.rs]
          11  +
all-features = true
          12  +
targets = ["x86_64-unknown-linux-gnu"]
          13  +
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
          14  +
rustdoc-args = ["--cfg", "docsrs"]
          15  +
          16  +
[package.metadata.smithy-rs-release-tooling]
          17  +
stable = false
          18  +
          19  +
[dependencies]
          20  +
rsa = "0.9.9"
          21  +
base64-simd = "0.8.0"
          22  +
url = "2.5.4"
          23  +
          24  +
[dependencies.aws-smithy-types]
          25  +
path = "../aws-smithy-types"
          26  +
version = "1.3.5"
          27  +
          28  +
[dependencies.aws-smithy-async]
          29  +
path = "../aws-smithy-async"
          30  +
version = "1.2.7"
          31  +
          32  +
[dependencies.sha1]
          33  +
version = "0.10.6"
          34  +
features = ["oid"]
          35  +
          36  +
[dependencies.tokio]
          37  +
version = "1.40.0"
          38  +
features = ["fs"]
          39  +
optional = true
          40  +
          41  +
[dependencies.aws-smithy-json]
          42  +
path = "../aws-smithy-json"
          43  +
version = "0.61.8"
          44  +
          45  +
[dependencies.p256]
          46  +
version = "0.13.2"
          47  +
features = ["ecdsa", "pem"]
          48  +
          49  +
[dependencies.http]
          50  +
version = "1.1"
          51  +
optional = true
          52  +
          53  +
[dev-dependencies]
          54  +
serde_json = "1"
          55  +
          56  +
[dev-dependencies.serde]
          57  +
version = "1"
          58  +
features = ["derive"]
          59  +
          60  +
[features]
          61  +
rt-tokio = ["dep:tokio"]
          62  +
http-1x = ["dep:http"]

tmp-codegen-diff/aws-sdk/sdk/aws-sdk-cloudfront-url-signer/LICENSE

@@ -0,1 +0,175 @@
           1  +
           2  +
                                 Apache License
           3  +
                           Version 2.0, January 2004
           4  +
                        http://www.apache.org/licenses/
           5  +
           6  +
   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
           7  +
           8  +
   1. Definitions.
           9  +
          10  +
      "License" shall mean the terms and conditions for use, reproduction,
          11  +
      and distribution as defined by Sections 1 through 9 of this document.
          12  +
          13  +
      "Licensor" shall mean the copyright owner or entity authorized by
          14  +
      the copyright owner that is granting the License.
          15  +
          16  +
      "Legal Entity" shall mean the union of the acting entity and all
          17  +
      other entities that control, are controlled by, or are under common
          18  +
      control with that entity. For the purposes of this definition,
          19  +
      "control" means (i) the power, direct or indirect, to cause the
          20  +
      direction or management of such entity, whether by contract or
          21  +
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
          22  +
      outstanding shares, or (iii) beneficial ownership of such entity.
          23  +
          24  +
      "You" (or "Your") shall mean an individual or Legal Entity
          25  +
      exercising permissions granted by this License.
          26  +
          27  +
      "Source" form shall mean the preferred form for making modifications,
          28  +
      including but not limited to software source code, documentation
          29  +
      source, and configuration files.
          30  +
          31  +
      "Object" form shall mean any form resulting from mechanical
          32  +
      transformation or translation of a Source form, including but
          33  +
      not limited to compiled object code, generated documentation,
          34  +
      and conversions to other media types.
          35  +
          36  +
      "Work" shall mean the work of authorship, whether in Source or
          37  +
      Object form, made available under the License, as indicated by a
          38  +
      copyright notice that is included in or attached to the work
          39  +
      (an example is provided in the Appendix below).
          40  +
          41  +
      "Derivative Works" shall mean any work, whether in Source or Object
          42  +
      form, that is based on (or derived from) the Work and for which the
          43  +
      editorial revisions, annotations, elaborations, or other modifications
          44  +
      represent, as a whole, an original work of authorship. For the purposes
          45  +
      of this License, Derivative Works shall not include works that remain
          46  +
      separable from, or merely link (or bind by name) to the interfaces of,
          47  +
      the Work and Derivative Works thereof.
          48  +
          49  +
      "Contribution" shall mean any work of authorship, including
          50  +
      the original version of the Work and any modifications or additions
          51  +
      to that Work or Derivative Works thereof, that is intentionally
          52  +
      submitted to Licensor for inclusion in the Work by the copyright owner
          53  +
      or by an individual or Legal Entity authorized to submit on behalf of
          54  +
      the copyright owner. For the purposes of this definition, "submitted"
          55  +
      means any form of electronic, verbal, or written communication sent
          56  +
      to the Licensor or its representatives, including but not limited to
          57  +
      communication on electronic mailing lists, source code control systems,
          58  +
      and issue tracking systems that are managed by, or on behalf of, the
          59  +
      Licensor for the purpose of discussing and improving the Work, but
          60  +
      excluding communication that is conspicuously marked or otherwise
          61  +
      designated in writing by the copyright owner as "Not a Contribution."
          62  +
          63  +
      "Contributor" shall mean Licensor and any individual or Legal Entity
          64  +
      on behalf of whom a Contribution has been received by Licensor and
          65  +
      subsequently incorporated within the Work.
          66  +
          67  +
   2. Grant of Copyright License. Subject to the terms and conditions of
          68  +
      this License, each Contributor hereby grants to You a perpetual,
          69  +
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
          70  +
      copyright license to reproduce, prepare Derivative Works of,
          71  +
      publicly display, publicly perform, sublicense, and distribute the
          72  +
      Work and such Derivative Works in Source or Object form.
          73  +
          74  +
   3. Grant of Patent License. Subject to the terms and conditions of
          75  +
      this License, each Contributor hereby grants to You a perpetual,
          76  +
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
          77  +
      (except as stated in this section) patent license to make, have made,
          78  +
      use, offer to sell, sell, import, and otherwise transfer the Work,
          79  +
      where such license applies only to those patent claims licensable
          80  +
      by such Contributor that are necessarily infringed by their
          81  +
      Contribution(s) alone or by combination of their Contribution(s)
          82  +
      with the Work to which such Contribution(s) was submitted. If You
          83  +
      institute patent litigation against any entity (including a
          84  +
      cross-claim or counterclaim in a lawsuit) alleging that the Work
          85  +
      or a Contribution incorporated within the Work constitutes direct
          86  +
      or contributory patent infringement, then any patent licenses
          87  +
      granted to You under this License for that Work shall terminate
          88  +
      as of the date such litigation is filed.
          89  +
          90  +
   4. Redistribution. You may reproduce and distribute copies of the
          91  +
      Work or Derivative Works thereof in any medium, with or without
          92  +
      modifications, and in Source or Object form, provided that You
          93  +
      meet the following conditions:
          94  +
          95  +
      (a) You must give any other recipients of the Work or
          96  +
          Derivative Works a copy of this License; and
          97  +
          98  +
      (b) You must cause any modified files to carry prominent notices
          99  +
          stating that You changed the files; and
         100  +
         101  +
      (c) You must retain, in the Source form of any Derivative Works
         102  +
          that You distribute, all copyright, patent, trademark, and
         103  +
          attribution notices from the Source form of the Work,
         104  +
          excluding those notices that do not pertain to any part of
         105  +
          the Derivative Works; and
         106  +
         107  +
      (d) If the Work includes a "NOTICE" text file as part of its
         108  +
          distribution, then any Derivative Works that You distribute must
         109  +
          include a readable copy of the attribution notices contained
         110  +
          within such NOTICE file, excluding those notices that do not
         111  +
          pertain to any part of the Derivative Works, in at least one
         112  +
          of the following places: within a NOTICE text file distributed
         113  +
          as part of the Derivative Works; within the Source form or
         114  +
          documentation, if provided along with the Derivative Works; or,
         115  +
          within a display generated by the Derivative Works, if and
         116  +
          wherever such third-party notices normally appear. The contents
         117  +
          of the NOTICE file are for informational purposes only and
         118  +
          do not modify the License. You may add Your own attribution
         119  +
          notices within Derivative Works that You distribute, alongside
         120  +
          or as an addendum to the NOTICE text from the Work, provided
         121  +
          that such additional attribution notices cannot be construed
         122  +
          as modifying the License.
         123  +
         124  +
      You may add Your own copyright statement to Your modifications and
         125  +
      may provide additional or different license terms and conditions
         126  +
      for use, reproduction, or distribution of Your modifications, or
         127  +
      for any such Derivative Works as a whole, provided Your use,
         128  +
      reproduction, and distribution of the Work otherwise complies with
         129  +
      the conditions stated in this License.
         130  +
         131  +
   5. Submission of Contributions. Unless You explicitly state otherwise,
         132  +
      any Contribution intentionally submitted for inclusion in the Work
         133  +
      by You to the Licensor shall be under the terms and conditions of
         134  +
      this License, without any additional terms or conditions.
         135  +
      Notwithstanding the above, nothing herein shall supersede or modify
         136  +
      the terms of any separate license agreement you may have executed
         137  +
      with Licensor regarding such Contributions.
         138  +
         139  +
   6. Trademarks. This License does not grant permission to use the trade
         140  +
      names, trademarks, service marks, or product names of the Licensor,
         141  +
      except as required for reasonable and customary use in describing the
         142  +
      origin of the Work and reproducing the content of the NOTICE file.
         143  +
         144  +
   7. Disclaimer of Warranty. Unless required by applicable law or
         145  +
      agreed to in writing, Licensor provides the Work (and each
         146  +
      Contributor provides its Contributions) on an "AS IS" BASIS,
         147  +
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
         148  +
      implied, including, without limitation, any warranties or conditions
         149  +
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
         150  +
      PARTICULAR PURPOSE. You are solely responsible for determining the
         151  +
      appropriateness of using or redistributing the Work and assume any
         152  +
      risks associated with Your exercise of permissions under this License.
         153  +
         154  +
   8. Limitation of Liability. In no event and under no legal theory,
         155  +
      whether in tort (including negligence), contract, or otherwise,
         156  +
      unless required by applicable law (such as deliberate and grossly
         157  +
      negligent acts) or agreed to in writing, shall any Contributor be
         158  +
      liable to You for damages, including any direct, indirect, special,
         159  +
      incidental, or consequential damages of any character arising as a
         160  +
      result of this License or out of the use or inability to use the
         161  +
      Work (including but not limited to damages for loss of goodwill,
         162  +
      work stoppage, computer failure or malfunction, or any and all
         163  +
      other commercial damages or losses), even if such Contributor
         164  +
      has been advised of the possibility of such damages.
         165  +
         166  +
   9. Accepting Warranty or Additional Liability. While redistributing
         167  +
      the Work or Derivative Works thereof, You may choose to offer,
         168  +
      and charge a fee for, acceptance of support, warranty, indemnity,
         169  +
      or other liability obligations and/or rights consistent with this
         170  +
      License. However, in accepting such obligations, You may act only
         171  +
      on Your own behalf and on Your sole responsibility, not on behalf
         172  +
      of any other Contributor, and only if You agree to indemnify,
         173  +
      defend, and hold each Contributor harmless for any liability
         174  +
      incurred by, or claims asserted against, such Contributor by reason
         175  +
      of your accepting any such warranty or additional liability.

tmp-codegen-diff/aws-sdk/sdk/aws-sdk-cloudfront-url-signer/README.md

@@ -0,1 +0,329 @@
           1  +
# aws-sdk-cloudfront-url-signer
           2  +
           3  +
A library for generating signed URLs and cookies for Amazon CloudFront private content.
           4  +
           5  +
This crate provides utilities to create cryptographically signed URLs and cookies that grant
           6  +
time-limited access to private CloudFront distributions. It supports both RSA-SHA1 and
           7  +
ECDSA-SHA1 signing algorithms with canned (simple) and custom (advanced) policies.
           8  +
           9  +
## Key Features
          10  +
          11  +
- **Signed URLs**: Generate URLs with embedded signatures for single-resource access
          12  +
- **Signed Cookies**: Generate cookies for multi-resource access without URL modification
          13  +
- **Canned Policies**: Simple time-based expiration
          14  +
- **Custom Policies**: Advanced access control with activation dates, IP restrictions, and wildcards
          15  +
- **Multiple Key Formats**: RSA (PKCS#1/PKCS#8) and ECDSA P-256 (PKCS#8) private keys
          16  +
          17  +
## When to Use Signed URLs vs Cookies
          18  +
          19  +
**Use signed URLs when:**
          20  +
- Restricting access to individual files (e.g., a download link)
          21  +
- Users are accessing content through a client that doesn't support cookies
          22  +
- You want to share a link that works without additional setup
          23  +
          24  +
**Use signed cookies when:**
          25  +
- Providing access to multiple restricted files (e.g., all files in a subscriber area)
          26  +
- You don't want to change your existing URLs
          27  +
- You're building a web application where cookies are naturally handled
          28  +
          29  +
## Feature Flags
          30  +
          31  +
| Feature | Description |
          32  +
|---------|-------------|
          33  +
| `rt-tokio` | Enables async file loading with `PrivateKey::from_pem_file()` |
          34  +
| `http-1x` | Enables conversion to `http::Request` types |
          35  +
          36  +
## CloudFront Setup
          37  +
          38  +
Before using this library, you need to:
          39  +
          40  +
1. Create a CloudFront key pair in the AWS Console or via CLI
          41  +
2. Upload the public key to CloudFront
          42  +
3. Create a key group containing your public key
          43  +
4. Configure your CloudFront distribution to use the key group for restricted content
          44  +
5. Keep the private key secure for use with this library
          45  +
          46  +
See the [CloudFront Developer Guide](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html) for detailed setup instructions.
          47  +
          48  +
## Basic Usage
          49  +
          50  +
### Signing a URL with Canned Policy
          51  +
          52  +
Canned policies provide simple time-based access control with just an expiration time:
          53  +
          54  +
```rust,ignore
          55  +
use aws_sdk_cloudfront_url_signer::{sign_url, SigningRequest, PrivateKey};
          56  +
use aws_smithy_types::DateTime;
          57  +
          58  +
// Load your CloudFront private key
          59  +
let private_key = PrivateKey::from_pem(include_bytes!("private_key.pem"))?;
          60  +
          61  +
// Create a signing request
          62  +
let request = SigningRequest::builder()
          63  +
    .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg")
          64  +
    .key_pair_id("APKAEIBAERJR2EXAMPLE")
          65  +
    .private_key(private_key)
          66  +
    .expires_at(DateTime::from_secs(1767290400))  // Absolute expiration
          67  +
    .build()?;
          68  +
          69  +
// Generate the signed URL
          70  +
let signed_url = sign_url(request)?;
          71  +
println!("Signed URL: {}", signed_url);
          72  +
```
          73  +
          74  +
The resulting URL will include `Expires`, `Signature`, and `Key-Pair-Id` query parameters.
          75  +
          76  +
### Using Relative Expiration
          77  +
          78  +
Instead of an absolute timestamp, you can specify a duration from now:
          79  +
          80  +
```rust,ignore
          81  +
use std::time::Duration;
          82  +
          83  +
let request = SigningRequest::builder()
          84  +
    .resource_url("https://d111111abcdef8.cloudfront.net/video.mp4")
          85  +
    .key_pair_id("APKAEIBAERJR2EXAMPLE")
          86  +
    .private_key(private_key)
          87  +
    .expires_in(Duration::from_secs(3600))  // Valid for 1 hour
          88  +
    .build()?;
          89  +
```
          90  +
          91  +
### Signing a URL with Custom Policy
          92  +
          93  +
Custom policies enable advanced access control with activation dates, IP restrictions, and wildcard patterns:
          94  +
          95  +
```rust,ignore
          96  +
use aws_sdk_cloudfront_url_signer::{sign_url, SigningRequest, PrivateKey};
          97  +
use aws_smithy_types::DateTime;
          98  +
          99  +
let private_key = PrivateKey::from_pem(include_bytes!("private_key.pem"))?;
         100  +
         101  +
let request = SigningRequest::builder()
         102  +
    .resource_url("https://d111111abcdef8.cloudfront.net/videos/*")
         103  +
    .key_pair_id("APKAEIBAERJR2EXAMPLE")
         104  +
    .private_key(private_key)
         105  +
    .expires_at(DateTime::from_secs(1767290400))
         106  +
    .active_at(DateTime::from_secs(1767200000))  // Not valid before this time
         107  +
    .ip_range("192.0.2.0/24")                    // Restrict to IP range
         108  +
    .build()?;
         109  +
         110  +
let signed_url = sign_url(request)?;
         111  +
```
         112  +
         113  +
Custom policy URLs include a `Policy` parameter (base64-encoded JSON) instead of `Expires`.
         114  +
         115  +
### Generating Signed Cookies
         116  +
         117  +
Signed cookies work similarly but return cookie name-value pairs:
         118  +
         119  +
```rust,ignore
         120  +
use aws_sdk_cloudfront_url_signer::{sign_cookies, SigningRequest, PrivateKey};
         121  +
use aws_smithy_types::DateTime;
         122  +
         123  +
let private_key = PrivateKey::from_pem(include_bytes!("private_key.pem"))?;
         124  +
         125  +
let request = SigningRequest::builder()
         126  +
    .resource_url("https://d111111abcdef8.cloudfront.net/*")
         127  +
    .key_pair_id("APKAEIBAERJR2EXAMPLE")
         128  +
    .private_key(private_key)
         129  +
    .expires_at(DateTime::from_secs(1767290400))
         130  +
    .build()?;
         131  +
         132  +
let cookies = sign_cookies(request)?;
         133  +
         134  +
// Set cookies in your HTTP response
         135  +
for (name, value) in cookies.iter() {
         136  +
    println!("Set-Cookie: {}={}; Domain=d111111abcdef8.cloudfront.net; Secure; HttpOnly", name, value);
         137  +
}
         138  +
```
         139  +
         140  +
For canned policies, cookies include:
         141  +
- `CloudFront-Expires`
         142  +
- `CloudFront-Signature`
         143  +
- `CloudFront-Key-Pair-Id`
         144  +
         145  +
For custom policies, cookies include:
         146  +
- `CloudFront-Policy`
         147  +
- `CloudFront-Signature`
         148  +
- `CloudFront-Key-Pair-Id`
         149  +
         150  +
## Private Key Loading
         151  +
         152  +
### From PEM Bytes
         153  +
         154  +
Load a key from bytes (useful when loading from AWS Secrets Manager or environment variables):
         155  +
         156  +
```rust,ignore
         157  +
use aws_sdk_cloudfront_url_signer::PrivateKey;
         158  +
         159  +
// From a byte slice
         160  +
let key = PrivateKey::from_pem(include_bytes!("private_key.pem"))?;
         161  +
         162  +
// From a string
         163  +
let pem_string = std::fs::read_to_string("private_key.pem")?;
         164  +
let key = PrivateKey::from_pem(pem_string.as_bytes())?;
         165  +
```
         166  +
         167  +
### From File (Async)
         168  +
         169  +
With the `rt-tokio` feature enabled, you can load keys directly from files:
         170  +
         171  +
```rust,ignore
         172  +
use aws_sdk_cloudfront_url_signer::PrivateKey;
         173  +
         174  +
let key = PrivateKey::from_pem_file("private_key.pem").await?;
         175  +
```
         176  +
         177  +
### Supported Key Formats
         178  +
         179  +
| Format | Header | Key Type |
         180  +
|--------|--------|----------|
         181  +
| PKCS#1 | `-----BEGIN RSA PRIVATE KEY-----` | RSA only |
         182  +
| PKCS#8 | `-----BEGIN PRIVATE KEY-----` | RSA or ECDSA P-256 |
         183  +
         184  +
Both RSA and ECDSA keys use SHA-1 signatures (required by CloudFront).
         185  +
         186  +
## Policy Types
         187  +
         188  +
### Canned Policy
         189  +
         190  +
A canned policy is automatically used when you only specify an expiration time. It's simpler and produces shorter URLs:
         191  +
         192  +
```rust,ignore
         193  +
let request = SigningRequest::builder()
         194  +
    .resource_url("https://example.cloudfront.net/file.pdf")
         195  +
    .key_pair_id("APKAEXAMPLE")
         196  +
    .private_key(key)
         197  +
    .expires_at(DateTime::from_secs(1767290400))
         198  +
    .build()?;
         199  +
```
         200  +
         201  +
### Custom Policy
         202  +
         203  +
A custom policy is used when you specify any of:
         204  +
- `resource_pattern()` - Wildcard pattern for the policy (different from the signed URL)
         205  +
- `active_at()` - URL becomes valid at this time (not-before)
         206  +
- `ip_range()` - Restrict access to an IPv4 CIDR range
         207  +
         208  +
```rust,ignore
         209  +
let request = SigningRequest::builder()
         210  +
    .resource_url("https://example.cloudfront.net/premium/video.mp4")
         211  +
    .key_pair_id("APKAEXAMPLE")
         212  +
    .private_key(key)
         213  +
    .expires_at(DateTime::from_secs(1767290400))
         214  +
    .active_at(DateTime::from_secs(1767200000))  // Triggers custom policy
         215  +
    .ip_range("10.0.0.0/8")                      // Also triggers custom policy
         216  +
    .build()?;
         217  +
```
         218  +
         219  +
## Wildcard Patterns
         220  +
         221  +
Custom policies support wildcards in the resource pattern. Use `resource_pattern()` to specify
         222  +
a wildcard pattern that grants access to multiple resources, while `resource_url()` specifies
         223  +
the actual URL being signed:
         224  +
         225  +
- `*` matches zero or more characters
         226  +
- `?` matches exactly one character
         227  +
         228  +
```rust,ignore
         229  +
// Sign a specific URL but grant access to all files under /videos/
         230  +
let request = SigningRequest::builder()
         231  +
    .resource_url("https://d111111abcdef8.cloudfront.net/videos/intro.mp4")
         232  +
    .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*")
         233  +
    .key_pair_id("APKAEXAMPLE")
         234  +
    .private_key(key)
         235  +
    .expires_at(DateTime::from_secs(1767290400))
         236  +
    .build()?;
         237  +
         238  +
// The signed URL points to intro.mp4, but the policy grants access to all /videos/*
         239  +
```
         240  +
         241  +
Common wildcard patterns:
         242  +
- `https://example.cloudfront.net/videos/*` - All files under /videos/
         243  +
- `https://example.cloudfront.net/*.mp4` - All .mp4 files
         244  +
- `https://example.cloudfront.net/video-?.mp4` - video-1.mp4, video-2.mp4, etc.
         245  +
- `*` - All resources (use with caution)
         246  +
         247  +
## Using Signed URLs with HTTP Clients
         248  +
         249  +
The `SignedUrl` type provides multiple ways to access the signed URL for use with HTTP clients:
         250  +
         251  +
```rust,ignore
         252  +
let signed_url = sign_url(request)?;
         253  +
         254  +
// As a string slice
         255  +
let url_str: &str = signed_url.as_str();
         256  +
         257  +
// As a parsed url::Url
         258  +
let url: &url::Url = signed_url.as_url();
         259  +
         260  +
// Convert to owned url::Url
         261  +
let url: url::Url = signed_url.into_url();
         262  +
         263  +
// Use with reqwest (reqwest re-exports url::Url)
         264  +
let response = reqwest::get(signed_url.as_url()).await?;
         265  +
         266  +
// Display trait
         267  +
println!("Signed URL: {}", signed_url);
         268  +
```
         269  +
         270  +
### HTTP 1.x Integration
         271  +
         272  +
With the `http-1x` feature enabled, you can convert signed URLs directly to `http::Request`:
         273  +
         274  +
```rust,ignore
         275  +
use http::Request;
         276  +
         277  +
let signed_url = sign_url(request)?;
         278  +
         279  +
// Convert to http::Request
         280  +
let http_request: Request<()> = signed_url.try_into()?;
         281  +
         282  +
// Or from a reference
         283  +
let http_request: Request<()> = (&signed_url).try_into()?;
         284  +
```
         285  +
         286  +
This is useful when working with HTTP clients that use the `http` crate types.
         287  +
## Error Handling
         288  +
         289  +
All operations return `Result<T, SigningError>`:
         290  +
         291  +
```rust,ignore
         292  +
use aws_sdk_cloudfront_url_signer::{sign_url, SigningRequest, PrivateKey, error::SigningError};
         293  +
         294  +
let result = sign_url(request);
         295  +
match result {
         296  +
    Ok(signed_url) => println!("Success: {}", signed_url),
         297  +
    Err(e) => {
         298  +
        eprintln!("Signing failed: {}", e);
         299  +
        if let Some(source) = e.source() {
         300  +
            eprintln!("Caused by: {}", source);
         301  +
        }
         302  +
    }
         303  +
}
         304  +
```
         305  +
         306  +
Common error scenarios:
         307  +
- Invalid private key format
         308  +
- Missing required fields (resource_url, key_pair_id, private_key, expiration)
         309  +
- Cryptographic signing failures
         310  +
         311  +
## URLs with Existing Query Parameters
         312  +
         313  +
The library correctly handles URLs that already have query parameters:
         314  +
         315  +
```rust,ignore
         316  +
let request = SigningRequest::builder()
         317  +
    .resource_url("https://d111111abcdef8.cloudfront.net/video.mp4?quality=hd")
         318  +
    .key_pair_id("APKAEXAMPLE")
         319  +
    .private_key(key)
         320  +
    .expires_at(DateTime::from_secs(1767290400))
         321  +
    .build()?;
         322  +
         323  +
let signed_url = sign_url(request)?;
         324  +
// Result: https://...?quality=hd&Expires=...&Signature=...&Key-Pair-Id=...
         325  +
```
         326  +
         327  +
<!-- anchor_start:footer -->
         328  +
This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/smithy-lang/smithy-rs) code generator.
         329  +
<!-- anchor_end:footer -->

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

@@ -0,1 +0,122 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
use std::borrow::Cow;
           7  +
use std::error::Error as StdError;
           8  +
use std::fmt;
           9  +
          10  +
#[derive(Debug)]
          11  +
pub(crate) enum ErrorKind {
          12  +
    InvalidKey,
          13  +
    InvalidPolicy,
          14  +
    InvalidInput,
          15  +
    SigningFailure,
          16  +
}
          17  +
          18  +
/// Error type for CloudFront signing operations
          19  +
#[derive(Debug)]
          20  +
pub struct SigningError {
          21  +
    kind: ErrorKind,
          22  +
    source: Option<Box<dyn StdError + Send + Sync>>,
          23  +
    message: Option<Cow<'static, str>>,
          24  +
}
          25  +
          26  +
impl SigningError {
          27  +
    pub(crate) fn new(
          28  +
        kind: ErrorKind,
          29  +
        source: Option<Box<dyn StdError + Send + Sync>>,
          30  +
        message: Option<Cow<'static, str>>,
          31  +
    ) -> Self {
          32  +
        Self {
          33  +
            kind,
          34  +
            source,
          35  +
            message,
          36  +
        }
          37  +
    }
          38  +
          39  +
    pub(crate) fn invalid_key(source: impl Into<Box<dyn StdError + Send + Sync>>) -> Self {
          40  +
        Self::new(ErrorKind::InvalidKey, Some(source.into()), None)
          41  +
    }
          42  +
          43  +
    pub(crate) fn invalid_policy(message: impl Into<Cow<'static, str>>) -> Self {
          44  +
        Self::new(ErrorKind::InvalidPolicy, None, Some(message.into()))
          45  +
    }
          46  +
          47  +
    pub(crate) fn invalid_input(message: impl Into<Cow<'static, str>>) -> Self {
          48  +
        Self::new(ErrorKind::InvalidInput, None, Some(message.into()))
          49  +
    }
          50  +
          51  +
    pub(crate) fn signing_failure(source: impl Into<Box<dyn StdError + Send + Sync>>) -> Self {
          52  +
        Self::new(ErrorKind::SigningFailure, Some(source.into()), None)
          53  +
    }
          54  +
}
          55  +
          56  +
impl fmt::Display for SigningError {
          57  +
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
          58  +
        match self.kind {
          59  +
            ErrorKind::InvalidKey => write!(f, "invalid private key"),
          60  +
            ErrorKind::InvalidPolicy => {
          61  +
                write!(f, "invalid policy")?;
          62  +
                if let Some(ref msg) = self.message {
          63  +
                    write!(f, ": {msg}")?;
          64  +
                }
          65  +
                Ok(())
          66  +
            }
          67  +
            ErrorKind::InvalidInput => {
          68  +
                write!(f, "invalid input")?;
          69  +
                if let Some(ref msg) = self.message {
          70  +
                    write!(f, ": {msg}")?;
          71  +
                }
          72  +
                Ok(())
          73  +
            }
          74  +
            ErrorKind::SigningFailure => write!(f, "signing operation failed"),
          75  +
        }
          76  +
    }
          77  +
}
          78  +
          79  +
impl StdError for SigningError {
          80  +
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
          81  +
        self.source.as_ref().map(|e| e.as_ref() as _)
          82  +
    }
          83  +
}
          84  +
          85  +
impl From<ErrorKind> for SigningError {
          86  +
    fn from(kind: ErrorKind) -> Self {
          87  +
        Self::new(kind, None, None)
          88  +
    }
          89  +
}
          90  +
          91  +
#[cfg(test)]
          92  +
mod tests {
          93  +
    use super::*;
          94  +
          95  +
    #[test]
          96  +
    fn test_invalid_key_display() {
          97  +
        let err = SigningError::invalid_key("test error");
          98  +
        assert_eq!(err.to_string(), "invalid private key");
          99  +
        assert!(err.source().is_some());
         100  +
    }
         101  +
         102  +
    #[test]
         103  +
    fn test_invalid_policy_display() {
         104  +
        let err = SigningError::invalid_policy("missing expires_at");
         105  +
        assert_eq!(err.to_string(), "invalid policy: missing expires_at");
         106  +
        assert!(err.source().is_none());
         107  +
    }
         108  +
         109  +
    #[test]
         110  +
    fn test_invalid_input_display() {
         111  +
        let err = SigningError::invalid_input("empty URL");
         112  +
        assert_eq!(err.to_string(), "invalid input: empty URL");
         113  +
        assert!(err.source().is_none());
         114  +
    }
         115  +
         116  +
    #[test]
         117  +
    fn test_signing_failure_display() {
         118  +
        let err = SigningError::signing_failure("RSA error");
         119  +
        assert_eq!(err.to_string(), "signing operation failed");
         120  +
        assert!(err.source().is_some());
         121  +
    }
         122  +
}

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

@@ -0,1 +0,153 @@
           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::SigningError;
           7  +
use rsa::pkcs1::DecodeRsaPrivateKey;
           8  +
use rsa::RsaPrivateKey;
           9  +
use sha1::{Digest, Sha1};
          10  +
          11  +
use p256::ecdsa::signature::SignatureEncoding;
          12  +
#[cfg(feature = "rt-tokio")]
          13  +
use std::path::Path;
          14  +
          15  +
/// Private key for signing CloudFront URLs and cookies.
          16  +
#[derive(Debug, Clone)]
          17  +
pub enum PrivateKey {
          18  +
    /// RSA private key.
          19  +
    Rsa(Box<RsaPrivateKey>),
          20  +
    /// ECDSA P-256 private key.
          21  +
    Ecdsa(p256::ecdsa::SigningKey),
          22  +
}
          23  +
          24  +
impl 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)]
         101  +
mod tests {
         102  +
    use super::*;
         103  +
         104  +
    const TEST_RSA_KEY_PEM: &[u8] = b"-----BEGIN RSA PRIVATE KEY-----
         105  +
MIIBPAIBAAJBANW8WjQksUoX/7nwOfRDNt1XQpLCueHoXSt91MASMOSAqpbzZvXO
         106  +
g2hW2gCFUIFUPCByMXPoeRe6iUZ5JtjepssCAwEAAQJBALR7ybwQY/lKTLKJrZab
         107  +
D4BXCCt/7ZFbMxnftsC+W7UHef4S4qFW8oOOLeYfmyGZK1h44rXf2AIp4PndKUID
         108  +
1zECIQD1suunYw5U22Pa0+2dFThp1VMXdVbPuf/5k3HT2/hSeQIhAN6yX0aT/N6G
         109  +
gb1XlBKw6GQvhcM0fXmP+bVXV+RtzFJjAiAP+2Z2yeu5u1egeV6gdCvqPnUcNobC
         110  +
FmA/NMcXt9xMSQIhALEMMJEFAInNeAIXSYKeoPNdkMPDzGnD3CueuCLEZCevAiEA
         111  +
j+KnJ7pJkTvOzFwE8RfNLli9jf6/OhyYaLL4et7Ng5k=
         112  +
-----END RSA PRIVATE KEY-----";
         113  +
         114  +
    const TEST_ECDSA_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
         115  +
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg4//aTM1/HqiVWagy
         116  +
01cAx3EaegJ0Y5KLRoTtub8T8EWhRANCAARV/wa477wYpyWB5LCrCdS5M9bEAvD+
         117  +
VORtjoydSpheKlsa+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  +
}

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

@@ -0,1 +0,35 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
/* Automatically managed default lints */
           7  +
#![cfg_attr(docsrs, feature(doc_cfg))]
           8  +
/* End of automatically managed default lints */
           9  +
#![doc = include_str!("../README.md")]
          10  +
#![warn(
          11  +
    missing_docs,
          12  +
    rustdoc::missing_crate_level_docs,
          13  +
    missing_debug_implementations,
          14  +
    rust_2018_idioms,
          15  +
    unreachable_pub
          16  +
)]
          17  +
          18  +
/// Error types for CloudFront signing operations.
          19  +
pub mod error;
          20  +
mod key;
          21  +
mod policy;
          22  +
mod sign;
          23  +
          24  +
pub use key::PrivateKey;
          25  +
pub use sign::{SignedCookies, SignedUrl, SigningRequest, SigningRequestBuilder};
          26  +
          27  +
/// Sign a CloudFront URL with canned or custom policy
          28  +
pub fn sign_url(request: SigningRequest) -> Result<SignedUrl, error::SigningError> {
          29  +
    request.sign_url()
          30  +
}
          31  +
          32  +
/// Generate signed cookies with canned or custom policy
          33  +
pub fn sign_cookies(request: SigningRequest) -> Result<SignedCookies, error::SigningError> {
          34  +
    request.sign_cookies()
          35  +
}

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

@@ -0,1 +0,215 @@
           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::SigningError;
           7  +
use aws_smithy_types::{DateTime, Number};
           8  +
           9  +
#[derive(Debug, Clone)]
          10  +
pub(crate) struct Policy {
          11  +
    resource: String,
          12  +
    date_less_than: DateTime,
          13  +
    date_greater_than: Option<DateTime>,
          14  +
    ip_address: Option<String>,
          15  +
}
          16  +
          17  +
impl Policy {
          18  +
    pub(crate) fn builder() -> PolicyBuilder {
          19  +
        PolicyBuilder::default()
          20  +
    }
          21  +
          22  +
    pub(crate) fn to_json(&self) -> String {
          23  +
        let mut out = String::new();
          24  +
        let mut root = aws_smithy_json::serialize::JsonObjectWriter::new(&mut out);
          25  +
          26  +
        let mut statement_array = root.key("Statement").start_array();
          27  +
        let mut statement = statement_array.value().start_object();
          28  +
        statement.key("Resource").string(&self.resource);
          29  +
          30  +
        let mut condition = statement.key("Condition").start_object();
          31  +
          32  +
        let mut date_less = condition.key("DateLessThan").start_object();
          33  +
        date_less
          34  +
            .key("AWS:EpochTime")
          35  +
            .number(Number::PosInt(self.date_less_than.secs() as u64));
          36  +
        date_less.finish();
          37  +
          38  +
        if let Some(starts) = self.date_greater_than {
          39  +
            let mut date_greater = condition.key("DateGreaterThan").start_object();
          40  +
            date_greater
          41  +
                .key("AWS:EpochTime")
          42  +
                .number(Number::PosInt(starts.secs() as u64));
          43  +
            date_greater.finish();
          44  +
        }
          45  +
          46  +
        if let Some(ref ip) = self.ip_address {
          47  +
            let mut ip_addr = condition.key("IpAddress").start_object();
          48  +
            ip_addr.key("AWS:SourceIp").string(ip);
          49  +
            ip_addr.finish();
          50  +
        }
          51  +
          52  +
        condition.finish();
          53  +
        statement.finish();
          54  +
        statement_array.finish();
          55  +
        root.finish();
          56  +
          57  +
        out
          58  +
    }
          59  +
          60  +
    pub(crate) fn to_cloudfront_base64(&self) -> String {
          61  +
        let json = self.to_json();
          62  +
        base64_simd::STANDARD
          63  +
            .encode_to_string(json.as_bytes())
          64  +
            .replace('+', "-")
          65  +
            .replace('=', "_")
          66  +
            .replace('/', "~")
          67  +
    }
          68  +
}
          69  +
          70  +
#[derive(Default)]
          71  +
pub(crate) struct PolicyBuilder {
          72  +
    resource: Option<String>,
          73  +
    expires_at: Option<DateTime>,
          74  +
    starts_at: Option<DateTime>,
          75  +
    ip_range: Option<String>,
          76  +
}
          77  +
          78  +
impl PolicyBuilder {
          79  +
    pub(crate) fn resource(mut self, url: impl Into<String>) -> Self {
          80  +
        self.resource = Some(url.into());
          81  +
        self
          82  +
    }
          83  +
          84  +
    pub(crate) fn expires_at(mut self, time: DateTime) -> Self {
          85  +
        self.expires_at = Some(time);
          86  +
        self
          87  +
    }
          88  +
          89  +
    pub(crate) fn starts_at(mut self, time: DateTime) -> Self {
          90  +
        self.starts_at = Some(time);
          91  +
        self
          92  +
    }
          93  +
          94  +
    pub(crate) fn ip_range(mut self, cidr: impl Into<String>) -> Self {
          95  +
        self.ip_range = Some(cidr.into());
          96  +
        self
          97  +
    }
          98  +
          99  +
    pub(crate) fn build(self) -> Result<Policy, SigningError> {
         100  +
        let resource = self
         101  +
            .resource
         102  +
            .ok_or_else(|| SigningError::invalid_policy("resource is required"))?;
         103  +
         104  +
        let date_less_than = self
         105  +
            .expires_at
         106  +
            .ok_or_else(|| SigningError::invalid_policy("expires_at is required"))?;
         107  +
         108  +
        if let Some(starts) = self.starts_at {
         109  +
            if starts.secs() >= date_less_than.secs() {
         110  +
                return Err(SigningError::invalid_policy(
         111  +
                    "starts_at must be before expires_at",
         112  +
                ));
         113  +
            }
         114  +
        }
         115  +
         116  +
        Ok(Policy {
         117  +
            resource,
         118  +
            date_less_than,
         119  +
            date_greater_than: self.starts_at,
         120  +
            ip_address: self.ip_range,
         121  +
        })
         122  +
    }
         123  +
}
         124  +
         125  +
#[cfg(test)]
         126  +
mod tests {
         127  +
    use super::*;
         128  +
         129  +
    #[test]
         130  +
    fn test_canned_policy() {
         131  +
        let policy = Policy::builder()
         132  +
            .resource("https://d111111abcdef8.cloudfront.net/image.jpg")
         133  +
            .expires_at(DateTime::from_secs(1767290400))
         134  +
            .build()
         135  +
            .expect("valid canned policy");
         136  +
         137  +
        let json = policy.to_json();
         138  +
        assert!(json.contains("\"Resource\":\"https://d111111abcdef8.cloudfront.net/image.jpg\""));
         139  +
        assert!(json.contains("\"AWS:EpochTime\":1767290400"));
         140  +
        assert!(!json.contains("DateGreaterThan"));
         141  +
        assert!(!json.contains("IpAddress"));
         142  +
    }
         143  +
         144  +
    #[test]
         145  +
    fn test_custom_policy_with_starts_at() {
         146  +
        let policy = Policy::builder()
         147  +
            .resource("https://d111111abcdef8.cloudfront.net/*")
         148  +
            .expires_at(DateTime::from_secs(1767290400))
         149  +
            .starts_at(DateTime::from_secs(1767200000))
         150  +
            .build()
         151  +
            .expect("valid custom policy");
         152  +
         153  +
        let json = policy.to_json();
         154  +
        assert!(json.contains("DateGreaterThan"));
         155  +
        assert!(json.contains("\"AWS:EpochTime\":1767200000"));
         156  +
    }
         157  +
         158  +
    #[test]
         159  +
    fn test_custom_policy_with_ip_range() {
         160  +
        let policy = Policy::builder()
         161  +
            .resource("https://d111111abcdef8.cloudfront.net/video.mp4")
         162  +
            .expires_at(DateTime::from_secs(1767290400))
         163  +
            .ip_range("192.0.2.0/24")
         164  +
            .build()
         165  +
            .expect("valid custom policy");
         166  +
         167  +
        let json = policy.to_json();
         168  +
        assert!(json.contains("IpAddress"));
         169  +
        assert!(json.contains("\"AWS:SourceIp\":\"192.0.2.0/24\""));
         170  +
    }
         171  +
         172  +
    #[test]
         173  +
    fn test_missing_resource() {
         174  +
        let result = Policy::builder()
         175  +
            .expires_at(DateTime::from_secs(1767290400))
         176  +
            .build();
         177  +
         178  +
        assert!(result.is_err());
         179  +
    }
         180  +
         181  +
    #[test]
         182  +
    fn test_missing_expires_at() {
         183  +
        let result = Policy::builder()
         184  +
            .resource("https://example.com/file.txt")
         185  +
            .build();
         186  +
         187  +
        assert!(result.is_err());
         188  +
    }
         189  +
         190  +
    #[test]
         191  +
    fn test_starts_at_after_expires_at() {
         192  +
        let result = Policy::builder()
         193  +
            .resource("https://example.com/file.txt")
         194  +
            .expires_at(DateTime::from_secs(1767200000))
         195  +
            .starts_at(DateTime::from_secs(1767290400))
         196  +
            .build();
         197  +
         198  +
        assert!(result.is_err());
         199  +
    }
         200  +
         201  +
    #[test]
         202  +
    fn test_cloudfront_base64_encoding() {
         203  +
        let policy = Policy::builder()
         204  +
            .resource("https://example.com/test")
         205  +
            .expires_at(DateTime::from_secs(1767290400))
         206  +
            .build()
         207  +
            .expect("valid policy");
         208  +
         209  +
        let encoded = policy.to_cloudfront_base64();
         210  +
        // CloudFront encoding uses ~ for / and _ for =
         211  +
        assert!(!encoded.contains('+'));
         212  +
        assert!(!encoded.contains('/'));
         213  +
        assert!(!encoded.contains('='));
         214  +
    }
         215  +
}