This page documents the S3 surface Peregrine’s swift-s3api middleware
actually implements, at the behavior level a client sees. Scope discipline:
everything here is IMPLEMENTED_SUBSET_ONLY, verified against the Python
Swift 2.33 s3api oracle by the strict dual-oracle harness
(49/8 as of 2026-08-16, docs/fairness-lab/S3-ALIGN-20260815.md). It is not
“full S3”, and it is not a production compatibility promise.
Authentication
| Scheme | Support | Notes |
|---|---|---|
SigV4 header (Authorization: AWS4-HMAC-SHA256 …) |
yes | canonical request + signed headers verified |
SigV4 presigned query (X-Amz-Signature=…) |
yes | expiry enforced (below) |
| SigV2 header (HMAC-SHA1) | yes | AWS test-vector verified |
| SigV2 presigned query | yes | Expires enforced |
aws-chunked / STREAMING-AWS4-HMAC-SHA256-PAYLOAD |
yes | per-chunk HMAC and trailer signature enforced; bad chunk or trailer signature fails the upload |
UNSIGNED-PAYLOAD |
yes | header-signed requests with unsigned body |
| s3token / EC2 credentials → Keystone | yes (lab) | see the parity matrix |
Time-based rejection is real, not a dead config knob:
| Condition | Response |
|---|---|
| SigV4/SigV2 clock skew beyond the allowed window | 403 RequestTimeTooSkewed |
| Presigned URL past its expiry | 403 AccessDenied (“Request has expired”) |
Unknown x-amz-storage-class on PUT/copy |
400 InvalidStorageClass, rejected before any persist |
Bucket operations
| Operation | Status | Behavior notes |
|---|---|---|
PUT /bucket (create) |
yes | answers Location: /<bucket>; invalid names → InvalidBucketName before any Swift routing |
DELETE /bucket |
yes | non-empty → BucketNotEmpty |
HEAD /bucket |
yes | |
GET / (ListBuckets) |
yes | the only account-level success path |
PUT / DELETE / POST / HEAD / |
405 live | Python-aligned 405 MethodNotAllowed (ResourceType=SERVICE) on Guard. Supplement Section B 3/3. Do not treat Gate rollback copies as safe for these verbs |
| ListObjects v1 / v2 | yes | list-type=2 continuation semantics |
| GetBucketLocation | yes | answered locally from location config, no Swift hop |
Bucket ACL (canned + x-amz-grant-* + ACP XML) |
yes | ?acl GET/PUT |
Bucket versioning ?versioning |
yes | Enabled / Suspended, see versioning |
ListObjectVersions ?versions |
yes | pagination; Size semantics in versioning |
Bucket tagging ?tagging |
read: yes · write: Rust extra | Python 2.33 answers 501 on write |
Bucket CORS ?cors |
Rust extra | GET/PUT/DELETE (200/200/204); Python 2.33 answers 501 |
Lifecycle ?lifecycle |
yes (unit level) | Expiration + Transition metadata + AbortIncompleteMultipartUpload; outside the 57-case suite |
Object Lock config ?object-lock |
yes (unit level) | validated XML stored on protected container sysmeta; outside the 57-case suite |
Unsupported subresources (?policy, ?website, ?inventory, …) |
no | explicit 501 NotImplemented |
Object operations
| Operation | Status | Behavior notes |
|---|---|---|
| PUT / GET / HEAD / DELETE object | yes | streaming bodies, no full-object buffering |
| Range and conditional requests | yes | |
CopyObject (x-amz-copy-source) |
yes | LastModified in the result XML is ISO …T…Z, never HTTP-date |
Multi-delete (POST /bucket?delete) |
yes | per-key <Deleted> / <Error> result; missing keys are errors, not silent deletes |
Object ACL ?acl |
read: yes · write: Rust extra | Python 2.33 answers 501 on write |
Object tagging ?tagging |
read: yes · delete: Rust extra | |
| Legal hold / retention / lock | yes (unit + 2026-08-14 live canaries) | see WORM — bucket lock config is required first (400 InvalidRequest otherwise) |
RestoreObject ?restore |
honest reject | cold backend disabled → 400 InvalidObjectState; with a lab backend → 202 and a restore window |
Versioned GET/DELETE (?versionId=) |
yes | null and 32-hex ids; malformed id → 400 InvalidArgument |
Multipart upload
The full MPU path is implemented and dual-oracle covered: initiate, upload part, list parts, list uploads, complete, abort.
- Composite ETag is AWS-shaped:
MD5(part-MD5s)-N(unit vector"b4b77f5320cfe9ce9c0c70c35e84d511-2"). - ListMultipartUploads requires
GET; other verbs on?uploadsanswer405 MethodNotAllowed. - Part storage uses the
+segmentscompanion container, compatible with Swift’s SLO layout.
Error responses
Error XML is aligned with the Python 2.33 oracle shape:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>NoSuchKey</Code>
<Message>The specified key does not exist.</Message>
<RequestId>tx000000000000000000005-0068a01c22</RequestId>
<Key>missing.txt</Key>
</Error>RequestIdistx{21 hex}-{10-hex unix time}, present on every error.- Timestamps in listing/result XML are
YYYY-MM-DDTHH:MM:SS.000Z. - Common codes:
NoSuchBucket,NoSuchKey,NoSuchVersion,InvalidBucketName,InvalidArgument,InvalidStorageClass,RequestTimeTooSkewed,AccessDenied,BucketNotEmpty,MalformedXML,InvalidRequest,InvalidObjectState,MethodNotAllowed,NotImplemented,InternalError.
The Rust-ahead extras
Seven surfaces are implemented in Rust that stock Python 2.33 s3api answers
501 to. Policy (2026-08-15, docs/fairness-lab/S3-ALIGN-20260815.md): they
stay implemented — they are never downgraded to 501 to mint dual-oracle
PASSes, so they are permanent, explained FAILs in the strict runner:
| Case | Python 2.33 | Rust |
|---|---|---|
put-object-acl-not-implemented |
501 | 200 |
put-bucket-tagging-not-implemented |
501 | 200 |
delete-object-tagging-not-implemented |
501 | 204 |
get-bucket-cors-not-implemented |
501 | 200 (CORS XML) |
put-bucket-cors-not-implemented |
501 | 200 |
delete-bucket-cors-not-implemented |
501 | 204 |
restore-not-implemented |
501 | 400 InvalidObjectState (honest reject while cold is off) |
Anonymous and unsigned requests
- Unsigned S3-shaped reads can map to a configured
anonymous_account(GET/HEAD only) so container.r:*ACLs can serve public reads — off unless configured. Everything else unsigned answers403 AccessDenied. - Unsigned
GET /is only ListBuckets-shaped when it matches the S3 form; a bare unsigned/bucketprobe on the Python oracle is412 NotS3Request, and the harness signs existence gates accordingly.
Not implemented / not claimable
- Physical cold-tier storage-policy routing (the proxy does not load the cold policy map in the fleet build; the LocalDir backend is a lab prototype, default off — details).
- Runtime-wired multi-tenant IAM policy enforcement (library-level only).
- Bucket policy, website hosting, inventory, analytics, replication config,
and every other subresource not listed above:
501 NotImplemented. - Hosted-AWS behaviors that Swift itself does not have.
Full strict matrix: Rust vs Python parity · methodology and current scoreboard: dual-oracle.