# Research: MailioReceipt v1

**Date**: 2026-02-04
**Branch**: `001-mailio-receipt-v1`

## Research Tasks

This document consolidates research findings for technical decisions required before
implementation.

---

## 1. RFC8785 JSON Canonicalization Library

**Context**: FR-002 and FR-024 require RFC8785 canonicalization for deterministic digests
and signatures.

### Decision: Use `canonicaljson` package

**Rationale**:
- Pure Python implementation of RFC8785 (JSON Canonicalization Scheme)
- Well-maintained, minimal dependencies
- Handles Unicode normalization, number formatting, and key sorting per spec
- Used in production by identity/credential systems

**Alternatives Considered**:

| Option | Pros | Cons | Rejected Because |
|--------|------|------|------------------|
| `json-canonicalization` | Also RFC8785 compliant | Less mature, fewer downloads | Lower adoption |
| Manual implementation | No dependency | Error-prone, maintenance burden | Violates Simplicity principle |
| `orjson` + custom sort | Fast | Not RFC8785 compliant for floats/Unicode | Non-compliant |

**Implementation Note**: Import as `from canonicaljson import encode_canonical_json`.
Returns bytes; decode to string if needed for JWS payload.

---

## 2. JWS Signature Library

**Context**: FR-003 requires detached JWS signatures in compact serialization. FR-004
mandates ES256 (ECDSA P-256 + SHA-256).

### Decision: Use `cryptography` directly (no `jwcrypto`)

**Rationale**:
- Constitution mandates `cryptography` library
- JWS compact format is simple: `base64url(header).base64url(payload).base64url(signature)`
- Detached JWS omits payload: `base64url(header)..base64url(signature)`
- Avoids extra dependency; `jwcrypto` wraps `cryptography` anyway
- Full control over header construction (important for `kid` matching)

**Alternatives Considered**:

| Option | Pros | Cons | Rejected Because |
|--------|------|------|------------------|
| `jwcrypto` | High-level JWS API | Extra dependency, less control | Constitution prefers minimal deps |
| `python-jose` | Popular | Uses `pycryptodome` by default | Constitution mandates `cryptography` |
| `authlib` | Full OAuth suite | Overkill for JWS-only use | Violates Simplicity principle |

**Implementation Note**: Create helper functions:
- `create_jws_detached(payload: bytes, private_key, key_id: str) -> str`
- `verify_jws_detached(jws: str, payload: bytes, public_key) -> bool`

---

## 3. DID:web Resolution

**Context**: FR-027 and FR-028 require DID:web resolution to fetch public keys for
signature verification.

### Decision: Use `httpx` for HTTP fetching, manual DID document parsing

**Rationale**:
- DID:web resolution is simple: convert DID to HTTPS URL, fetch JSON document
- No mature Python DID library worth the dependency weight
- `httpx` provides async support for future optimization
- DID document structure is well-defined; parsing is straightforward

**DID:web URL Resolution Algorithm**:
```
did:web:example.com           → https://example.com/.well-known/did.json
did:web:example.com:user:alice → https://example.com/user/alice/did.json
did:web:example.com%3A8443    → https://example.com:8443/.well-known/did.json
```

**Alternatives Considered**:

| Option | Pros | Cons | Rejected Because |
|--------|------|------|------------------|
| `did-resolver` (npm) | Mature | Wrong language | N/A for Python |
| `pyld` | JSON-LD processing | Overkill, complex | Violates Simplicity |
| `requests` | Simple, synchronous | No async, less modern | `httpx` more flexible |

**Implementation Note**:
- Cache resolved DID documents with configurable TTL (default: 5 minutes)
- Handle HTTP errors with retryable error types
- Extract `verificationMethod` array, match by `id` (key ID from JWS header)

---

## 4. Event ID Generation

**Context**: FR-005 requires globally unique event `id`.

### Decision: Use UUID v4 (random)

**Rationale**:
- Standard, well-understood format
- No coordination required between systems
- Python stdlib: `uuid.uuid4()`
- Sufficient entropy for uniqueness (122 random bits)

