# Signed envelope authentication (X-Nukez-Envelope / X-Nukez-Signature)

Signed envelopes are Nukez's request-bound authentication scheme.

Instead of a bearer token, you sign a **small JSON envelope** that binds:

- who you are (payer key),
- *what* you're doing (method + path),
- *which* locker/receipt it applies to,
- *and* the hash of the exact request body.

The server verifies the signature and checks that the envelope is fresh.

## Signature Algorithms

Envelope auth supports two signature algorithms depending on the wallet type:

- **Ed25519** -- used for Solana wallets. Signatures are base58-encoded.
- **secp256k1** -- used for EVM wallets (Monad, Ethereum, etc.). Signatures are hex-encoded.

The `sig_alg` field in the envelope specifies which algorithm was used. The server auto-detects based on the payer key format if `sig_alg` is omitted, but explicit is preferred.

---

## Quick Start: Making a Signed Request

If you have a `build_signed_envelope` tool/function available, use it! Example flow:

```
1. Call build_signed_envelope with:
   - receipt_id: "556db5b5bec924ea"
   - method: "POST"
   - path: "/v1/storage/signed_provision"
   - ops: ["locker:provision"]
   - body: {"receipt_id": "556db5b5bec924ea", "tags": []}

2. You receive:
   - headers: {"X-Nukez-Envelope": "...", "X-Nukez-Signature": "...", "Content-Type": "application/json"}
   - body_canonical: '{"receipt_id":"556db5b5bec924ea","tags":[]}'
   - locker_id: "locker_xxxxxxxxxxxx"

3. Make HTTP request with those headers and body_canonical as the raw body
```

If you need to construct the envelope manually, see the detailed steps below.

---

## Headers

- `X-Nukez-Envelope`: base64url-encoded canonical JSON envelope (no padding)
- `X-Nukez-Signature`: base58-encoded Ed25519 signature (Solana) or hex-encoded secp256k1 signature (EVM) over the **envelope bytes**

## Envelope JSON schema

```json
{
  "v": 1,
  "locker_id": "locker_48039d44f06a",
  "receipt_id": "01b1c58b9e6f2f01",
  "nonce": "2f4b0e5f9cfe4c1c9f0d0b2a5e7f1122",
  "iat": 1737316800,
  "exp": 1737316860,
  "ops": ["locker:provision"],
  "method": "POST",
  "path": "/v1/storage/signed_provision",
  "body_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "sig_alg": "ed25519"
}
```

Field notes:

- `iat` and `exp` are UNIX seconds.
- Keep `exp - iat` short (<= 60 seconds is a good norm).
- `ops` must include the operation(s) required by the endpoint (see mapping below).
- `body_sha256` is the SHA-256 hex of the **exact request body bytes**.
  - For an empty body, it is SHA-256 of empty string.
  - If you send JSON, hash the bytes you actually send on the wire.
- `sig_alg` specifies the signing algorithm: `"ed25519"` for Solana wallets, `"secp256k1"` for EVM wallets.

## Deterministic locker_id

Locker IDs are derived from the receipt_id:

```
locker_id = "locker_" + sha256_hex(receipt_id)[:12]
```

This lets agents compute the locker id without extra lookups.

## Required ops mapping

These ops appear in `x-nukez-ops` in OpenAPI, and should be copied into `envelope.ops`.

| Endpoint | HTTP Method | Required ops |
|----------|-------------|--------------|
| `/v1/storage/signed_provision` | POST | `locker:provision` |
| `/v1/lockers/{locker_id}/files` | POST | `locker:write` |
| `/v1/lockers/{locker_id}/files` | GET | `locker:list` |
| `/v1/lockers/{locker_id}/files/{filename}` | GET | `locker:read` |
| `/v1/lockers/{locker_id}/files/{filename}` | DELETE | `locker:write` |
| `/v1/lockers/{locker_id}/record` | GET | `locker:read` |
| `/v1/lockers/{locker_id}/urls` | GET | `locker:urls` |
| `/v1/lockers/{locker_id}/operators` | POST | `locker:admin` |
| `/v1/lockers/{locker_id}/operators/{pubkey}` | DELETE | `locker:admin` |
| `/v1/lockers/{locker_id}/manifest` | GET | `locker:read` |
| `/v1/files/fetch-and-store` | POST | `locker:write` |
| `/v1/files/confirm` | POST | `locker:write` |
| `/v1/storage/attest` | POST | `locker:attest` |
| `/v1/attest/push` | POST | `locker:attest` |

