You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ozone.apache.org by sa...@apache.org on 2026/10/02 19:34:22 UTC

(ozone) branch master updated: S3 header auth expiry in 15min and correct date validation.

This is an automated email from the ASF dual-hosted git repository.

SaketaChalamchala pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 29b98532703 S3 header auth expiry in 15min and correct date validation.
29b98532703 is described below

commit 29b9853270342eea4d065b1f14c332ad384f83e5
Author: SaketaChalamchala <sa...@gmail.com>
AuthorDate: Fri Oct 2 12:22:59 2026 -0700

    S3 header auth expiry in 15min and correct date 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: commits-unsubscribe@ozone.apache.org
For additional commands, e-mail: commits-help@ozone.apache.org