**Alternatives Considered**:

| Option | Pros | Cons | Rejected Because |
|--------|------|------|------------------|
| UUID v7 | Time-ordered | Not in Python stdlib yet | Would need `uuid7` package |
| ULID | Sortable, compact | Extra dependency | Simplicity principle |
| Snowflake | Ordered, compact | Requires coordination | Adds complexity |

**Format**: String representation with hyphens: `550e8400-e29b-41d4-a716-446655440000`

---

## 5. Timestamp Format

**Context**: FR-005 requires RFC3339 timestamps.

### Decision: Use `datetime.isoformat()` with UTC timezone

**Rationale**:
- Python's `datetime.isoformat()` produces RFC3339-compliant output
- Always use UTC to avoid timezone ambiguity
- Include microseconds for ordering precision

**Implementation Note**:
```python
from datetime import datetime, timezone
ts = datetime.now(timezone.utc).isoformat()
# Output: "2026-02-04T15:30:00.123456+00:00"
```

---

## 6. Digest Algorithm

**Context**: FR-002 specifies SHA-256 for event digests.

### Decision: Use `hashlib.sha256()` with hex encoding

**Rationale**:
- Python stdlib, no additional dependency
- SHA-256 is industry standard, secure for integrity verification
- Hex encoding is human-readable and URL-safe

**Implementation Note**:
```python
import hashlib
digest = hashlib.sha256(canonical_bytes).hexdigest()
# Output: "a1b2c3d4..." (64 hex characters)
```

---

## 7. Error Handling Strategy

**Context**: FR-029 requires clear, actionable errors for various failure modes.

### Decision: Custom exception hierarchy

**Rationale**:
- Allows callers to catch specific error types
- Each exception includes relevant context (e.g., which signature failed)
- Follows Python conventions (inherit from base exception)

**Exception Hierarchy**:
```
MailioReceiptError (base)
├── SignatureError
│   ├── InvalidSignatureError
│   └── SignerNotFoundError
├── ChainError
│   ├── ChainIntegrityError
│   └── MissingPredecessorError
├── DIDResolutionError
│   ├── DIDNotFoundError
│   └── DIDNetworkError (retryable)
├── ExecutionBlockedError
│   ├── RevocationError
│   ├── ExpiredApprovalError
│   └── BindingMismatchError
└── ValidationError
```

---

## 8. Public API Design

**Context**: SC-001 requires single function call for receipt creation with clear, typed API.

### Decision: Dataclass-based models with factory functions

**Rationale**:
- Python dataclasses provide type hints, immutability (`frozen=True`), and validation
- Factory functions allow sensible defaults (e.g., auto-generate `id`, `ts`)
- Flat API: `create_receipt()`, `verify_receipt()`, `check_execution()`

**API Surface**:
```python
# Creation
receipt = create_receipt(
    event=event,  # Required
    signer=signer,  # Required: PrivateKey + DID
    chain=prev_receipt,  # Optional
)

# Verification
result = verify_receipt(receipt, resolver=did_resolver)

# Execution check
decision = check_execution(receipt_chain, decision_time=datetime.now(timezone.utc))
```

---

## Summary of Dependencies

| Package | Version | Purpose | Constitution Alignment |
|---------|---------|---------|------------------------|
| `cryptography` | ≥41.0 | ES256 signing/verification | ✅ Mandated |
| `httpx` | ≥0.25 | DID:web HTTP fetching | ✅ Minimal, modern |
| `canonicaljson` | ≥2.0 | RFC8785 canonicalization | ✅ Essential for spec compliance |

**Dev Dependencies**:
| Package | Purpose |
|---------|---------|
| `pytest` | Testing (mandated by constitution) |
| `pytest-asyncio` | Async test support for DID resolution |
| `respx` | Mock HTTP for DID resolver tests |
| `ruff` | Linting and formatting (per constitution) |
| `mypy` | Type checking |
