This is an automated email from the ASF dual-hosted git repository. SaketaChalamchala pushed a commit to branch upstream-master in repository https://gitbox.apache.org/repos/asf/ozone.git
commit 680c578ca007df11db5531b9a347afb86283ad66 Author: SaketaChalamchala <[email protected]> AuthorDate: Fri Oct 2 12:13:41 2026 -0700 S3 header auth expiry in 15min and correct date header validation. --- .../hadoop/ozone/s3/exception/S3ErrorTable.java | 9 ++ .../ozone/s3/signature/AWSSignatureProcessor.java | 3 +- .../s3/signature/AuthorizationV4HeaderParser.java | 43 +++++-- .../s3/signature/AuthorizationV4QueryParser.java | 7 +- .../hadoop/ozone/s3/signature/Credential.java | 20 ++++ .../s3/signature/MalformedResourceException.java | 19 ++- .../ozone/s3/signature/StringToSignProducer.java | 50 -------- .../hadoop/ozone/s3/TestAuthorizationFilter.java | 94 +++++++++------ .../signature/TestAuthorizationV4HeaderParser.java | 133 ++++++++++++--------- .../signature/TestAuthorizationV4QueryParser.java | 55 ++++++--- .../s3/signature/TestStringToSignProducer.java | 56 +++++---- 11 files changed, 290 insertions(+), 199 deletions(-) diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/exception/S3ErrorTable.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/exception/S3ErrorTable.java index ac52cbd851e..bb03196caab 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/exception/S3ErrorTable.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/exception/S3ErrorTable.java @@ -66,6 +66,11 @@ public enum S3ErrorTable { "AuthorizationHeaderMalformed", "The authorization header you provided " + "is invalid.", HTTP_BAD_REQUEST), + MALFORMED_CREDENTIAL_DATE( + "AuthorizationHeaderMalformed", "The authorization header is malformed; " + + "Invalid credential date. Date is not the same as X-Amz-Date.", + HTTP_BAD_REQUEST), + NO_SUCH_KEY( "NoSuchKey", "The specified key does not exist", HTTP_NOT_FOUND), @@ -111,6 +116,10 @@ public enum S3ErrorTable { "AccessDenied", "User doesn't have the right to access this " + "resource.", HTTP_FORBIDDEN), + REQUEST_TIME_TOO_SKEWED( + "RequestTimeTooSkewed", "The difference between the request time and " + + "the server's time is too large.", HTTP_FORBIDDEN), + SIGNATURE_DOES_NOT_MATCH( "SignatureDoesNotMatch", "The request signature we calculated does not " + "match the signature you provided. Check your key and signing method.", diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AWSSignatureProcessor.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AWSSignatureProcessor.java index 12ed0266f41..3ec4a43074c 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AWSSignatureProcessor.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AWSSignatureProcessor.java @@ -101,7 +101,8 @@ public SignatureInfo parseSignature() throws OS3Exception, IOException, NoSuchAl throw S3ErrorTable.newError(ACCESS_DENIED, e.getResource()); } catch (MalformedResourceException e) { AUDIT.logAuthFailure(buildAuthFailureMessage(e)); - throw S3ErrorTable.newError(MALFORMED_HEADER, e.getResource()); + S3ErrorTable error = e.getErrorCode() != null ? e.getErrorCode() : MALFORMED_HEADER; + throw S3ErrorTable.newError(error, e.getResource()); } if (signatureInfo != null) { break; diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AuthorizationV4HeaderParser.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AuthorizationV4HeaderParser.java index ed4ef4b13dd..6382eeae56a 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AuthorizationV4HeaderParser.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AuthorizationV4HeaderParser.java @@ -17,14 +17,17 @@ package org.apache.hadoop.ozone.s3.signature; -import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.SECONDS; import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isNotEmpty; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.REQUEST_TIME_TOO_SKEWED; import static org.apache.hadoop.ozone.s3.signature.SignatureProcessor.AWS4_SIGNING_ALGORITHM; import static org.apache.hadoop.ozone.s3.signature.SignatureProcessor.DATE_FORMATTER; import com.google.common.annotations.VisibleForTesting; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.time.format.DateTimeParseException; import java.util.Collection; import org.apache.commons.codec.DecoderException; @@ -37,6 +40,9 @@ */ public class AuthorizationV4HeaderParser implements SignatureParser { + /** Max allowed clock skew for SigV4 authorization-header requests (15 minutes). */ + private static final long SIGV4_MAX_REQUEST_SKEW_SECONDS = 15 * 60; + private static final String CREDENTIAL = "Credential="; private static final String SIGNEDHEADERS = "SignedHeaders="; private static final String SIGNATURE = "Signature="; @@ -65,6 +71,7 @@ public SignatureInfo parseSignature() throws MalformedResourceException { if (authHeader == null || !authHeader.startsWith("AWS4")) { return null; } + validateDateHeader(); int firstSep = authHeader.indexOf(' '); if (firstSep < 0) { throw new MalformedResourceException(authHeader); @@ -175,15 +182,16 @@ private Credential parseCredentials(String credential) "AWS service is empty. credential:" + credential, authHeader); } - // Date should not be empty and within valid range. + // Date should not be empty and must match X-Amz-Date. if (!credentialObj.getDate().isEmpty()) { try { - validateDateRange(credentialObj); + LocalDate.parse(credentialObj.getDate(), DATE_FORMATTER); + Credential.validateScopeDateMatchesAmzDate(credentialObj.getDate(), dateHeader, + authHeader); } catch (DateTimeParseException ex) { throw new MalformedResourceException( "AWS date format is invalid. credential:" + credential, authHeader); } - } else { throw new MalformedResourceException( "AWS date is empty. credential:{}" + credential, authHeader); @@ -191,17 +199,26 @@ private Credential parseCredentials(String credential) return credentialObj; } + /** + * Validates {@code X-Amz-Date} clock skew for authorization-header SigV4. + * AWS SigV4 clients send this timestamp in UTC. + */ @VisibleForTesting - public void validateDateRange(Credential credentialObj) - throws MalformedResourceException, DateTimeParseException { - LocalDate date = LocalDate.parse(credentialObj.getDate(), DATE_FORMATTER); - LocalDate now = LocalDate.now(); - if (date.isBefore(now.minus(1, DAYS)) || - date.isAfter(now.plus(1, DAYS))) { + void validateDateHeader() throws MalformedResourceException { + if (dateHeader == null || dateHeader.isEmpty()) { + throw new MalformedResourceException("Missing X-Amz-Date header.", authHeader); + } + try { + LocalDateTime date = LocalDateTime.parse(dateHeader, + StringToSignProducer.TIME_FORMATTER); + LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); + if (date.isBefore(now.minus(SIGV4_MAX_REQUEST_SKEW_SECONDS, SECONDS)) + || date.isAfter(now.plus(SIGV4_MAX_REQUEST_SKEW_SECONDS, SECONDS))) { + throw new MalformedResourceException(REQUEST_TIME_TOO_SKEWED, dateHeader); + } + } catch (DateTimeParseException ex) { throw new MalformedResourceException( - "AWS date not in valid range. Date: " + date + " should not be older " - + "than 1 day(i.e yesterday) and " - + "greater than 1 day(i.e tomorrow).", authHeader); + "Invalid X-Amz-Date format: " + dateHeader, authHeader); } } diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AuthorizationV4QueryParser.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AuthorizationV4QueryParser.java index 3dc6d3218d1..89b4b659252 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AuthorizationV4QueryParser.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/AuthorizationV4QueryParser.java @@ -26,6 +26,7 @@ import com.google.common.annotations.VisibleForTesting; import java.io.UnsupportedEncodingException; import java.time.LocalDate; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; import java.util.Map; @@ -136,7 +137,7 @@ protected void validateDateAndExpires() final Long expires = Long.parseLong(expiresString); if (expires >= X_AMZ_EXPIRES_MIN && expires <= X_AMZ_EXPIRES_MAX) { if (ZonedDateTime.parse(dateString, StringToSignProducer.TIME_FORMATTER) - .plus(expires, SECONDS).isBefore(ZonedDateTime.now())) { + .plus(expires, SECONDS).isBefore(ZonedDateTime.now(ZoneOffset.UTC))) { // An expired pre-signed URL is an authorization failure (403), not a // malformed header (400). throw new AccessDeniedResourceException("Pre-signed S3 url is expired. " @@ -170,10 +171,12 @@ private void validateCredential(Credential credential) throw new MalformedResourceException( "AWS service is empty. credential:" + credential); } - // Date should not be empty and should be properly formatted. + // Date should not be empty, properly formatted, and match X-Amz-Date. if (!credential.getDate().isEmpty()) { try { LocalDate.parse(credential.getDate(), DATE_FORMATTER); + Credential.validateScopeDateMatchesAmzDate(credential.getDate(), + queryParameters.get("X-Amz-Date"), credential.getCredential()); } catch (DateTimeParseException ex) { throw new MalformedResourceException( "AWS date format is invalid. credential:" + credential); diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/Credential.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/Credential.java index fab1718d93e..ee7c9577f79 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/Credential.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/Credential.java @@ -17,6 +17,8 @@ package org.apache.hadoop.ozone.s3.signature; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.MALFORMED_CREDENTIAL_DATE; + /** * Credential in the AWS authorization header. * Ref: https://docs.aws.amazon.com/AmazonS3/latest/API/ @@ -105,4 +107,22 @@ public String createScope() { getAwsRegion(), getAwsService(), getAwsRequest()); } + + /** + * Validates that credential scope date matches the date portion of + * {@code X-Amz-Date}. AWS SigV4 requires zero tolerance between these values. + */ + static void validateScopeDateMatchesAmzDate(String credentialDate, + String amzDate, String resource) throws MalformedResourceException { + if (amzDate == null || amzDate.isEmpty()) { + throw new MalformedResourceException("Missing X-Amz-Date.", resource); + } + if (amzDate.length() < 8) { + throw new MalformedResourceException( + "Invalid X-Amz-Date format: " + amzDate, resource); + } + if (!credentialDate.equals(amzDate.substring(0, 8))) { + throw new MalformedResourceException(MALFORMED_CREDENTIAL_DATE, resource); + } + } } diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/MalformedResourceException.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/MalformedResourceException.java index 0d2a42a7759..b300be3030d 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/MalformedResourceException.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/MalformedResourceException.java @@ -17,22 +17,39 @@ package org.apache.hadoop.ozone.s3.signature; +import org.apache.hadoop.ozone.s3.exception.S3ErrorTable; + /** * This exception is used to communicate validation errors when parsing * signatures. */ public class MalformedResourceException extends Exception { + private final S3ErrorTable errorCode; private final String resource; public MalformedResourceException(String resource) { - this.resource = resource; + this(null, null, resource); } public MalformedResourceException(String message, String resource) { + this(null, message, resource); + } + + public MalformedResourceException(S3ErrorTable errorCode, String resource) { + this(errorCode, null, resource); + } + + private MalformedResourceException(S3ErrorTable errorCode, String message, + String resource) { super(message); + this.errorCode = errorCode; this.resource = resource; } + public S3ErrorTable getErrorCode() { + return errorCode; + } + public String getResource() { return resource; } diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/StringToSignProducer.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/StringToSignProducer.java index 69cc088e184..793669beda8 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/StringToSignProducer.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/signature/StringToSignProducer.java @@ -17,7 +17,6 @@ package org.apache.hadoop.ozone.s3.signature; -import static java.time.temporal.ChronoUnit.SECONDS; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.S3_AUTHINFO_CREATION_ERROR; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.newError; import static org.apache.hadoop.ozone.s3.util.S3Consts.X_AMZ_CONTENT_SHA256; @@ -28,10 +27,8 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -64,11 +61,6 @@ public final class StringToSignProducer { private static final String[] URL_ENCODE_SEARCH_CHARS = new String[] {"+", "*", "%7E"}; private static final String[] URL_ENCODE_REPLACE_CHARS = new String[] {"%20", "%2A", "~"}; - /** - * Seconds in a week, which is the max expiration time Sig-v4 accepts. - */ - private static final long PRESIGN_URL_MAX_EXPIRATION_SECONDS = - 60 * 60 * 24 * 7; public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'") .withZone(ZoneOffset.UTC); @@ -81,7 +73,6 @@ public static String createSignatureBase( ContainerRequestContext context ) throws Exception { return createSignatureBase(signatureInfo, - context.getUriInfo().getRequestUri().getScheme(), context.getMethod(), LowerCaseKeyStringMap.fromHeaderMap(context.getHeaders()), fromMultiValueToSingleValueMap( @@ -91,7 +82,6 @@ public static String createSignatureBase( @VisibleForTesting public static String createSignatureBase( SignatureInfo signatureInfo, - String scheme, String method, LowerCaseKeyStringMap headers, Map<String, String> queryParams @@ -123,7 +113,6 @@ public static String createSignatureBase( .append(credentialScope).append(NEWLINE); String canonicalRequest = buildCanonicalRequest( - scheme, method, uri, signatureInfo.getSignedHeaders(), @@ -157,7 +146,6 @@ public static String hash(String payload) throws NoSuchAlgorithmException { @VisibleForTesting public static String buildCanonicalRequest( - String schema, String method, String uri, String signedHeaders, @@ -187,15 +175,6 @@ public static String buildCanonicalRequest( } canonicalHeaders.append(headerValue) .append(NEWLINE); - - // Set for testing purpose only to skip date and host validation. - try { - validateSignedHeader(schema, header, headerValue); - } catch (DateTimeParseException ex) { - LOG.error("DateTime format invalid.", ex); - throw newError(S3_AUTHINFO_CREATION_ERROR); - } - } else { LOG.error("Header " + header + " not present in " + "request but requested to be signed."); @@ -283,35 +262,6 @@ private static String getQueryParamString( return result.toString(); } - @VisibleForTesting - static void validateSignedHeader( - String schema, - String header, - String headerValue - ) throws OS3Exception, DateTimeParseException { - switch (header) { - case HOST: - break; - case X_AMAZ_DATE: - LocalDateTime date = LocalDateTime.parse(headerValue, TIME_FORMATTER); - LocalDateTime now = LocalDateTime.now(); - if (date.isBefore(now.minus(PRESIGN_URL_MAX_EXPIRATION_SECONDS, SECONDS)) - || date.isAfter(now.plus(PRESIGN_URL_MAX_EXPIRATION_SECONDS, - SECONDS))) { - LOG.error("AWS date not in valid range. Request timestamp:{} should " - + "not be older than {} seconds.", - headerValue, PRESIGN_URL_MAX_EXPIRATION_SECONDS); - throw newError(S3_AUTHINFO_CREATION_ERROR); - } - break; - case X_AMZ_CONTENT_SHA256: - // Validate x-amz-content-sha256 during upload, before committing the key. - break; - default: - break; - } - } - /** * According to AWS Sig V4 documentation: * https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestAuthorizationFilter.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestAuthorizationFilter.java index cc9dd813fb9..89d7e4a16de 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestAuthorizationFilter.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestAuthorizationFilter.java @@ -19,8 +19,9 @@ import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; -import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.MALFORMED_HEADER; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.MALFORMED_CREDENTIAL_DATE; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.PAYLOAD_TOO_LARGE; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.REQUEST_TIME_TOO_SKEWED; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.S3_AUTHINFO_CREATION_ERROR; import static org.apache.hadoop.ozone.s3.signature.AWSSignatureProcessor.DATE_FORMATTER; import static org.apache.hadoop.ozone.s3.signature.SignatureParser.AUTHORIZATION_HEADER; @@ -45,6 +46,7 @@ import java.security.MessageDigest; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.stream.Stream; import javax.ws.rs.WebApplicationException; import javax.ws.rs.container.ContainerRequestContext; @@ -67,48 +69,76 @@ public class TestAuthorizationFilter { private AuthorizationFilter authorizationFilter = new AuthorizationFilter(); private static final String DATETIME = StringToSignProducer.TIME_FORMATTER. - format(LocalDateTime.now()); + format(LocalDateTime.now(ZoneOffset.UTC)); - private static final String CURDATE = DATE_FORMATTER.format(LocalDate.now()); + private static final String CURDATE = + DATE_FORMATTER.format(LocalDate.now(ZoneOffset.UTC)); + private static final String MISMATCHED_CREDENTIAL_DATE = + DATE_FORMATTER.format(LocalDate.now(ZoneOffset.UTC).minusDays(1)); private static Stream<Arguments>testAuthFilterFailuresInput() { + String staleCredentialDate = "20190221"; + String staleDateTime = staleCredentialDate + "T002037Z"; return Stream.of( arguments( "GET", - "AWS4-HMAC-SHA256 Credential=testuser1/20190221/us-west-1/s3" + - "/aws4_request, SignedHeaders=content-md5;host;" + - "x-amz-content-sha256;x-amz-date, " + - "Signature" + - "=56ec73ba1974f8feda8365c3caef89c5d4a688d5f9baccf47" + - "65f46a14cd745ad", + "AWS4-HMAC-SHA256 Credential=testuser1/" + staleCredentialDate + + "/us-west-1/s3/aws4_request, SignedHeaders=content-md5;host;" + + "x-amz-content-sha256;x-amz-date, " + + "Signature" + + "=56ec73ba1974f8feda8365c3caef89c5d4a688d5f9baccf47" + + "65f46a14cd745ad", "Zi68x2nPDDXv5qfDC+ZWTg==", "s3g:9878", - "e2bd43f11c97cde3465e0e8d1aad77af7ec7aa2ed8e213cd0e24" + - "1e28375860c6", - "20190221T002037Z", + "e2bd43f11c97cde3465e0e8d1aad77af7ec7aa2ed8e213cd0e24" + + "1e28375860c6", + staleDateTime, "", "/", - MALFORMED_HEADER.getErrorMessage() + HTTP_FORBIDDEN, + REQUEST_TIME_TOO_SKEWED.getErrorMessage() ), arguments( "GET", - "AWS4-HMAC-SHA256 " + - "Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request," + - " SignedHeaders=content-type;host;x-amz-date, " + - "Signature=" + - "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400" + - "e06b5924a6f2b5d7", + "AWS4-HMAC-SHA256 " + + "Credential=AKIDEXAMPLE/" + staleCredentialDate + + "/us-east-1/iam/aws4_request," + + " SignedHeaders=content-type;host;x-amz-date, " + + "Signature=" + + "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400" + + "e06b5924a6f2b5d7", "", "iam.amazonaws.com", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "20150830T123600Z", + staleDateTime, "application/x-www-form-urlencoded; charset=utf-8", "", - MALFORMED_HEADER.getErrorMessage() + HTTP_FORBIDDEN, + REQUEST_TIME_TOO_SKEWED.getErrorMessage() + ), + arguments( + "GET", + "AWS4-HMAC-SHA256 Credential=testuser1/" + MISMATCHED_CREDENTIAL_DATE + + "/us-east-1/s3/aws4_request, SignedHeaders=content-md5;host;" + + "x-amz-content-sha256;x-amz-date, " + + "Signature" + + "=56ec73ba1974f8feda8365c3caef89c5d4a688d5f9baccf47" + + "65f46a14cd745ad", + "Zi68x2nPDDXv5qfDC+ZWTg==", + "s3g:9878", + "e2bd43f11c97cde3465e0e8d1aad77af7ec7aa2ed8e213cd0e24" + + "1e28375860c6", + DATETIME, + "", + "/", + HTTP_BAD_REQUEST, + MALFORMED_CREDENTIAL_DATE.getErrorMessage() ), arguments(null, null, null, null, null, null, null, null, + HTTP_FORBIDDEN, S3_AUTHINFO_CREATION_ERROR.getErrorMessage()), arguments(null, "", null, null, null, null, null, null, + HTTP_FORBIDDEN, S3_AUTHINFO_CREATION_ERROR.getErrorMessage()), // AWS V2 signature arguments( @@ -120,6 +150,7 @@ public class TestAuthorizationFilter { "Wed, 22 Mar 2023 17:00:06 +0000", "application/octet-stream", "/", + HTTP_FORBIDDEN, S3_AUTHINFO_CREATION_ERROR.getErrorMessage() ), // Too huge payload for signature V4 of STS request @@ -138,6 +169,7 @@ public class TestAuthorizationFilter { DATETIME, "application/x-www-form-urlencoded; charset=utf-8", "/", + HTTP_BAD_REQUEST, PAYLOAD_TOO_LARGE.getErrorMessage() ) ); @@ -149,7 +181,7 @@ public class TestAuthorizationFilter { void testAuthFilterFailures( String method, String authHeader, String contentMd5, String host, String amzContentSha256, String date, String contentType, - String path, String expectedErrorMsg + String path, int expectedStatus, String expectedErrorMsg ) throws Exception { try { ContainerRequestContext context = setupContext(method, authHeader, @@ -172,22 +204,8 @@ void testAuthFilterFailures( fail("Empty AuthHeader must fail"); } } catch (WebApplicationException ex) { - if (authHeader == null || authHeader.isEmpty() || - authHeader.startsWith("AWS ")) { - // Empty auth header and unsupported AWS signature - // should fail with Invalid Request. - assertEquals(HTTP_FORBIDDEN, ex.getResponse().getStatus()); - assertEquals(expectedErrorMsg, - ex.getMessage()); - } else { - // Other requests have stale timestamp and - // should fail with Malformed Authorization Header. - assertEquals(HTTP_BAD_REQUEST, ex.getResponse().getStatus()); - assertEquals(expectedErrorMsg, - ex.getMessage()); - - } - + assertEquals(expectedStatus, ex.getResponse().getStatus()); + assertEquals(expectedErrorMsg, ex.getMessage()); } } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestAuthorizationV4HeaderParser.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestAuthorizationV4HeaderParser.java index 8812405d10d..0c259c42377 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestAuthorizationV4HeaderParser.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestAuthorizationV4HeaderParser.java @@ -18,12 +18,17 @@ package org.apache.hadoop.ozone.s3.signature; import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.MINUTES; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.MALFORMED_CREDENTIAL_DATE; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.REQUEST_TIME_TOO_SKEWED; import static org.apache.hadoop.ozone.s3.signature.SignatureProcessor.DATE_FORMATTER; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,14 +38,15 @@ public class TestAuthorizationV4HeaderParser { - private static final String SAMPLE_DATE = "20210202T144559Z"; - private String curDate; + private String sampleDate; @BeforeEach public void setup() { - LocalDate now = LocalDate.now(); + LocalDate now = LocalDate.now(ZoneOffset.UTC); curDate = DATE_FORMATTER.format(now); + sampleDate = StringToSignProducer.TIME_FORMATTER.format( + LocalDateTime.now(ZoneOffset.UTC)); } @Test @@ -50,7 +56,7 @@ public void testV4HeaderWellFormed() throws Exception { "SignedHeaders=host;range;x-amz-date, " + "Signature=fe5f80f77d5fa3beca038a248ff027"; AuthorizationV4HeaderParser v4 = - new AuthorizationV4HeaderParser(auth, SAMPLE_DATE); + new AuthorizationV4HeaderParser(auth, sampleDate); final SignatureInfo signatureInfo = v4.parseSignature(); assertEquals("ozone", signatureInfo.getAwsAccessId()); assertEquals(curDate, signatureInfo.getDate()); @@ -65,7 +71,7 @@ public void testV4HeaderMissingParts() { "Credential=ozone/" + curDate + "/us-east-1/s3/aws4_request, " + "SignedHeaders=host;range;x-amz-date,"; AuthorizationV4HeaderParser v4 = - new AuthorizationV4HeaderParser(auth, SAMPLE_DATE); + new AuthorizationV4HeaderParser(auth, sampleDate); assertThrows(MalformedResourceException.class, () -> v4.parseSignature()); } @@ -76,7 +82,7 @@ public void testV4HeaderInvalidCredential() { "SignedHeaders=host;range;x-amz-date, " + "Signature=fe5f80f77d5fa3beca038a248ff027"; AuthorizationV4HeaderParser v4 = - new AuthorizationV4HeaderParser(auth, SAMPLE_DATE); + new AuthorizationV4HeaderParser(auth, sampleDate); assertThrows(MalformedResourceException.class, () -> v4.parseSignature()); } @@ -91,7 +97,7 @@ public void testV4HeaderWithoutSpace() throws MalformedResourceException { + "=fe5f80f77d5fa3beca038a248ff027"; AuthorizationV4HeaderParser v4 = new AuthorizationV4HeaderParser(auth, - SAMPLE_DATE); + sampleDate); SignatureInfo signature = v4.parseSignature(); assertEquals("AWS4-HMAC-SHA256", signature.getAlgorithm()); @@ -106,46 +112,64 @@ public void testV4HeaderWithoutSpace() throws MalformedResourceException { @Test public void testV4HeaderDateValidationSuccess() throws MalformedResourceException { - // Case 1: valid date within range. - LocalDate now = LocalDate.now(); - String dateStr = DATE_FORMATTER.format(now); - testRequestWithSpecificDate(dateStr); - - // Case 2: Valid date with in range. - dateStr = DATE_FORMATTER.format(now.plus(1, DAYS)); - testRequestWithSpecificDate(dateStr); - - // Case 3: Valid date with in range. - dateStr = DATE_FORMATTER.format(now.minus(1, DAYS)); - testRequestWithSpecificDate(dateStr); + testRequestWithSpecificDate(curDate); + + String amzDatePlus14Min = StringToSignProducer.TIME_FORMATTER.format( + LocalDateTime.now(ZoneOffset.UTC).plus(14, MINUTES)); + testRequestWithSpecificDate(curDate, amzDatePlus14Min); + } + + @Test + public void testV4HeaderCredentialDateMismatch() { + LocalDate yesterday = LocalDate.now(ZoneOffset.UTC).minus(1, DAYS); + String mismatchedCredentialDate = DATE_FORMATTER.format(yesterday); + String auth = + "AWS4-HMAC-SHA256 Credential=ozone/" + mismatchedCredentialDate + + "/us-east-1/s3/aws4_request," + + "SignedHeaders=host;x-amz-content-sha256;x-amz-date," + + "Signature=fe5f80f77d5fa3beca038a248ff027"; + AuthorizationV4HeaderParser v4 = + new AuthorizationV4HeaderParser(auth, sampleDate); + MalformedResourceException ex = assertThrows(MalformedResourceException.class, + () -> v4.parseSignature()); + assertEquals(MALFORMED_CREDENTIAL_DATE, ex.getErrorCode()); + } + + @Test + public void testV4HeaderDateSkewValidationFailure() { + String staleDateTime = StringToSignProducer.TIME_FORMATTER.format( + LocalDateTime.now(ZoneOffset.UTC).minusMinutes(16)); + String auth = "AWS4-HMAC-SHA256 Credential=ozone/" + curDate + + "/us-east-1/s3/aws4_request, " + + "SignedHeaders=host;x-amz-content-sha256;x-amz-date, " + + "Signature=fe5f80f77d5fa3beca038a248ff027"; + AuthorizationV4HeaderParser v4 = + new AuthorizationV4HeaderParser(auth, staleDateTime); + MalformedResourceException ex = assertThrows(MalformedResourceException.class, + () -> v4.parseSignature()); + assertEquals(REQUEST_TIME_TOO_SKEWED, ex.getErrorCode()); } @Test public void testV4HeaderDateValidationFailure() { // Case 1: Empty date. - LocalDate now = LocalDate.now(); String dateStr = ""; assertThrows(MalformedResourceException.class, () -> testRequestWithSpecificDate(dateStr)); - // Case 2: Date after yesterday. - String dateStr2 = DATE_FORMATTER.format(now.plus(2, DAYS)); + // Case 2: Invalid date format + String dateStr2 = LocalDate.now(ZoneOffset.UTC).toString(); assertThrows(MalformedResourceException.class, () -> testRequestWithSpecificDate(dateStr2)); - - // Case 3: Date before yesterday. - String dateStr3 = DATE_FORMATTER.format(now.minus(2, DAYS)); - assertThrows(MalformedResourceException.class, - () -> testRequestWithSpecificDate(dateStr3)); - - // Case 4: Invalid date format - String dateStr4 = now.toString(); - assertThrows(MalformedResourceException.class, - () -> testRequestWithSpecificDate(dateStr4)); } private void testRequestWithSpecificDate(String dateStr) throws MalformedResourceException { + testRequestWithSpecificDate(dateStr, sampleDate); + } + + private void testRequestWithSpecificDate(String dateStr, String amzDate) + throws MalformedResourceException { String auth = "AWS4-HMAC-SHA256 Credential=ozone/" + dateStr + "/us-east-1/s3" + "/aws4_request," @@ -153,12 +177,13 @@ private void testRequestWithSpecificDate(String dateStr) + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; AuthorizationV4HeaderParser v4 = - new AuthorizationV4HeaderParser(auth, SAMPLE_DATE); + new AuthorizationV4HeaderParser(auth, amzDate); SignatureInfo signature = v4.parseSignature(); assertEquals("AWS4-HMAC-SHA256", signature.getAlgorithm()); assertEquals("ozone", signature.getAwsAccessId()); assertEquals(dateStr, signature.getDate()); + assertEquals(amzDate, signature.getDateTime()); assertEquals("host;x-amz-content-sha256;x-amz-date", signature.getSignedHeaders()); assertEquals("fe5f80f77d5fa3beca038a248ff027", signature.getSignature()); @@ -173,7 +198,7 @@ public void testV4HeaderRegionValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027%"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth, sampleDate) .parseSignature()); String auth2 = "AWS4-HMAC-SHA256 Credential=ozone/" + curDate + "s3/aws4_request," @@ -181,7 +206,7 @@ public void testV4HeaderRegionValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027%"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth2, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth2, sampleDate) .parseSignature()); } @@ -194,7 +219,7 @@ public void testV4HeaderServiceValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth, sampleDate) .parseSignature()); String auth2 = @@ -204,7 +229,7 @@ public void testV4HeaderServiceValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth2, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth2, sampleDate) .parseSignature()); } @@ -217,7 +242,7 @@ public void testV4HeaderRequestValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth, sampleDate) .parseSignature()); String auth2 = @@ -227,7 +252,7 @@ public void testV4HeaderRequestValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth2, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth2, sampleDate) .parseSignature()); String auth3 = @@ -237,7 +262,7 @@ public void testV4HeaderRequestValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth3, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth3, sampleDate) .parseSignature()); String auth4 = @@ -247,7 +272,7 @@ public void testV4HeaderRequestValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth4, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth4, sampleDate) .parseSignature()); } @@ -260,7 +285,7 @@ public void testV4HeaderSignedHeaderValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth, sampleDate) .parseSignature()); String auth2 = @@ -270,7 +295,7 @@ public void testV4HeaderSignedHeaderValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth2, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth2, sampleDate) .parseSignature()); String auth3 = @@ -280,7 +305,7 @@ public void testV4HeaderSignedHeaderValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth3, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth3, sampleDate) .parseSignature()); String auth4 = @@ -290,7 +315,7 @@ public void testV4HeaderSignedHeaderValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth4, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth4, sampleDate) .parseSignature()); } @@ -303,7 +328,7 @@ public void testV4HeaderSignatureValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027%"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth, sampleDate) .parseSignature()); String auth2 = @@ -313,7 +338,7 @@ public void testV4HeaderSignatureValidationFailure() throws Exception { + "Signature" + "="; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth2, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth2, sampleDate) .parseSignature()); String auth3 = @@ -322,7 +347,7 @@ public void testV4HeaderSignatureValidationFailure() throws Exception { + "SignedHeaders=host;x-amz-content-sha256;x-amz-date," + "="; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth3, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth3, sampleDate) .parseSignature()); } @@ -335,7 +360,7 @@ public void testV4HeaderHashAlgoValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth, sampleDate) .parseSignature()); String auth2 = @@ -344,7 +369,7 @@ public void testV4HeaderHashAlgoValidationFailure() throws Exception { + "SignedHeaders=host;x-amz-content-sha256;x-amz-date," + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; - assertNull(new AuthorizationV4HeaderParser(auth2, SAMPLE_DATE) + assertNull(new AuthorizationV4HeaderParser(auth2, sampleDate) .parseSignature()); String auth3 = @@ -353,7 +378,7 @@ public void testV4HeaderHashAlgoValidationFailure() throws Exception { + "SignedHeaders=host;x-amz-content-sha256;x-amz-date," + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; - assertNull(new AuthorizationV4HeaderParser(auth3, SAMPLE_DATE) + assertNull(new AuthorizationV4HeaderParser(auth3, sampleDate) .parseSignature()); // Invalid algorithm @@ -362,7 +387,7 @@ public void testV4HeaderHashAlgoValidationFailure() throws Exception { "SignedHeaders=host;range;x-amz-date, " + "Signature=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth4, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth4, sampleDate) .parseSignature()); } @@ -375,7 +400,7 @@ public void testV4HeaderCredentialValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth, sampleDate) .parseSignature()); String auth2 = @@ -385,7 +410,7 @@ public void testV4HeaderCredentialValidationFailure() throws Exception { + "Signature" + "=fe5f80f77d5fa3beca038a248ff027"; assertThrows(MalformedResourceException.class, - () -> new AuthorizationV4HeaderParser(auth2, SAMPLE_DATE) + () -> new AuthorizationV4HeaderParser(auth2, sampleDate) .parseSignature()); } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestAuthorizationV4QueryParser.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestAuthorizationV4QueryParser.java index bc86c2a9b28..7fbe108ab6c 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestAuthorizationV4QueryParser.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestAuthorizationV4QueryParser.java @@ -17,6 +17,7 @@ package org.apache.hadoop.ozone.s3.signature; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.MALFORMED_CREDENTIAL_DATE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -37,6 +38,7 @@ public class TestAuthorizationV4QueryParser { private static final String DATETIME = ZonedDateTime.now().format( StringToSignProducer.TIME_FORMATTER); + private static final String CREDENTIAL_DATE = DATETIME.substring(0, 8); @Test public void testInvalidAlgorithm() { @@ -44,7 +46,8 @@ public void testInvalidAlgorithm() { // Missing algorithm Map<String, String> parameters = new HashMap<>(); parameters.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request"); + "AKIAIOSFODNN7EXAMPLE%2F" + CREDENTIAL_DATE + + "%2Fus-east-1%2Fs3%2Faws4_request"); parameters.put("X-Amz-Date", DATETIME); parameters.put("X-Amz-Expires", "10000"); parameters.put("X-Amz-SignedHeaders", "host"); @@ -71,7 +74,8 @@ public void testInvalidDateExpiredHeaders() throws Exception { Map<String, String> parameters = new HashMap<>(); parameters.put("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); parameters.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request"); + "AKIAIOSFODNN7EXAMPLE%2F" + CREDENTIAL_DATE + + "%2Fus-east-1%2Fs3%2Faws4_request"); parameters.put("X-Amz-Expires", "10000"); parameters.put("X-Amz-SignedHeaders", "host"); parameters.put("X-Amz-Signature", @@ -112,6 +116,8 @@ public void testInvalidDateExpiredHeaders() throws Exception { () -> new AuthorizationV4QueryParser(parameters).parseSignature()); // Expired request -> 403 (AccessDenied), not 400. + parameters.put("X-Amz-Credential", + "AKIAIOSFODNN7EXAMPLE%2F20160801%2Fus-east-1%2Fs3%2Faws4_request"); parameters.put("X-Amz-Date", "20160801T083241Z"); parameters.put("X-Amz-Expires", "10000"); assertThrows(AccessDeniedResourceException.class, @@ -126,7 +132,8 @@ public void testUnExpiredHeaders() throws Exception { Map<String, String> parameters = new HashMap<>(); parameters.put("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); parameters.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request"); + "AKIAIOSFODNN7EXAMPLE%2F" + CREDENTIAL_DATE + + "%2Fus-east-1%2Fs3%2Faws4_request"); parameters.put("X-Amz-Date", DATETIME); parameters.put("X-Amz-Expires", "10000"); parameters.put("X-Amz-SignedHeaders", "host"); @@ -151,7 +158,7 @@ public void testInvalidCredential() throws Exception { Map<String, String> parameters = new HashMap<>(); parameters.put("X-Amz-Algorithm", "AWS4-ZAVC-HJUA123"); parameters.put("X-Amz-Credential", - "%2F20130524%2Fus-east-1%2Fs3%2Faws4_request"); + "%2F" + CREDENTIAL_DATE + "%2Fus-east-1%2Fs3%2Faws4_request"); parameters.put("X-Amz-Date", DATETIME); parameters.put("X-Amz-Expires", "10000"); parameters.put("X-Amz-SignedHeaders", "host"); @@ -162,25 +169,25 @@ public void testInvalidCredential() throws Exception { // Empty AWS region parameters.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE%2F20130524%2F%2Fs3%2Faws4_request"); + "AKIAIOSFODNN7EXAMPLE%2F" + CREDENTIAL_DATE + "%2F%2Fs3%2Faws4_request"); assertThrows(MalformedResourceException.class, () -> new AuthorizationV4QueryParser(parameters).parseSignature()); // Empty AWS request parameters.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2F"); + "AKIAIOSFODNN7EXAMPLE%2F" + CREDENTIAL_DATE + "%2Fus-east-1%2Fs3%2F"); assertThrows(MalformedResourceException.class, () -> new AuthorizationV4QueryParser(parameters).parseSignature()); // Invalid aws request parameters.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws_request"); + "AKIAIOSFODNN7EXAMPLE%2F" + CREDENTIAL_DATE + "%2Fus-east-1%2Fs3%2Faws_request"); assertThrows(MalformedResourceException.class, () -> new AuthorizationV4QueryParser(parameters).parseSignature()); // Empty aws service parameters.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2F%2Faws4_request"); + "AKIAIOSFODNN7EXAMPLE%2F" + CREDENTIAL_DATE + "%2Fus-east-1%2F%2Faws4_request"); assertThrows(MalformedResourceException.class, () -> new AuthorizationV4QueryParser(parameters).parseSignature()); @@ -199,7 +206,7 @@ public void testInvalidCredential() throws Exception { // No URL encoding parameters.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"); + "AKIAIOSFODNN7EXAMPLE/" + CREDENTIAL_DATE + "/us-east-1/s3/aws4_request"); assertThrows(MalformedResourceException.class, () -> new AuthorizationV4QueryParser(parameters).parseSignature()); } @@ -211,7 +218,8 @@ public void testInvalidSignedHeaders() throws Exception { Map<String, String> parameters = new HashMap<>(); parameters.put("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); parameters.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request"); + "AKIAIOSFODNN7EXAMPLE%2F" + CREDENTIAL_DATE + + "%2Fus-east-1%2Fs3%2Faws4_request"); parameters.put("X-Amz-Date", DATETIME); parameters.put("X-Amz-Expires", "10000"); parameters.put("X-Amz-Signature", @@ -232,7 +240,8 @@ public void testInvalidSignature() throws Exception { Map<String, String> parameters = new HashMap<>(); parameters.put("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); parameters.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request"); + "AKIAIOSFODNN7EXAMPLE%2F" + CREDENTIAL_DATE + + "%2Fus-east-1%2Fs3%2Faws4_request"); parameters.put("X-Amz-Date", DATETIME); parameters.put("X-Amz-Expires", "10000"); parameters.put("X-Amz-SignedHeaders", "host"); @@ -247,6 +256,22 @@ public void testInvalidSignature() throws Exception { () -> new AuthorizationV4QueryParser(parameters).parseSignature()); } + @Test + public void testCredentialDateMismatch() { + Map<String, String> parameters = new HashMap<>(); + parameters.put("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); + parameters.put("X-Amz-Credential", + "AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request"); + parameters.put("X-Amz-Date", DATETIME); + parameters.put("X-Amz-Expires", "10000"); + parameters.put("X-Amz-SignedHeaders", "host"); + parameters.put("X-Amz-Signature", + "aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404"); + MalformedResourceException ex = assertThrows(MalformedResourceException.class, + () -> new AuthorizationV4QueryParser(parameters).parseSignature()); + assertEquals(MALFORMED_CREDENTIAL_DATE, ex.getErrorCode()); + } + /** * Based on <a href="https://docs.aws.amazon.com * /AmazonS3/latest/API/sigv4-query-string-auth.html">AWS example</a>. @@ -258,7 +283,7 @@ public void testWithAWSExample() throws Exception { queryParams.put("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); queryParams.put("X-Amz-Credential", - "AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"); + "AKIAIOSFODNN7EXAMPLE/" + CREDENTIAL_DATE + "/us-east-1/s3/aws4_request"); queryParams.put("X-Amz-Date", DATETIME); queryParams.put("X-Amz-Expires", "86400"); queryParams.put("X-Amz-SignedHeaders", "host"); @@ -268,7 +293,7 @@ public void testWithAWSExample() throws Exception { String canonicalRequest = "GET\n" + "/test.txt\n" + "X-Amz-Algorithm=AWS4-HMAC-SHA256&" - + "X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2F" + + "X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F" + CREDENTIAL_DATE + "%2F" + "us-east-1%2Fs3%2Faws4_request&X-Amz-Date=" + DATETIME + "&X-Amz-Expires=86400&X-Amz-SignedHeaders=host\n" + "host:localhost\n" @@ -291,7 +316,7 @@ protected void validateDateAndExpires() { headers.put("host", "localhost"); final String stringToSign = - StringToSignProducer.createSignatureBase(signatureInfo, "https", "GET", + StringToSignProducer.createSignatureBase(signatureInfo, "GET", headers, queryParams); MessageDigest md = MessageDigest.getInstance("SHA-256"); @@ -299,7 +324,7 @@ protected void validateDateAndExpires() { assertEquals("AWS4-HMAC-SHA256\n" + DATETIME + "\n" - + "20130524/us-east-1/s3/aws4_request\n" + + CREDENTIAL_DATE + "/us-east-1/s3/aws4_request\n" + Hex.encode(md.digest()).toLowerCase(), stringToSign); } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestStringToSignProducer.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestStringToSignProducer.java index bc2da6fde91..ba95cab32c6 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestStringToSignProducer.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/signature/TestStringToSignProducer.java @@ -17,6 +17,8 @@ package org.apache.hadoop.ozone.s3.signature; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.MALFORMED_HEADER; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.REQUEST_TIME_TOO_SKEWED; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.S3_AUTHINFO_CREATION_ERROR; import static org.apache.hadoop.ozone.s3.signature.SignatureProcessor.DATE_FORMATTER; import static org.apache.hadoop.ozone.s3.util.S3Consts.UNSIGNED_PAYLOAD; @@ -30,6 +32,7 @@ import java.security.MessageDigest; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -53,7 +56,7 @@ public class TestStringToSignProducer { private static final String DATETIME = StringToSignProducer.TIME_FORMATTER. - format(LocalDateTime.now()); + format(LocalDateTime.now(ZoneOffset.UTC)); @Test public void test() throws Exception { @@ -75,8 +78,10 @@ public void test() throws Exception { + "host;x-amz-content-sha256;x-amz-date;content-type\n" + "Content-SHA"; + String credentialDate = DATETIME.substring(0, 8); String authHeader = - "AWS4-HMAC-SHA256 Credential=AKIAJWFJK62WUTKNFJJA/20181009/us-east-1" + "AWS4-HMAC-SHA256 Credential=AKIAJWFJK62WUTKNFJJA/" + credentialDate + + "/us-east-1" + "/s3/aws4_request, " + "SignedHeaders=host;x-amz-content-sha256;x-amz-date;" + "content-type, " @@ -90,12 +95,7 @@ public void test() throws Exception { Map<String, String> queryParameters = new HashMap<>(); final SignatureInfo signatureInfo = - new AuthorizationV4HeaderParser(authHeader, DATETIME) { - @Override - public void validateDateRange(Credential credentialObj) { - //NOOP - } - }.parseSignature(); + new AuthorizationV4HeaderParser(authHeader, DATETIME).parseSignature(); signatureInfo.setPayloadHash("Content-SHA"); signatureInfo.setUnfilteredURI("/buckets"); @@ -104,7 +104,6 @@ public void validateDateRange(Credential credentialObj) { final String signatureBase = StringToSignProducer.createSignatureBase( signatureInfo, - "http", "GET", headers, queryParameters); @@ -114,7 +113,7 @@ public void validateDateRange(Credential credentialObj) { assertEquals("AWS4-HMAC-SHA256\n" + DATETIME + "\n" - + "20181009/us-east-1/s3/aws4_request\n" + + credentialDate + "/us-east-1/s3/aws4_request\n" + Hex.encode(md.digest()).toLowerCase(), signatureBase, "String to sign is invalid"); } @@ -130,7 +129,7 @@ public void testUrlEncodeInCanonicalRequest() { queryParams.put("q+1*2~3", "v 4*5~6"); final String canonicalRequest = StringToSignProducer.buildCanonicalRequest( - "https", "GET", "/bucket/a+b*c~d/foo bar", "host;x-amz-content-sha256;x-amz-date", + "GET", "/bucket/a+b*c~d/foo bar", "host;x-amz-content-sha256;x-amz-date", headers, queryParams, UNSIGNED_PAYLOAD); assertEquals( @@ -157,7 +156,7 @@ public void testUrlEncodeInCanonicalRequest() { @ValueSource(strings = {"[::1]:9878", "[2001:db8::1]:9878", "[::1]"}) public void testIPv6HostInStringToSign(String host) throws Exception { String signedHeaders = "host;x-amz-content-sha256;x-amz-date"; - String credentialScope = DATE_FORMATTER.format(LocalDate.now()) + String credentialScope = DATETIME.substring(0, 8) + "/us-east-1/s3/aws4_request"; String authHeader = "AWS4-HMAC-SHA256 Credential=ozone/" + credentialScope + ", SignedHeaders=" + signedHeaders @@ -223,7 +222,7 @@ private ContainerRequestContext setupContext( private static Stream<Arguments> testValidateRequestHeadersInput() { String authHeader = "AWS4-HMAC-SHA256 Credential=ozone/" - + DATE_FORMATTER.format(LocalDate.now()) + + DATE_FORMATTER.format(LocalDate.now(ZoneOffset.UTC)) + "/us-east-1/s3/aws4_request, " + "SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date," + " Signature=db81b057718d7c1b3b8" @@ -246,17 +245,18 @@ private static Stream<Arguments> testValidateRequestHeadersInput() { new MultivaluedHashMap<String, String>(headersMap1); headersMap3.remove("X-Amz-Date"); headersMap3.putSingle("X-Amz-Date", LocalDateTime.now().toString()); - // Expired X-Amz-Date + // Expired X-Amz-Date (more than 15 minutes in the past) MultivaluedMap<String, String> headersMap4 = new MultivaluedHashMap<String, String>(headersMap1); headersMap4.remove("X-Amz-Date"); headersMap4.putSingle("X-Amz-Date", StringToSignProducer.TIME_FORMATTER. - format(LocalDateTime.now().minusDays(8))); + format(LocalDateTime.now(ZoneOffset.UTC).minusMinutes(16))); + // X-Amz-Date too far in the future (more than 15 minutes) MultivaluedMap<String, String> headersMap5 = new MultivaluedHashMap<String, String>(headersMap1); headersMap5.remove("X-Amz-Date"); headersMap5.putSingle("X-Amz-Date", StringToSignProducer.TIME_FORMATTER. - format(LocalDateTime.now().plusDays(8))); + format(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(16))); // Missing X-Amz-Content-Sha256 MultivaluedMap<String, String> headersMap6 = new MultivaluedHashMap<String, String>(headersMap1); @@ -264,10 +264,10 @@ private static Stream<Arguments> testValidateRequestHeadersInput() { return Stream.of( arguments(headersMap1, "success"), - arguments(headersMap2, S3_AUTHINFO_CREATION_ERROR.getCode()), - arguments(headersMap3, S3_AUTHINFO_CREATION_ERROR.getCode()), - arguments(headersMap4, S3_AUTHINFO_CREATION_ERROR.getCode()), - arguments(headersMap5, S3_AUTHINFO_CREATION_ERROR.getCode()), + arguments(headersMap2, MALFORMED_HEADER.getCode()), + arguments(headersMap3, MALFORMED_HEADER.getCode()), + arguments(headersMap4, REQUEST_TIME_TOO_SKEWED.getCode()), + arguments(headersMap5, REQUEST_TIME_TOO_SKEWED.getCode()), arguments(headersMap6, S3_AUTHINFO_CREATION_ERROR.getCode()) ); } @@ -284,12 +284,18 @@ public void testValidateRequestHeaders( "GET", headerMap, new MultivaluedHashMap<>()); - SignatureInfo signatureInfo = new AuthorizationV4HeaderParser( - headerMap.getFirst("Authorization"), - headerMap.getFirst("X-Amz-Date")).parseSignature(); - signatureInfo.setUnfilteredURI("/"); try { + SignatureInfo signatureInfo = new AuthorizationV4HeaderParser( + headerMap.getFirst("Authorization"), + headerMap.getFirst("X-Amz-Date")).parseSignature(); + signatureInfo.setUnfilteredURI("/"); StringToSignProducer.createSignatureBase(signatureInfo, context); + } catch (MalformedResourceException e) { + if (REQUEST_TIME_TOO_SKEWED.equals(e.getErrorCode())) { + actualResult = REQUEST_TIME_TOO_SKEWED.getCode(); + } else { + actualResult = MALFORMED_HEADER.getCode(); + } } catch (OS3Exception e) { actualResult = e.getCode(); } @@ -324,7 +330,7 @@ public void testValidateCanonicalHeaders( String expectedResult) throws Exception { String actualResult = "success"; String authHeader = "AWS4-HMAC-SHA256 Credential=ozone/" - + DATE_FORMATTER.format(LocalDate.now()) + + DATE_FORMATTER.format(LocalDate.now(ZoneOffset.UTC)) + "/us-east-1/s3/aws4_request, " + "SignedHeaders=" + signedHeaders + ", " + "Signature=db81b057718d7c1b3b" + --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
