Skip to content

Commit

Permalink
Update presigner so host is set and content-hash is excluded from sig…
Browse files Browse the repository at this point in the history
…ned-headers (#4560)

* Update presigner so host is set and content-hash is excluded

* Address comments

* Rebase on latest changes
  • Loading branch information
haydenbaker committed Oct 10, 2023
1 parent d2a0473 commit a00ee8a
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
package software.amazon.awssdk.http.auth.aws.internal.signer;

import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.AWS4_SIGNING_ALGORITHM;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_CONTENT_SHA256;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerUtils.addHostHeader;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerUtils.deriveSigningKey;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerUtils.hashCanonicalRequest;

Expand All @@ -42,17 +40,15 @@ public final class DefaultV4RequestSigner implements V4RequestSigner {
private static final Logger LOG = Logger.loggerFor(DefaultV4RequestSigner.class);

private final V4Properties properties;
private final String contentHash;

public DefaultV4RequestSigner(V4Properties properties) {
public DefaultV4RequestSigner(V4Properties properties, String contentHash) {
this.properties = properties;
this.contentHash = contentHash;
}

@Override
public V4RequestSigningResult sign(SdkHttpRequest.Builder requestBuilder) {
// Step 0: Pre-requisites
String contentHash = getContentHash(requestBuilder);
addHostHeader(requestBuilder);

// Step 1: Create a canonical request
V4CanonicalRequest canonicalRequest = createCanonicalRequest(requestBuilder.build(), contentHash);

Expand All @@ -71,12 +67,6 @@ public V4RequestSigningResult sign(SdkHttpRequest.Builder requestBuilder) {
return new V4RequestSigningResult(contentHash, signingKey, signature, canonicalRequest, requestBuilder);
}

private String getContentHash(SdkHttpRequest.Builder requestBuilder) {
return requestBuilder.firstMatchingHeader(X_AMZ_CONTENT_SHA256).orElseThrow(
() -> new IllegalArgumentException("Content hash must be present in the '" + X_AMZ_CONTENT_SHA256 + "' header!")
);
}

private V4CanonicalRequest createCanonicalRequest(SdkHttpRequest request, String contentHash) {
return new V4CanonicalRequest(request, contentHash, new V4CanonicalRequest.Options(
properties.shouldDoubleUrlEncode(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
import static software.amazon.awssdk.http.auth.aws.internal.signer.V4CanonicalRequest.getCanonicalHeaders;
import static software.amazon.awssdk.http.auth.aws.internal.signer.V4CanonicalRequest.getSignedHeadersString;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.AWS4_SIGNING_ALGORITHM;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_CONTENT_SHA256;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerUtils.addDateHeader;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerUtils.addHostHeader;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerUtils.formatDateTime;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerUtils.getContentHash;

import java.time.Duration;
import java.util.List;
Expand All @@ -38,8 +41,8 @@ public interface V4RequestSigner {
/**
* Retrieve an implementation of a V4RequestSigner, which signs the request, but does not add authentication to the request.
*/
static V4RequestSigner create(V4Properties properties) {
return new DefaultV4RequestSigner(properties);
static V4RequestSigner create(V4Properties properties, String contentHash) {
return new DefaultV4RequestSigner(properties, contentHash);
}

/**
Expand All @@ -52,9 +55,10 @@ static V4RequestSigner header(V4Properties properties) {
requestBuilder.putHeader(SignerConstant.X_AMZ_SECURITY_TOKEN,
((AwsSessionCredentialsIdentity) properties.getCredentials()).sessionToken());
}
addHostHeader(requestBuilder);
addDateHeader(requestBuilder, formatDateTime(properties.getCredentialScope().getInstant()));

V4RequestSigningResult result = create(properties).sign(requestBuilder);
V4RequestSigningResult result = create(properties, getContentHash(requestBuilder)).sign(requestBuilder);

// Add the signature within an authorization header
String authHeader = AWS4_SIGNING_ALGORITHM
Expand All @@ -77,6 +81,8 @@ static V4RequestSigner query(V4Properties properties) {
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_SECURITY_TOKEN,
((AwsSessionCredentialsIdentity) properties.getCredentials()).sessionToken());
}
// We have to add the host-header here explicitly, since query-signed request requires it in the signed-header param
addHostHeader(requestBuilder);

List<Pair<String, List<String>>> canonicalHeaders = getCanonicalHeaders(requestBuilder.build());
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_ALGORITHM, AWS4_SIGNING_ALGORITHM);
Expand All @@ -85,7 +91,7 @@ static V4RequestSigner query(V4Properties properties) {
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_CREDENTIAL,
properties.getCredentialScope().scope(properties.getCredentials()));

V4RequestSigningResult result = create(properties).sign(requestBuilder);
V4RequestSigningResult result = create(properties, getContentHash(requestBuilder)).sign(requestBuilder);

// Add the signature
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_SIGNATURE, result.getSignature());
Expand All @@ -100,11 +106,33 @@ static V4RequestSigner query(V4Properties properties) {
*/
static V4RequestSigner presigned(V4Properties properties, Duration expirationDuration) {
return requestBuilder -> {
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_EXPIRES,
Long.toString(expirationDuration.getSeconds())
);
// Add pre-requisites
if (properties.getCredentials() instanceof AwsSessionCredentialsIdentity) {
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_SECURITY_TOKEN,
((AwsSessionCredentialsIdentity) properties.getCredentials()).sessionToken());
}
// We have to add the host-header here explicitly, since pre-signed request requires it in the signed-header param
addHostHeader(requestBuilder);

// Pre-signed requests shouldn't have the content-hash header
String contentHash = getContentHash(requestBuilder);
requestBuilder.removeHeader(X_AMZ_CONTENT_SHA256);

List<Pair<String, List<String>>> canonicalHeaders = getCanonicalHeaders(requestBuilder.build());
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_ALGORITHM, AWS4_SIGNING_ALGORITHM);
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_DATE, properties.getCredentialScope().getDatetime());
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_SIGNED_HEADERS, getSignedHeadersString(canonicalHeaders));
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_CREDENTIAL,
properties.getCredentialScope().scope(properties.getCredentials()));
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_EXPIRES, Long.toString(expirationDuration.getSeconds()));

V4RequestSigningResult result = create(properties, contentHash).sign(requestBuilder);

// Add the signature
requestBuilder.putRawQueryParameter(SignerConstant.X_AMZ_SIGNATURE, result.getSignature());

return result;

return query(properties).sign(requestBuilder);
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.awssdk.http.auth.aws.internal.signer.util;

import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_CONTENT_SHA256;
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_DECODED_CONTENT_LENGTH;

import java.io.ByteArrayInputStream;
Expand Down Expand Up @@ -275,4 +276,10 @@ private static int readAll(InputStream inputStream) {
throw new RuntimeException("Could not finish reading stream: ", e);
}
}

public static String getContentHash(SdkHttpRequest.Builder requestBuilder) {
return requestBuilder.firstMatchingHeader(X_AMZ_CONTENT_SHA256).orElseThrow(
() -> new IllegalArgumentException("Content hash must be present in the '" + X_AMZ_CONTENT_SHA256 + "' header!")
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package software.amazon.awssdk.http.auth.aws.internal.signer;

import static java.time.ZoneOffset.UTC;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static software.amazon.awssdk.utils.BinaryUtils.toHex;

Expand All @@ -37,21 +38,21 @@ public class DefaultRequestSignerTest {
.normalizePath(true)
.build();

V4RequestSigner requestSigner = new DefaultV4RequestSigner(v4Properties);
DefaultV4RequestSigner requestSigner = new DefaultV4RequestSigner(v4Properties, "quux");

@Test
public void requestSigner_sign_shouldReturnSignedResult_butNotAddAnyAuthInfoToRequest() {
SdkHttpRequest.Builder request = SdkHttpRequest
.builder()
.uri(URI.create("https://localhost"))
.method(SdkHttpMethod.GET)
.putHeader("x-amz-content-sha256", "quux");
.putHeader("Host", "localhost");
String expectedContentHash = "quux";
String expectedSigningKeyHex = "3d558b7a87b67996abc908071e0771a31b2a7977ab247144e60a6cba3356be1f";
String expectedSignature = "7557839280ea0ef5c4acc66e5670d0857ac4f491884b1b8031d4dea2fc33483c";
String expectedSignature = "6c1f4222e0888e6e68b20ded382bc80c7312465c69fb52cbd6d6ce2d073533bf";
String expectedCanonicalRequestString = "GET\n/\n\n"
+ "host:localhost\nx-amz-content-sha256:quux\n\n"
+ "host;x-amz-content-sha256\nquux";
+ "host:localhost\n\n"
+ "host\nquux";
String expectedHost = "localhost";

V4RequestSigningResult requestSigningResult = requestSigner.sign(request);
Expand All @@ -61,5 +62,6 @@ public void requestSigner_sign_shouldReturnSignedResult_butNotAddAnyAuthInfoToRe
assertEquals(expectedSignature, requestSigningResult.getSignature());
assertEquals(expectedCanonicalRequestString, requestSigningResult.getCanonicalRequest().getCanonicalRequestString());
assertEquals(expectedHost, requestSigningResult.getSignedRequest().firstMatchingHeader("Host").orElse(""));
assertThat(requestSigningResult.getSignedRequest().build()).usingRecursiveComparison().isEqualTo(request.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static software.amazon.awssdk.http.auth.aws.TestUtils.TickingClock;
import static software.amazon.awssdk.http.auth.aws.internal.signer.V4RequestSigner.header;
import static software.amazon.awssdk.http.auth.aws.internal.signer.V4RequestSigner.presigned;
import static software.amazon.awssdk.http.auth.aws.internal.signer.V4RequestSigner.query;

import java.net.URI;
import java.time.Clock;
Expand All @@ -39,20 +42,28 @@ public class V4RequestSignerTest {
AwsSessionCredentialsIdentity.create("access", "secret", "token");

@Test
public void sign_computesSignatureAndAddsHostHeader() {
String expectedSignature = "9f7da47c7fe7989712658509580c725430af16a2dccb6bf38b3506bd9606642e";
V4RequestSigningResult result = V4RequestSigner.create(getProperties(creds)).sign(getRequest());
public void sign_computesSigningResult() {
String expectedSignature = "4c7049543386ba32bc85e7a7d7b892e7da1c412abf3508ca84775ed099790acf";
String expectedContentHash = "abc123";
String expectedCanonicalRequestString = "GET\n"
+ "/./foo\n\n"
+ "x-amz-archive-description:test test\n"
+ "x-amz-content-sha256:checksum\n\n"
+ "x-amz-archive-description;x-amz-content-sha256\n"
+ "abc123";
V4RequestSigningResult result = V4RequestSigner.create(getProperties(creds), "abc123").sign(getRequest());

assertEquals(expectedSignature, result.getSignature());
assertThat(result.getSignedRequest().firstMatchingHeader("Host")).hasValue("test.com");
assertEquals(expectedContentHash, result.getContentHash());
assertEquals(expectedCanonicalRequestString, result.getCanonicalRequest().getCanonicalRequestString());
}

@Test
public void sign_withHeader_addsAuthHeaders() {
String expectedAuthorization = "AWS4-HMAC-SHA256 Credential=access/19700101/us-east-1/demo/aws4_request, " +
"SignedHeaders=host;x-amz-archive-description;x-amz-content-sha256;x-amz-date, " +
"Signature=0fafd04465eb6201e868a80f72d15d50731512298f554684ce6627c0619f429a";
V4RequestSigningResult result = V4RequestSigner.header(getProperties(creds)).sign(getRequest());
V4RequestSigningResult result = header(getProperties(creds)).sign(getRequest());

assertThat(result.getSignedRequest().firstMatchingHeader("X-Amz-Date")).hasValue("19700101T000000Z");
assertThat(result.getSignedRequest().firstMatchingHeader("Authorization")).hasValue(expectedAuthorization);
Expand All @@ -64,7 +75,7 @@ public void sign_withHeaderAndSessionCredentials_addsAuthHeadersAndTokenHeader()
"SignedHeaders=host;x-amz-archive-description;x-amz-content-sha256;x-amz-date;"
+ "x-amz-security-token, " +
"Signature=cda79272f6d258c2cb2f04ac84a5f9515440e0158bf39e212c3dcf88b3a477a9";
V4RequestSigningResult result = V4RequestSigner.header(getProperties(sessionCreds)).sign(getRequest());
V4RequestSigningResult result = header(getProperties(sessionCreds)).sign(getRequest());

assertThat(result.getSignedRequest().firstMatchingHeader("X-Amz-Date")).hasValue("19700101T000000Z");
assertThat(result.getSignedRequest().firstMatchingHeader("Authorization")).hasValue(expectedAuthorization);
Expand All @@ -73,48 +84,59 @@ public void sign_withHeaderAndSessionCredentials_addsAuthHeadersAndTokenHeader()

@Test
public void sign_withQuery_addsAuthQueryParams() {
V4RequestSigningResult result = V4RequestSigner.query(getProperties(creds)).sign(getRequest());
V4RequestSigningResult result = query(getProperties(creds)).sign(getRequest());

assertEquals("AWS4-HMAC-SHA256", result.getSignedRequest().rawQueryParameters().get("X-Amz-Algorithm").get(0));
assertEquals("19700101T000000Z", result.getSignedRequest().rawQueryParameters().get("X-Amz-Date").get(0));
assertEquals("x-amz-archive-description;x-amz-content-sha256",
assertEquals("host;x-amz-archive-description;x-amz-content-sha256",
result.getSignedRequest().rawQueryParameters().get("X-Amz-SignedHeaders").get(0));
assertEquals("access/19700101/us-east-1/demo/aws4_request", result.getSignedRequest().rawQueryParameters().get(
"X-Amz-Credential").get(0));
assertEquals("448f105ad26c5adfdf07b482b0f46ff294032c7bc72e10bb944e2f45929bf468",
assertEquals("bb3ddb98bc32b85c8aa484bfaf321171a22ad802baa03ee9d5fcda9842b769c9",
result.getSignedRequest().rawQueryParameters().get("X-Amz-Signature").get(0));
}

@Test
public void sign_withQueryAndSessionCredentials_addsAuthQueryParamsAndTokenParam() {
V4RequestSigningResult result = V4RequestSigner.query(getProperties(sessionCreds)).sign(getRequest());
V4RequestSigningResult result = query(getProperties(sessionCreds)).sign(getRequest());

assertEquals("AWS4-HMAC-SHA256", result.getSignedRequest().rawQueryParameters().get("X-Amz-Algorithm").get(0));
assertEquals("19700101T000000Z", result.getSignedRequest().rawQueryParameters().get("X-Amz-Date").get(0));
assertEquals("x-amz-archive-description;x-amz-content-sha256",
assertEquals("host;x-amz-archive-description;x-amz-content-sha256",
result.getSignedRequest().rawQueryParameters().get("X-Amz-SignedHeaders").get(0));
assertEquals(
"access/19700101/us-east-1/demo/aws4_request",
result.getSignedRequest().rawQueryParameters().get("X-Amz-Credential").get(0));
assertEquals("f8a7d5ed62c2095240dd847ea48ebb1f1471b3fccb8d8165f8c5dbd8c5a670da",
assertEquals("2ffe9562fefd57e14f43bf1937b6b85cc0f0180d63789254bddec25498e14a29",
result.getSignedRequest().rawQueryParameters().get("X-Amz-Signature").get(0));
assertEquals("token", result.getSignedRequest().rawQueryParameters().get("X-Amz-Security-Token").get(0));
}

@Test
public void sign_withPresigned_addsExpirationParam() {
V4RequestSigningResult result = V4RequestSigner.presigned(getProperties(creds), Duration.ZERO).sign(getRequest());
V4RequestSigningResult result = presigned(getProperties(creds), Duration.ZERO).sign(getRequest());

assertEquals("0", result.getSignedRequest().rawQueryParameters().get("X-Amz-Expires").get(0));
assertEquals("45d00c9c0eb2c05cfe7e42231a572aef30d96d1afb2a205e27d9c6b89867a865", result.getSignature());
assertEquals("host;x-amz-archive-description",
result.getSignedRequest().rawQueryParameters().get("X-Amz-SignedHeaders").get(0));
assertEquals("691f39caa2064fe4fb897976dfb4b09df54749c825a5fcd1e2f0b3fcd1bcc600", result.getSignature());
}

@Test
public void sign_withHeader_withNoContentHashHeader_throws() {
SdkHttpRequest.Builder request = getRequest().removeHeader("x-amz-content-sha256");

assertThrows(IllegalArgumentException.class,
() -> header(getProperties(sessionCreds)).sign(request)
);
}

@Test
public void sign_withNoChecksumHeader_throws() {
public void sign_withQuery_withNoContentHashHeader_throws() {
SdkHttpRequest.Builder request = getRequest().removeHeader("x-amz-content-sha256");

assertThrows(IllegalArgumentException.class,
() -> V4RequestSigner.create(getProperties(sessionCreds)).sign(request)
() -> query(getProperties(sessionCreds)).sign(request)
);
}

Expand Down

0 comments on commit a00ee8a

Please sign in to comment.