Skip to content

Commit c36062e

Browse files
committed
Add documentation to a number of elements
1 parent bbd2f08 commit c36062e

File tree

5 files changed

+195
-21
lines changed

5 files changed

+195
-21
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ tower = { version = "^0.4", features = [ "util" ] }
3737
[dev-dependencies]
3838
env_logger = "^0.11"
3939
tokio = { version = "^1.38", features = [ "macros", "rt" ] }
40+
tokio-test = "^0.4"
4041
test-log = "0.2"

src/canonical.rs

+23-3
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,14 @@ pub struct SignedHeaderRequirements {
632632
}
633633

634634
impl SignedHeaderRequirements {
635+
/// Create a new `SignedHeaderRequirements` structure from the provided data.
636+
///
637+
/// # Parameters
638+
/// * `always_present`: Headers in addition to the standard AWS SigV4 headers that must be
639+
/// present and signed.
640+
/// * `if_in_request`: Headers that must be signed if they are present in the request.
641+
/// * `prefixes`: Prefixes that must be signed if any headers with that prefix are present in
642+
/// the request.
635643
pub fn new(mut always_present: Vec<String>, mut if_in_request: Vec<String>, mut prefixes: Vec<String>) -> Self {
636644
always_present.sort();
637645
if_in_request.sort();
@@ -644,47 +652,59 @@ impl SignedHeaderRequirements {
644652
}
645653
}
646654

647-
#[inline]
655+
/// Return the headers that must always be present in SignedHeaders.
656+
#[inline(always)]
648657
pub fn always_present(&self) -> &[String] {
649658
&self.always_present
650659
}
651660

652-
#[inline]
661+
/// Return the headers that must be present in SignedHeaders if they are present in the request.
662+
#[inline(always)]
653663
pub fn if_in_request(&self) -> &[String] {
654664
&self.if_in_request
655665
}
656666

657-
#[inline]
667+
/// Return the prefixes that must be present in SignedHeaders if any headers with that prefix.
668+
#[inline(always)]
658669
pub fn prefixes(&self) -> &[String] {
659670
&self.prefixes
660671
}
661672

673+
/// Adds an additional header that must always be present in SignedHeaders.
662674
pub fn add_always_present(&mut self, header: &str) {
663675
self.always_present.push(header.to_string());
664676
self.always_present.sort();
665677
self.always_present.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
666678
}
667679

680+
/// Adds an additional header that must be present in SignedHeaders if it is present in the
681+
/// request.
668682
pub fn add_if_in_request(&mut self, header: &str) {
669683
self.if_in_request.push(header.to_string());
670684
self.if_in_request.sort();
671685
self.if_in_request.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
672686
}
673687

688+
/// Adds an additional prefix that must be present in SignedHeaders if any headers with that
689+
/// prefix are present in the request.
674690
pub fn add_prefix(&mut self, prefix: &str) {
675691
self.prefixes.push(prefix.to_string());
676692
self.prefixes.sort();
677693
self.prefixes.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
678694
}
679695

696+
/// Removes a header that must always be present in SignedHeaders.
680697
pub fn remove_always_present(&mut self, header: &str) {
681698
self.always_present.retain(|h| !h.eq_ignore_ascii_case(header));
682699
}
683700

701+
/// Removes a header that must be present in SignedHeaders if it is present in the request.
684702
pub fn remove_if_in_request(&mut self, header: &str) {
685703
self.if_in_request.retain(|h| !h.eq_ignore_ascii_case(header));
686704
}
687705

706+
/// Removes a prefix that must be present in SignedHeaders if any headers with that prefix are
707+
/// present in the request.
688708
pub fn remove_prefix(&mut self, prefix: &str) {
689709
self.prefixes.retain(|p| !p.eq_ignore_ascii_case(prefix));
690710
}

src/lib.rs

+113-15
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,124 @@
1-
#![warn(clippy::all)]
2-
3-
//! The `aws_sig_verify` crate provides AWS SigV4 _verification_ routines. This *is not* the library you want if you
4-
//! just want to call AWS services or other services that use AWS SigV4 signatures.
5-
//! [Rusoto](https://github.com/rusoto/rusoto) already has a library,
6-
//! [rusoto_signature](https://docs.rs/rusoto_signature/), that provides this functionality.
1+
//! AWS API request signatures verification routines.
2+
//!
3+
//! The `scratchstack_aws_signature` crate provides
4+
//! AWS [SigV4](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html)
5+
//! _validation_ routines. This *is not* the library you want if you just want to call AWS services
6+
//! or other services that use AWS SigV4 signatures. [Rusoto](https://github.com/rusoto/rusoto)
7+
//! already has a library, [rusoto_signature](https://docs.rs/rusoto_signature/), that provides
8+
//! this functionality.
9+
//!
10+
//! If you are attempting to perform AWS SigV4 verification using AWS-vended credentials, this
11+
//! library also ___will not work for you___. You need the caller's secret key (or a derivative),
12+
//! and AWS does not allow this for obvious reasons. Instead, you should be using [API Gateway with
13+
//! IAM authentication](https://docs.aws.amazon.com/apigateway/latest/developerguide/permissions.html).
14+
//!
15+
//! On the other hand, if you have your own ecosystem of AWS-like credentials and are developing
16+
//! mock-AWS services or other services that need to use AWS SigV4, this _might_ be the right
17+
//! crate for you.
18+
//!
19+
//! # Workflow
20+
//! This assumes you have a complete HTTP request (headers _and_ body) already. As a result, you may not be able to
21+
//! implement this as a middleware layer for a web server—those typically only provide the headers. Having the body is
22+
//! required for almost all modes of AWS SigV4.
23+
//!
24+
//! The typical workflow is:
25+
//! 1. Convert an HTTP `Request` object into a scratchstack `Request` object.
26+
//! 2. Create a `GetSigningKeyRequest` from this `Request`.
27+
//! 3. Call your service to obtain the principal and signing key for this request.
28+
//! 4. Verify the request using `sigv4_verify` or `sigv4_verify_at`.
29+
//!
30+
//! ## Example
31+
//! ```rust
32+
//! use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
33+
//! use http::Request;
34+
//! use scratchstack_aws_principal::{Principal, User};
35+
//! use scratchstack_aws_signature::{
36+
//! GetSigningKeyRequest, GetSigningKeyResponse, KSecretKey, SignatureOptions,
37+
//! SignedHeaderRequirements, service_for_signing_key_fn, sigv4_validate_request,
38+
//! };
39+
//! use tower::{BoxError, Service};
40+
//!
41+
//! const ACCESS_KEY: &str = "AKIAIOSFODNN7EXAMPLE";
42+
//! const SECRET_KEY: &str = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
43+
//! const ACCOUNT_ID: &str = "123456789012";
44+
//! const PARTITION: &str = "aws";
45+
//! const PATH: &str = "/engineering/";
46+
//! const REGION: &str = "us-east-1";
47+
//! const SERVICE: &str = "example";
48+
//! const USER_NAME: &str = "user";
49+
//! const USER_ID: &str = "AIDAQXZEAEXAMPLEUSER";
50+
//!
51+
//! // The date for which the signature calculation was made.
52+
//! #[allow(deprecated)]
53+
//! const TEST_TIMESTAMP: DateTime<Utc> = DateTime::<Utc>::from_naive_utc_and_offset(
54+
//! NaiveDateTime::new(
55+
//! NaiveDate::from_ymd(2021, 1, 1),
56+
//! NaiveTime::from_hms(0, 0, 0)),
57+
//! Utc
58+
//! );
59+
//!
60+
//! // This is a mock function that returns a static secret key converted into the requested type
61+
//! // of signing key. For actual use, you would call out to a database or other service to obtain
62+
//! // a signing key.
63+
//! async fn get_signing_key(
64+
//! request: GetSigningKeyRequest)
65+
//! -> Result<GetSigningKeyResponse, BoxError> {
66+
//! assert_eq!(request.access_key(), ACCESS_KEY);
67+
//! assert_eq!(request.region(), REGION);
68+
//! assert_eq!(request.service(), SERVICE);
69+
//! let user = User::new(PARTITION, ACCOUNT_ID, PATH, USER_NAME)?;
70+
//! let secret_key = KSecretKey::from_str(SECRET_KEY);
71+
//! let signing_key = secret_key.to_ksigning(request.request_date(), REGION, SERVICE);
72+
//! Ok(GetSigningKeyResponse::builder()
73+
//! .principal(user)
74+
//! .signing_key(signing_key)
75+
//! .build()?)
76+
//! }
77+
//!
78+
//! // Wrap `get_signing_key` in a `tower::Service`.
79+
//! let mut get_signing_key_service = service_for_signing_key_fn(get_signing_key);
80+
//!
81+
//! // Normally this would come from your web framework.
82+
//! let req = Request::get("https://example.com")
83+
//! .header("Host", "example.com")
84+
//! .header("X-Amz-Date", "20210101T000000Z")
85+
//! .header("Authorization", "AWS4-HMAC-SHA256 \
86+
//! Credential=AKIAIOSFODNN7EXAMPLE/20210101/us-east-1/example/aws4_request, \
87+
//! SignedHeaders=host;x-amz-date, \
88+
//! Signature=3ea4679d2ecf5a8293e1fb10298c82988f024a2e937e9b37876b34bb119da0bc")
89+
//! .body(())
90+
//! .unwrap();
91+
//!
92+
//! // The headers that _must_ be signed (beyond the default SigV4 headers) for this service.
93+
//! // In this case, we're not requiring any additional headers.
94+
//! let signed_headers = SignedHeaderRequirements::default();
795
//!
8-
//! If you are attempting to perform AWS SigV4 verification using AWS-vended credentials, this library also
9-
//! ___will not work for you___. You need the caller's secret key (or a derivative), and AWS does not allow this for
10-
//! obvious reasons. Instead, you should be using [API Gateway with IAM
11-
//! authentication](https://docs.aws.amazon.com/apigateway/latest/developerguide/permissions.html).
96+
//! // Signature options for the request. Defaults are typically used, except for S3.
97+
//! let signature_options = SignatureOptions::default();
1298
//!
13-
//! On the other hand, if you have your own ecosystem of AWS-like credentials and are developing mock-AWS services or
14-
//! just really like AWS SigV4 but can't run within AWS, this library _might_ be for you.
99+
//! # tokio_test::block_on(async {
100+
//! // Validate the request.
101+
//! let (parts, body, auth) = sigv4_validate_request(
102+
//! req, &REGION, &SERVICE, &mut get_signing_key_service, TEST_TIMESTAMP, &signed_headers,
103+
//! signature_options).await.unwrap();
15104
//!
16-
#![allow(clippy::all)]
105+
//! // The principal we expect to be associated with the request.
106+
//! let expected_principal: Principal = User::new(PARTITION, ACCOUNT_ID, PATH, USER_NAME)
107+
//! .unwrap()
108+
//! .into();
109+
//! assert_eq!(auth.principal(), &expected_principal);
110+
//! # });
111+
//! ```
112+
#![warn(missing_docs)]
113+
#![deny(rustdoc::broken_intra_doc_links)]
114+
#![warn(rustdoc::missing_crate_level_docs)]
17115

18116
mod auth;
19-
pub mod canonical;
117+
mod canonical;
20118
mod chronoutil;
21119
mod crypto;
22120
mod error;
23-
pub mod signature;
121+
mod signature;
24122
mod signing_key;
25123

26124
pub use {

src/signature.rs

+57-2
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,25 @@ pub struct SignatureOptions {
2323
}
2424

2525
impl SignatureOptions {
26-
#[inline]
27-
pub fn url_encode_form() -> Self {
26+
/// Create a `SignatureOptions` suitable for use with services that treat
27+
/// `application/x-www-form-urlencoded` bodies as part of the query string.
28+
///
29+
/// Some AWS services require this behavior. This typically happens when a query string is too
30+
/// long to fit in the URL, so a `GET` request is transformed into a `POST` request with the
31+
/// query string passed as an HTML form.
32+
///
33+
/// This sets `s3` to `false` and `url_encode_form` to `true`.
34+
pub const fn url_encode_form() -> Self {
2835
Self {
2936
s3: false,
3037
url_encode_form: true,
3138
}
3239
}
3340

41+
/// Create a `SignatureOptions` suitable for use with S3-type authentication.
42+
///
43+
/// This sets `s3` to `true` and `url_encode_form` to `false`, resulting in AWS SigV4S3-style
44+
/// canonicalization.
3445
pub const S3: Self = Self {
3546
s3: true,
3647
url_encode_form: false,
@@ -40,6 +51,34 @@ impl SignatureOptions {
4051
/// Default allowed timestamp mismatch in minutes.
4152
const ALLOWED_MISMATCH_MINUTES: i64 = 15;
4253

54+
/// Validate an AWS SigV4 request.
55+
///
56+
/// This takes in an HTTP [`Request`] along with other service-specific paramters. If the
57+
/// validation is successful (i.e. the request is properly signed with a known access key), this
58+
/// returns:
59+
/// * The request headers (as HTTP [`Parts`]).
60+
/// * The request body (as a [`Bytes`] object, which is empty if no body was provided).
61+
/// * The [response from the authenticator][SigV4AuthenticatorResponse], which contains the
62+
/// principal and other session data.
63+
///
64+
/// # Parameters
65+
/// * `request` - The HTTP [`Request`] to validate.
66+
/// * `region` - The AWS region in which the request is being made.
67+
/// * `service` - The AWS service to which the request is being made.
68+
/// * `get_signing_key` - A service that can provide the signing key for the request.
69+
/// * `server_timestamp` - The timestamp of the server when the request was received. Usually this
70+
/// is the current time, `Utc::now()`.
71+
/// * `required_headers` - The headers that are required to be signed in the request in addition to
72+
/// the default SigV4 headers. If none, use
73+
/// [`&SignedHeaderRequirements::default()`][SignedHeaderRequirements::default].
74+
/// * `options` - [`SignatureOptions`]` that affect the behavior of the signature validation. For
75+
/// most services, use `SignatureOptions::default()`.
76+
///
77+
/// # Errors
78+
/// This function returns a [`SignatureError`][crate::SignatureError] if the HTTP request is
79+
/// malformed or the request was not properly signed. The validation follows the
80+
/// [AWS Auth Error Ordering](https://github.com/dacut/scratchstack-aws-signature/blob/main/docs/AWS%20Auth%20Error%20Ordering.pdf)
81+
/// document.
4382
pub async fn sigv4_validate_request<B, S, F>(
4483
request: Request<B>,
4584
region: &str,
@@ -73,27 +112,43 @@ where
73112
Ok((parts, body, sigv4_response))
74113
}
75114

115+
/// A trait for converting various body types into a [`Bytes`] object.
116+
///
117+
/// This requires reading the entire body into memory.
76118
#[async_trait]
77119
pub trait IntoRequestBytes {
120+
/// Convert this object into a [`Bytes`] object.
78121
async fn into_request_bytes(self) -> Result<Bytes, BoxError>;
79122
}
80123

124+
/// Convert the unit type `()` into an empty [`Bytes`] object.
81125
#[async_trait]
82126
impl IntoRequestBytes for () {
127+
/// Convert the unit type `()` into an empty [`Bytes`] object.
128+
///
129+
/// This is infalliable.
83130
async fn into_request_bytes(self) -> Result<Bytes, BoxError> {
84131
Ok(Bytes::new())
85132
}
86133
}
87134

135+
/// Convert a `Vec<u8>` into a [`Bytes`] object.
88136
#[async_trait]
89137
impl IntoRequestBytes for Vec<u8> {
138+
/// Convert a `Vec<u8>` into a [`Bytes`] object.
139+
///
140+
/// This is infalliable.
90141
async fn into_request_bytes(self) -> Result<Bytes, BoxError> {
91142
Ok(Bytes::from(self))
92143
}
93144
}
94145

146+
/// Identity transformation: return the [`Bytes`] object as-is.
95147
#[async_trait]
96148
impl IntoRequestBytes for Bytes {
149+
/// Identity transformation: return the [`Bytes`] object as-is.
150+
///
151+
/// This is infalliable.
97152
async fn into_request_bytes(self) -> Result<Bytes, BoxError> {
98153
Ok(self)
99154
}

src/signing_key.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub struct KServiceKey {
4545
/// The `kSigning` key: an AWS `kService` key, HMAC-SHA256 hashed with the "aws4_request" string.
4646
#[derive(Clone, Copy, PartialEq, Eq)]
4747
pub struct KSigningKey {
48-
/// The raw key.
48+
/// The resulting raw signing key.
4949
key: [u8; SHA256_OUTPUT_LEN],
5050
}
5151

0 commit comments

Comments
 (0)