Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions src/main/java/org/gaul/s3proxy/AwsSignature.java
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,29 @@ private static byte[] signMessage(byte[] data, byte[] key, String algorithm)
return mac.doFinal(data);
}

/**
* Derive the AWS SigV4 signing key from the credential and auth header.
*/
static byte[] deriveSigningKeyV4(S3AuthorizationHeader authHeader,
String credential)
throws InvalidKeyException, NoSuchAlgorithmException {
String algorithm = authHeader.getHmacAlgorithm();
byte[] dateKey = signMessage(
authHeader.getDate().getBytes(StandardCharsets.UTF_8),
("AWS4" + credential).getBytes(StandardCharsets.UTF_8),
algorithm);
byte[] dateRegionKey = signMessage(
authHeader.getRegion().getBytes(StandardCharsets.UTF_8),
dateKey,
algorithm);
byte[] dateRegionServiceKey = signMessage(
authHeader.getService().getBytes(StandardCharsets.UTF_8),
dateRegionKey, algorithm);
return signMessage(
"aws4_request".getBytes(StandardCharsets.UTF_8),
dateRegionServiceKey, algorithm);
}

private static String getMessageDigest(byte[] payload, String algorithm)
throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance(algorithm);
Expand Down Expand Up @@ -338,20 +361,7 @@ static String createAuthorizationSignatureV4(
String canonicalRequest = createCanonicalRequest(request, uri, payload,
authHeader.getHashAlgorithm());
String algorithm = authHeader.getHmacAlgorithm();
byte[] dateKey = signMessage(
authHeader.getDate().getBytes(StandardCharsets.UTF_8),
("AWS4" + credential).getBytes(StandardCharsets.UTF_8),
algorithm);
byte[] dateRegionKey = signMessage(
authHeader.getRegion().getBytes(StandardCharsets.UTF_8),
dateKey,
algorithm);
byte[] dateRegionServiceKey = signMessage(
authHeader.getService().getBytes(StandardCharsets.UTF_8),
dateRegionKey, algorithm);
byte[] signingKey = signMessage(
"aws4_request".getBytes(StandardCharsets.UTF_8),
dateRegionServiceKey, algorithm);
byte[] signingKey = deriveSigningKeyV4(authHeader, credential);
String date = request.getHeader(AwsHttpHeaders.DATE);
if (date == null) {
date = request.getParameter("X-Amz-Date");
Expand Down
106 changes: 100 additions & 6 deletions src/main/java/org/gaul/s3proxy/ChunkedInputStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

import javax.annotation.Nullable;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;

/**
Expand All @@ -34,21 +41,28 @@
*/
final class ChunkedInputStream extends FilterInputStream {
private static final int MAX_LINE_LENGTH = 4096;
private static final String EMPTY_SHA256 =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
private byte[] chunk;
private int currentIndex;
private int currentLength;
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(
value = "URF_UNREAD_FIELD",
justification = "https://github.com/gaul/s3proxy/issues/205")
@SuppressWarnings("UnusedVariable")
private String currentSignature;
private final int maxChunkSize;
private final Hasher hasher;
@Nullable private final byte[] signingKey;
@Nullable private final String hmacAlgorithm;
@Nullable private final String timestamp;
@Nullable private final String scope;
@Nullable private String previousSignature;

ChunkedInputStream(InputStream is, int maxChunkSize) {
super(is);
this.maxChunkSize = maxChunkSize;
hasher = null;
signingKey = null;
hmacAlgorithm = null;
timestamp = null;
scope = null;
}

@SuppressWarnings("deprecation")
Expand All @@ -68,6 +82,33 @@ final class ChunkedInputStream extends FilterInputStream {
// TODO: Guava does not support x-amz-checksum-crc64nvme
hasher = null;
}
signingKey = null;
hmacAlgorithm = null;
timestamp = null;
scope = null;
}

/**
* Construct a chunked stream that verifies the per-chunk signature chain
* used by STREAMING-AWS4-HMAC-SHA256-PAYLOAD.
*
* @param seedSignature the Authorization header signature (hex-encoded)
* @param signingKey the AWS SigV4 signing key
* @param hmacAlgorithm HMAC algorithm name (e.g. "HmacSHA256")
* @param timestamp full ISO8601 request timestamp (x-amz-date)
* @param scope credential scope (date/region/service/aws4_request)
*/
ChunkedInputStream(InputStream is, int maxChunkSize,
String seedSignature, byte[] signingKey, String hmacAlgorithm,
String timestamp, String scope) {
super(is);
this.maxChunkSize = maxChunkSize;
this.hasher = null;
this.signingKey = signingKey.clone();
this.hmacAlgorithm = hmacAlgorithm;
this.timestamp = timestamp;
this.scope = scope;
this.previousSignature = seedSignature;
}

@Override
Expand Down Expand Up @@ -98,15 +139,21 @@ public int read() throws IOException {
}
}
if (parts.length > 1) {
currentSignature = parts[1];
String sigPart = parts[1];
int eq = sigPart.indexOf('=');
currentSignature = eq >= 0 ? sigPart.substring(eq + 1) : sigPart;
} else {
currentSignature = null;
}
chunk = new byte[currentLength];
currentIndex = 0;
ByteStreams.readFully(in, chunk);
if (hasher != null) {
hasher.putBytes(chunk);
}
// TODO: check currentSignature
if (signingKey != null) {
verifyChunkSignature(chunk, currentSignature);
}
if (currentLength == 0) {
return -1;
}
Expand All @@ -132,6 +179,53 @@ public int read(byte[] b, int off, int len) throws IOException {
return i;
}

private void verifyChunkSignature(byte[] data, @Nullable String signature)
throws IOException {
if (signature == null) {
throw new IOException(new S3Exception(
S3ErrorCode.SIGNATURE_DOES_NOT_MATCH));
}
String chunkHash;
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
chunkHash = BaseEncoding.base16().lowerCase()
.encode(md.digest(data));
} catch (NoSuchAlgorithmException e) {
throw new IOException(e);
}
String stringToSign = "AWS4-HMAC-SHA256-PAYLOAD\n" +
timestamp + "\n" +
scope + "\n" +
previousSignature + "\n" +
EMPTY_SHA256 + "\n" +
chunkHash;
String expected;
try {
Mac mac = Mac.getInstance(hmacAlgorithm);
mac.init(new SecretKeySpec(signingKey, hmacAlgorithm));
expected = BaseEncoding.base16().lowerCase().encode(
mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new IOException(e);
}
if (!constantTimeEquals(expected, signature)) {
throw new IOException(new S3Exception(
S3ErrorCode.SIGNATURE_DOES_NOT_MATCH));
}
previousSignature = signature;
}

private static boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int diff = 0;
for (int i = 0; i < a.length(); i++) {
diff |= a.charAt(i) ^ b.charAt(i);
}
return diff == 0;
}

