Skip to content

Answer SSE-C on the stores that encrypt - #1132

Merged
gaul merged 1 commit into
masterfrom
sse-c
Aug 9, 2026
Merged

Answer SSE-C on the stores that encrypt#1132
gaul merged 1 commit into
masterfrom
sse-c

Conversation

@gaul

@gaul gaul commented Aug 9, 2026

Copy link
Copy Markdown
Owner

The last commit refused SSE-C everywhere because honoring a caller's key means carrying it on every read and copy, not echoing it back on the write that supplied it. This carries it. The frontend admits the customer-key triple and its copy-source variants onto requests it already builds -- put, get, head, copy, the multipart create, part and completion, the part-copy in both its native and streamed forms, and the POST form fields -- and the aws-s3 store forwards them verbatim like everything else, its service judging the values.

The transient store is its own service, so it judges. A write vets the triple -- all three fields together, AES256, a 256-bit key, an MD5 that matches -- and the object comes to rest under the key's MD5 and never the key, which is no more than S3 itself echoes on responses. SSE-C has an observable half SSE-S3 does not: the object answers only to its key. A read presenting none or the wrong one is 400 -- the wrong one too, which the suite pins where AWS's documentation suggests 403 -- and every later request of a multipart upload, each part and the completion alike, must present the create-time key again. The completion response echoes the algorithm and MD5 from the request it judged, the SDK's response type modeling no field for them.

Reads are judged in the frontend rather than the store, whose getBlob and blobMetadata also serve the frontend's own bookkeeping -- the stub consulted before a completion, the ETag before a conditional write -- which reads without a key the way S3's own internals do. The stores vet what is always a caller's act: writes, parts, and the copy's source, read inside copyBlob. What both judge the same way lives in CustomerKeys.

uploadMultipartPart takes the SDK's UploadPartRequest now, the one call whose carrier had no room for a part's key, presented again on every part as S3 requires. The aws-s3 store forwarding the request instead of rebuilding it also stops dropping the Content-MD5 a part arrived with.

The encryption tag admitted whole brings the SSE-KMS conformance tests with it, and they found two holes beside SSE-C. The frontend refuses what is malformed wherever it lands -- an algorithm S3 does not have, a KMS key under an algorithm that does not name one -- because LocalStack accepts both and the pass-through lane has to answer as S3 does; and it refuses the write-side family on GET and HEAD, where an object's encryption is reported, not asked for. The transient store further requires a key id with aws:kms, the way rgw does. kms_keyid becomes an ARN in the lane config for the same reason kms_keyid2 already was: that is what a response carries.

Both lanes whose store encrypts run the full tag now and pass -- 659 tests on the transient store, 658 through LocalStack -- and the filesystem lane still refuses the family whole, 501 on every header of it, reporting no encryption it does not perform. Unlike S3, S3Proxy accepts SSE-C over plain HTTP, its TLS commonly terminating elsewhere.

See #402.

The last commit refused SSE-C everywhere because honoring a caller's
key means carrying it on every read and copy, not echoing it back on
the write that supplied it.  This carries it.  The frontend admits the
customer-key triple and its copy-source variants onto requests it
already builds -- put, get, head, copy, the multipart create, part and
completion, the part-copy in both its native and streamed forms, and
the POST form fields -- and the aws-s3 store forwards them verbatim
like everything else, its service judging the values.

The transient store is its own service, so it judges.  A write vets the
triple -- all three fields together, AES256, a 256-bit key, an MD5 that
matches -- and the object comes to rest under the key's MD5 and never
the key, which is no more than S3 itself echoes on responses.  SSE-C
has an observable half SSE-S3 does not: the object answers only to its
key.  A read presenting none or the wrong one is 400 -- the wrong one
too, which the suite pins where AWS's documentation suggests 403 -- and
every later request of a multipart upload, each part and the completion
alike, must present the create-time key again.  The completion response
echoes the algorithm and MD5 from the request it judged, the SDK's
response type modeling no field for them.

Reads are judged in the frontend rather than the store, whose getBlob
and blobMetadata also serve the frontend's own bookkeeping -- the stub
consulted before a completion, the ETag before a conditional write --
which reads without a key the way S3's own internals do.  The stores
vet what is always a caller's act: writes, parts, and the copy's
source, read inside copyBlob.  What both judge the same way lives in
CustomerKeys.

uploadMultipartPart takes the SDK's UploadPartRequest now, the one call
whose carrier had no room for a part's key, presented again on every
part as S3 requires.  The aws-s3 store forwarding the request instead
of rebuilding it also stops dropping the Content-MD5 a part arrived
with.

The encryption tag admitted whole brings the SSE-KMS conformance tests
with it, and they found two holes beside SSE-C.  The frontend refuses
what is malformed wherever it lands -- an algorithm S3 does not have, a
KMS key under an algorithm that does not name one -- because LocalStack
accepts both and the pass-through lane has to answer as S3 does; and it
refuses the write-side family on GET and HEAD, where an object's
encryption is reported, not asked for.  The transient store further
requires a key id with aws:kms, the way rgw does.  kms_keyid becomes an
ARN in the lane config for the same reason kms_keyid2 already was: that
is what a response carries.

Both lanes whose store encrypts run the full tag now and pass -- 659
tests on the transient store, 658 through LocalStack -- and the
filesystem lane still refuses the family whole, 501 on every header of
it, reporting no encryption it does not perform.  Unlike S3, S3Proxy
accepts SSE-C over plain HTTP, its TLS commonly terminating elsewhere.

See #402.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gaul

gaul commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

@gavin-thompson-postman FYI

@gaul
gaul merged commit 9e200e9 into master Aug 9, 2026
9 checks passed
@gaul
gaul deleted the sse-c branch August 9, 2026 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant