# objectstore.sigv4

> A module of objectstore's API.

`objectstore.sigv4` — AWS Signature Version 4, header and query flavours.

Split out of `s3` because it is pure string algebra over `crypto`: given a
method, a path, query parameters, headers and a payload hash it produces the
canonical request, the string to sign, and the signature. That shape is what
makes it testable against the published `aws-sig-v4-test-suite` vectors
without a network or an S3 server anywhere in the picture.

Two callers use it: `S3Client`, which signs with an `Authorization` header,
and `presign_url`, which moves the same material into the query string so a
URL can be handed to something that knows nothing about AWS.

## Structs

### `QueryParam`

```mojo
struct QueryParam
```

One query parameter, **unencoded**. Signing encodes it.

Implements: `Copyable`, `Movable`

Fields:

- `name: String`
- `value: String`

### `AmzTime`

```mojo
struct AmzTime
```

A signing timestamp in the two forms SigV4 needs.

Implements: `Copyable`, `Movable`

Fields:

- `amz_date: String`: `20150830T123600Z` — the `X-Amz-Date` header value.
- `date_stamp: String`: `20150830` — the date half of the credential scope.

#### `from_epoch`

```mojo
def from_epoch(epoch: Int) -> Self
```

UTC calendar date from a Unix timestamp.

Days-to-civil from Howard Hinnant's `chrono`-compatible algorithm: it
shifts the era so that leap days land at the end, which makes the
whole conversion branch-free integer arithmetic. Mojo has no calendar
library, and re-deriving this is cheaper than another FFI call.

#### `now`

```mojo
def now() -> Self
```

#### `from_amz_date`

```mojo
def from_amz_date(amz_date: String) -> Self
```

Reuses an `X-Amz-Date` value the caller already has (the test vectors, or a retried request that must keep its timestamp).

### `SignedRequest`

```mojo
struct SignedRequest
```

Everything a signing pass produced, so tests can check each stage.

Implements: `Copyable`, `Movable`

Fields:

- `canonical_request: String`
- `string_to_sign: String`
- `signature: String`
- `signed_headers: String`
- `authorization: String`

## Functions

### `epoch_now`

```mojo
def epoch_now() -> Int
```

Seconds since the Unix epoch, via the FFI shim.

Mojo's `std.time` offers only a monotonic clock, and SigV4 needs a real
UTC timestamp — AWS rejects requests more than 15 minutes out of skew.

### `canonical_path`

```mojo
def canonical_path(path: String, normalize: Bool = False) -> String
```

The canonical URI: each segment percent-encoded once, `/` left alone.

`normalize` collapses `.`/`..` and duplicate slashes, which is what every
AWS service except S3 wants. **S3 signs the un-normalized path** — an S3
key may legitimately contain a literal `..` segment — so the S3 client
leaves this off.

### `canonical_query`

```mojo
def canonical_query(params: List[QueryParam]) -> String
```

Encoded `k=v` pairs sorted by encoded name, then encoded value.

### `canonical_headers`

```mojo
def canonical_headers(headers: List[Header]) -> Tuple[String, String]
```

Returns `(canonical header block, signed header list)`.

Names are lowercased and sorted; repeated names are joined with commas in
the order they appeared, per the SigV4 spec.

### `canonical_request`

```mojo
def canonical_request(method: String, path: String, params: List[QueryParam], headers: List[Header], payload_hash: String, normalize: Bool = False) -> Tuple[String, String]
```

Returns `(canonical request, signed headers)`.

### `credential_scope`

```mojo
def credential_scope(date_stamp: String, region: String, service: String) -> String
```

### `string_to_sign`

```mojo
def string_to_sign(amz_date: String, scope: String, canonical_req: String) -> String
```

### `signing_key`

```mojo
def signing_key(secret_key: String, date_stamp: String, region: String, service: String) -> List[UInt8]
```

The four-step HMAC ladder: date, region, service, `aws4_request`.

### `sign`

```mojo
def sign(secret_key: String, date_stamp: String, region: String, service: String, to_sign: String) -> String
```

### `sign_request`

```mojo
def sign_request(method: String, path: String, params: List[QueryParam], headers: List[Header], payload_hash: String, access_key: String, secret_key: String, region: String, service: String, when: AmzTime, normalize: Bool = False) -> SignedRequest
```

Header-flavoured SigV4.

`headers` must already contain `host`, `x-amz-date` and, when a session
token is in play, `x-amz-security-token`: which headers are signed is the
caller's decision, and S3 in particular insists that
`x-amz-content-sha256` be among them.

### `presign_query`

```mojo
def presign_query(method: String, path: String, params: List[QueryParam], headers: List[Header], access_key: String, secret_key: String, session_token: String, region: String, service: String, when: AmzTime, expires_seconds: Int, normalize: Bool = False) -> String
```

Query-flavoured SigV4: returns the full canonical query string, `X-Amz-Signature` included.

The payload hash for a presigned URL is `UNSIGNED-PAYLOAD`: the point of
presigning is that whoever follows the URL never sees the credentials, so
they cannot be expected to hash a body the signer never had.

## Aliases

### `ALGORITHM`

```mojo
comptime ALGORITHM = String("AWS4-HMAC-SHA256")
```

### `UNSIGNED_PAYLOAD`

```mojo
comptime UNSIGNED_PAYLOAD = String("UNSIGNED-PAYLOAD")
```

### `EMPTY_SHA256`

```mojo
comptime EMPTY_SHA256 = String("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
```

SHA-256 of the empty string — the payload hash of every bodyless request.
