Enforce payload checksums

CRC32, CRC32C, SHA1, and SHA256 are support but CRC64NVME is not.
Fixes #806.  References #830.
master
Andrew Gaul 2025-09-03 10:51:31 -07:00
rodzic d503c07655
commit eb5c59a371
4 zmienionych plików z 63 dodań i 3 usunięć

Wyświetl plik

@ -19,10 +19,15 @@ package org.gaul.s3proxy;
import java.io.FilterInputStream; import java.io.FilterInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Base64;
import javax.annotation.Nullable;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
/** /**
* Parse an AWS v4 signature chunked stream. Reference: * Parse an AWS v4 signature chunked stream. Reference:
* https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html * https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
@ -36,9 +41,27 @@ final class ChunkedInputStream extends FilterInputStream {
justification = "https://github.com/gaul/s3proxy/issues/205") justification = "https://github.com/gaul/s3proxy/issues/205")
@SuppressWarnings("UnusedVariable") @SuppressWarnings("UnusedVariable")
private String currentSignature; private String currentSignature;
private final Hasher hasher;
ChunkedInputStream(InputStream is) { ChunkedInputStream(InputStream is) {
super(is); super(is);
hasher = null;
}
ChunkedInputStream(InputStream is, @Nullable String trailer) {
super(is);
if ("x-amz-checksum-crc32".equals(trailer)) {
hasher = Hashing.crc32().newHasher();
} else if ("x-amz-checksum-crc32c".equals(trailer)) {
hasher = Hashing.crc32c().newHasher();
} else if ("x-amz-checksum-sha1".equals(trailer)) {
hasher = Hashing.sha1().newHasher();
} else if ("x-amz-checksum-sha256".equals(trailer)) {
hasher = Hashing.sha256().newHasher();
} else {
// TODO: Guava does not support x-amz-checksum-crc64nvme
hasher = null;
}
} }
@Override @Override
@ -50,6 +73,25 @@ final class ChunkedInputStream extends FilterInputStream {
} }
String[] parts = line.split(";", 2); String[] parts = line.split(";", 2);
if (parts[0].startsWith("x-amz-checksum-")) { if (parts[0].startsWith("x-amz-checksum-")) {
String[] checksumParts = parts[0].split(":", 2);
var expectedHash = checksumParts[1];
String actualHash;
switch (checksumParts[0]) {
case "x-amz-checksum-crc32":
case "x-amz-checksum-crc32c":
// Use big-endian to match AWS
actualHash = Base64.getEncoder().encodeToString(ByteBuffer.allocate(4).putInt(hasher.hash().asInt()).array());
break;
case "x-amz-checksum-sha1":
case "x-amz-checksum-sha256":
actualHash = Base64.getEncoder().encodeToString(hasher.hash().asBytes());
break;
default:
throw new IllegalArgumentException("Unknown value: " + checksumParts[0]);
}
if (!expectedHash.equals(actualHash)) {
throw new IOException(new S3Exception(S3ErrorCode.BAD_DIGEST));
}
currentLength = 0; currentLength = 0;
} else { } else {
currentLength = Integer.parseInt(parts[0], 16); currentLength = Integer.parseInt(parts[0], 16);
@ -60,10 +102,14 @@ final class ChunkedInputStream extends FilterInputStream {
chunk = new byte[currentLength]; chunk = new byte[currentLength];
currentIndex = 0; currentIndex = 0;
ByteStreams.readFully(in, chunk); ByteStreams.readFully(in, chunk);
if (hasher != null) {
hasher.putBytes(chunk);
}
// TODO: check currentSignature // TODO: check currentSignature
if (currentLength == 0) { if (currentLength == 0) {
return -1; return -1;
} }
// consume trailing \r\n
readLine(in); readLine(in);
} }
return chunk[currentIndex++] & 0xFF; return chunk[currentIndex++] & 0xFF;

Wyświetl plik

@ -516,7 +516,7 @@ public class S3ProxyHandler {
if ("STREAMING-AWS4-HMAC-SHA256-PAYLOAD".equals(contentSha256)) { if ("STREAMING-AWS4-HMAC-SHA256-PAYLOAD".equals(contentSha256)) {
is = new ChunkedInputStream(is); is = new ChunkedInputStream(is);
} else if ("STREAMING-UNSIGNED-PAYLOAD-TRAILER".equals(contentSha256)) { } else if ("STREAMING-UNSIGNED-PAYLOAD-TRAILER".equals(contentSha256)) {
is = new ChunkedInputStream(is); is = new ChunkedInputStream(is, request.getHeader(AwsHttpHeaders.TRAILER));
} }
} else if (requestIdentity == null) { } else if (requestIdentity == null) {
throw new S3Exception(S3ErrorCode.ACCESS_DENIED); throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
@ -608,7 +608,7 @@ public class S3ProxyHandler {
is = new ChunkedInputStream(is); is = new ChunkedInputStream(is);
} else if ("STREAMING-UNSIGNED-PAYLOAD-TRAILER".equals(contentSha256)) { } else if ("STREAMING-UNSIGNED-PAYLOAD-TRAILER".equals(contentSha256)) {
payload = new byte[0]; payload = new byte[0];
is = new ChunkedInputStream(is); is = new ChunkedInputStream(is, request.getHeader(AwsHttpHeaders.TRAILER));
} else if ("UNSIGNED-PAYLOAD".equals(contentSha256)) { } else if ("UNSIGNED-PAYLOAD".equals(contentSha256)) {
payload = new byte[0]; payload = new byte[0];
} else { } else {

Wyświetl plik

@ -141,6 +141,14 @@ final class S3ProxyHandlerJetty extends AbstractHandler {
ise.getMessage()); ise.getMessage());
baseRequest.setHandled(true); baseRequest.setHandled(true);
return; return;
} catch (IOException ioe) {
var cause = Throwables2.getFirstThrowableOfType(ioe, S3Exception.class);
if (cause != null) {
sendS3Exception(request, response, cause);
baseRequest.setHandled(true);
return;
}
throw ioe;
} catch (KeyNotFoundException knfe) { } catch (KeyNotFoundException knfe) {
S3ErrorCode code = S3ErrorCode.NO_SUCH_KEY; S3ErrorCode code = S3ErrorCode.NO_SUCH_KEY;
handler.sendSimpleErrorResponse(request, response, code, handler.sendSimpleErrorResponse(request, response, code,

Wyświetl plik

@ -30,6 +30,7 @@ import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.apache.ApacheHttpClient; import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm;
import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.AttributeMap;
@ -84,6 +85,11 @@ public final class AwsSdk2Test {
var putRequest = PutObjectRequest.builder() var putRequest = PutObjectRequest.builder()
.bucket(containerName) .bucket(containerName)
.key(key) .key(key)
// TODO: parameterize test with JUnit 5
//.checksumAlgorithm(ChecksumAlgorithm.CRC32)
.checksumAlgorithm(ChecksumAlgorithm.CRC32_C)
//.checksumAlgorithm(ChecksumAlgorithm.SHA1)
//.checksumAlgorithm(ChecksumAlgorithm.SHA256)
.build(); .build();
s3Client.putObject(putRequest, RequestBody.fromBytes(byteSource.read())); s3Client.putObject(putRequest, RequestBody.fromBytes(byteSource.read()));