/**
* Read a \r\n terminated line from an InputStream.
*
Expand Down
21 changes: 20 additions & 1 deletion src/main/java/org/gaul/s3proxy/S3ProxyHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,9 @@ public final void doHandle(HttpServletRequest baseRequest,
} else if ("STREAMING-AWS4-HMAC-SHA256-PAYLOAD".equals(
contentSha256)) {
payload = new byte[0];
is = new ChunkedInputStream(is, v4MaxChunkSize);
// ChunkedInputStream constructed below after deriving
// the signing key so per-chunk signatures can be
// verified.
} else if ("STREAMING-UNSIGNED-PAYLOAD-TRAILER".equals(contentSha256)) {
payload = new byte[0];
is = new ChunkedInputStream(is, v4MaxChunkSize, request.getHeader(AwsHttpHeaders.TRAILER));
Expand Down Expand Up @@ -685,6 +687,23 @@ public final void doHandle(HttpServletRequest baseRequest,
.createAuthorizationSignatureV4(// v4 sign
baseRequest, authHeader, payload, uriForSigning,
credential);
if ("STREAMING-AWS4-HMAC-SHA256-PAYLOAD".equals(
contentSha256)) {
byte[] signingKey = AwsSignature.deriveSigningKeyV4(
authHeader, credential);
String scope = authHeader.getDate() + "/" +
authHeader.getRegion() + "/" +
authHeader.getService() + "/aws4_request";
String timestamp = request.getHeader(
AwsHttpHeaders.DATE);
if (timestamp == null) {
timestamp = request.getParameter("X-Amz-Date");
}
is = new ChunkedInputStream(is, v4MaxChunkSize,
expectedSignature, signingKey,
authHeader.getHmacAlgorithm(), timestamp,
scope);
}
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new S3Exception(S3ErrorCode.INVALID_ARGUMENT, e);
}
Expand Down
1 change: 0 additions & 1 deletion src/test/java/org/gaul/s3proxy/AwsSdkTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@ public void testAwsV4Signature() throws Exception {
}
}

@Ignore
@Test
public void testAwsV4SignatureChunkedSigned() throws Exception {
client = AmazonS3ClientBuilder.standard()
Expand Down
Loading