---

**Note on attestation endpoints (`locker:attest`):**

`/v1/storage/attest` and `/v1/attest/push` are payer-signed: only the receipt's payer key may issue `locker:attest`. The envelope's `receipt_id` must equal the request's `receipt_id` — the JSON body for `/v1/attest/push`, the query parameter for `/v1/storage/attest`. A mismatch returns `401 RECEIPT_ID_MISMATCH`.

---

**Note on GET and DELETE requests:**

For requests with no body (GET, DELETE), use the SHA-256 hash of an empty string:
```
body_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
```

The envelope headers are still required - signed envelope auth applies to ALL authenticated endpoints, not just POST.

## Complete Example: Provisioning a Locker

After `confirm_storage` returns `receipt_id: "556db5b5bec924ea"`:

### Step 1: Compute locker_id
```python
import hashlib
locker_id = "locker_" + hashlib.sha256("556db5b5bec924ea".encode()).hexdigest()[:12]
# Result: "locker_xxxxxx..."
```

### Step 2: Build request body
```python
body = {"receipt_id": "556db5b5bec924ea", "tags": []}
```

### Step 3: Serialize to canonical JSON
```python
import json
body_canonical = json.dumps(body, sort_keys=True, separators=(",", ":"))
# Result: '{"receipt_id":"556db5b5bec924ea","tags":[]}'
```

### Step 4: Compute body hash
```python
body_sha256 = hashlib.sha256(body_canonical.encode()).hexdigest()
```

### Step 5: Build envelope
```python
import time, os
envelope = {
    "v": 1,
    "locker_id": locker_id,
    "receipt_id": "556db5b5bec924ea",
    "nonce": os.urandom(16).hex(),
    "iat": int(time.time()),
    "exp": int(time.time()) + 60,
    "ops": ["locker:provision"],
    "method": "POST",
    "path": "/v1/storage/signed_provision",
    "body_sha256": body_sha256,
    "sig_alg": "ed25519",
}
```

### Step 6: Encode and sign
```python
import base64
from nacl.signing import SigningKey
import base58

env_bytes = json.dumps(envelope, sort_keys=True, separators=(",", ":")).encode()
x_nukez_envelope = base64.urlsafe_b64encode(env_bytes).decode().rstrip("=")

sig = signing_key.sign(env_bytes).signature
x_nukez_signature = base58.b58encode(sig).decode()
```

### Step 7: Make the request
```python
import requests
response = requests.post(
    "https://api.nukez.xyz/v1/storage/signed_provision",
    headers={
        "X-Nukez-Envelope": x_nukez_envelope,
        "X-Nukez-Signature": x_nukez_signature,
        "Content-Type": "application/json",
    },
    data=body_canonical,  # Use the EXACT canonical string, not json=body
)
```

---

## Canonical JSON rules (critical)

When serializing the envelope AND the request body:

- Sort keys alphabetically
- No whitespace between elements
- UTF-8 bytes

In Python, this canonical form is:

```python
json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
```

If you use a library that inserts spaces/newlines or reorders keys, signatures will fail.

---

## Common failure modes

| Error | Cause | Fix |
|-------|-------|-----|
| `SIGNATURE_INVALID` / 403 | Wrong key, wrong canonical JSON, wrong encoding | Log exact bytes signed, verify key matches payer |
| `BODY_HASH_MISMATCH` | body_sha256 doesn't match actual body | Use canonical JSON for BOTH hash and request body |
| `MISSING_OPS` | `ops` doesn't include required operation | Check endpoint's required_ops in tools.json |
| `EXPIRED_ENVELOPE` | `exp` is in the past | Check clock sync, use fresh timestamp |

If you're debugging, start by logging the exact bytes you hashed and signed.

---

## Agent Implementation Notes

If your agent framework provides a `build_signed_envelope` tool:
1. Use it instead of implementing the above manually
2. Pass the body as a JSON object; the tool handles canonicalization
3. Use the returned `body_canonical` as the HTTP request body (not your original object)
4. Use the returned `headers` dict directly in your HTTP request

If implementing manually:
1. Always serialize body ONCE using canonical JSON
2. Hash that exact string for body_sha256
3. Send that exact string as the HTTP body
4. The #1 cause of failures is computing the hash from one string but sending a different string
