Two SSE-C Keys, One CopyObject Response
This note documents the CopyObject SSE-C checksum response repair committed as e73436c99 and shipped in Server 20260903. It covers the successful CopyObject XML and HTTP response after the destination object has committed; stored object bytes, checksum metadata, encryption formats, source decryption, federation, and replication remain unchanged.
Overview
An SSE-C CopyObject request can supply two independent customer-provided encryption keys:
| Role | Request Headers | Purpose |
|---|---|---|
| Source | X-Amz-Copy-Source-Server-Side-Encryption-Customer-* |
Decrypt the source object and its stored metadata |
| Destination | X-Amz-Server-Side-Encryption-Customer-* |
Encrypt and later interpret the committed destination object |
SILO encrypted the destination object and sealed its checksum metadata under the destination key. However, after the write committed, both the CopyObjectResult XML builder and the generic PUT-response header helper received the original CopyObject request headers. Because decryptChecksums prioritizes X-Amz-Copy-Source-Server-Side-Encryption-Customer-* headers whenever they are present—which is right while reading the source object—it attempted to unseal the committed destination’s checksum metadata using the source key.
With source key A and destination key B:
The stored destination object and its encrypted checksum metadata were valid, but the one-time CopyObject response omitted the checksum fields. The repair constructs a destination response-header view by stripping the three copy-source SSE-C headers, decrypts the committed destination checksum metadata once under key B, and reuses that map for both the CopyObjectResult XML body and the HTTP response headers.
Observable Failure
Triggering the bug required a checksum-bearing destination object and distinct source and destination SSE-C keys:
Before the repair:
CopyObjectsucceeded with HTTP 200.GetObjecton the destination with keyBreturned the expected plaintext body.HeadObjecton the destination with keyBdecrypted the stored checksum metadata and returnedx-amz-checksum-crc32andx-amz-checksum-type.- The
CopyObjectXML body and HTTP response headers omitted bothChecksumCRC32/x-amz-checksum-crc32andChecksumType/x-amz-checksum-type.
The same mismatch affected in-place SSE-C key rotation (copying an object onto itself with a new SSE-C key). Once the destination metadata was resealed under key B, the request still carried the old key A in the copy-source headers, causing response generation to fail checksum decryption.
Why the Global Decrypter Must Not Change
decryptChecksums prefers copy-source SSE-C headers for a valid reason: earlier in CopyObject, the server inspects the source object’s checksum metadata to decide whether to carry its algorithm forward, recompute a full-object digest from a composite source, or attach a default CRC64NVME checksum. When the source object is SSE-C encrypted, its checksum metadata is sealed under the source key and requires the X-Amz-Copy-Source-Server-Side-Encryption-Customer-* headers.
Inverting the global header priority inside decryptChecksums to prefer destination SSE-C headers would fix the post-commit response while breaking pre-commit source checksum inspection. The boundary is temporal and object-specific:
Implementation
Destination Response View
After the destination write commits, the handler clones the request header map and deletes the three copy-source SSE-C headers:
X-Amz-Copy-Source-Server-Side-Encryption-Customer-AlgorithmX-Amz-Copy-Source-Server-Side-Encryption-Customer-KeyX-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-MD5
Destination X-Amz-Server-Side-Encryption-Customer-* headers remain intact. SSE-S3 and SSE-KMS destination metadata require no customer header and continue through the existing path.
Decrypt Once, Project Twice
Previously, CopyObject called decryptChecksums once while constructing the CopyObjectResult XML struct and a second time inside setPutObjHeaders while writing HTTP response headers. For SSE-S3 or SSE-KMS objects, that duplicated KMS or master-key unseal work.
The repaired flow decrypts the destination checksum map once and projects it into both outputs:
The general setPutObjHeaders helper remains unchanged for PutObject, CompleteMultipartUpload, and DeleteObject, while CopyObject passes the pre-decrypted checksum map directly to the shared header writer.
Regression Matrix
The encryption and API test suites cover:
- Plaintext source to SSE-C destination.
- Compressed and uncompressed SSE-C destinations.
- SSE-C source key
Ato SSE-C destination keyB. - Verification of checksum digest and
ChecksumTypein bothCopyObjectResultXML and HTTP response headers. - Verification that stored destination checksums decrypt with key
Band match the destination body. - In-place same-object SSE-C key rotation from key
Ato keyB. SSE-S3source and destination combinations across erasure backends.
Compatibility and Operational Impact
- Successful
CopyObjectresponses: Checksum digest andChecksumTypefields that were previously omitted when copying into an SSE-C destination now appear in both the XML body and HTTP response headers. - Stored objects: No on-disk rewrite, metadata format change, or encryption format change is required; existing objects were already stored with valid destination checksum metadata.
- Performance: Checksum metadata is decrypted once per
CopyObjectresponse instead of twice, with no extra object read or hashing pass. - Rolling upgrades and rollback: Upgraded nodes return the checksum fields immediately while older nodes may still omit them during a rolling upgrade window; stored objects remain readable across both versions.
Conclusion
Decrypting ObjectInfo before building the CopyObjectResult XML payload (21870fa2e) restores the stored checksum fields on SSE-C and KMS copy responses without altering storage layout or encryption semantics.