Files
at-container-registry/docs/ATPROTO_SIGNATURES.md
2025-10-31 21:03:33 -05:00

502 lines
17 KiB
Markdown

# ATProto Signatures for Container Images
## Overview
ATCR container images are **already cryptographically signed** through ATProto's repository commit system. Every manifest stored in a user's PDS is signed with the user's ATProto signing key, providing cryptographic proof of authorship and integrity.
This document explains:
- How ATProto signing works
- Why additional signing tools aren't needed
- How to bridge ATProto signatures to the OCI/ORAS ecosystem
- Trust model and security considerations
## Key Insight: Manifests Are Already Signed
When you push an image to ATCR:
```bash
docker push atcr.io/alice/myapp:latest
```
The following happens:
1. **AppView stores manifest** as an `io.atcr.manifest` record in alice's PDS
2. **PDS creates repository commit** containing the manifest record
3. **PDS signs the commit** with alice's ATProto signing key (ECDSA K-256)
4. **Signature is stored** in the repository commit object
**Result:** The manifest is cryptographically signed with alice's private key, and anyone can verify it using alice's public key from her DID document.
## ATProto Signing Mechanism
### Repository Commit Signing
ATProto uses a Merkle Search Tree (MST) to store records, and every modification creates a signed commit:
```
┌─────────────────────────────────────────────┐
│ Repository Commit │
├─────────────────────────────────────────────┤
│ DID: did:plc:alice123 │
│ Version: 3jzfkjqwdwa2a │
│ Previous: bafyreig7... (parent commit) │
│ Data CID: bafyreih8... (MST root) │
│ ┌───────────────────────────────────────┐ │
│ │ Signature (ECDSA K-256 + SHA-256) │ │
│ │ Signed with: alice's private key │ │
│ │ Value: 0x3045022100... (DER format) │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
┌─────────────────────┐
│ Merkle Search Tree │
│ (contains records) │
└─────────────────────┘
┌────────────────────────────┐
│ io.atcr.manifest record │
│ Repository: myapp │
│ Digest: sha256:abc123... │
│ Layers: [...] │
└────────────────────────────┘
```
### Signature Algorithm
**Algorithm:** ECDSA with K-256 (secp256k1) curve + SHA-256 hash
- **Curve:** secp256k1 (same as Bitcoin, Ethereum)
- **Hash:** SHA-256
- **Format:** DER-encoded signature bytes
- **Variant:** "low-S" signatures (per BIP-0062)
**Signing process:**
1. Serialize commit data as DAG-CBOR
2. Hash with SHA-256
3. Sign hash with ECDSA K-256 private key
4. Store signature in commit object
### Public Key Distribution
Public keys are distributed via DID documents, accessible through DID resolution:
**DID Resolution Flow:**
```
did:plc:alice123
Query PLC directory: https://plc.directory/did:plc:alice123
DID Document:
{
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:plc:alice123",
"verificationMethod": [{
"id": "did:plc:alice123#atproto",
"type": "Multikey",
"controller": "did:plc:alice123",
"publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z"
}],
"service": [{
"id": "#atproto_pds",
"type": "AtprotoPersonalDataServer",
"serviceEndpoint": "https://bsky.social"
}]
}
```
**Public key format:**
- **Encoding:** Multibase (base58btc with `z` prefix)
- **Codec:** Multicodec `0xE701` for K-256 keys
- **Example:** `zQ3sh...` decodes to 33-byte compressed public key
## Verification Process
To verify a manifest's signature:
### Step 1: Resolve Image to Manifest Digest
```bash
# Get manifest digest
DIGEST=$(crane digest atcr.io/alice/myapp:latest)
# Result: sha256:abc123...
```
### Step 2: Fetch Manifest Record from PDS
```bash
# Extract repository name from image reference
REPO="myapp"
# Query PDS for manifest record
curl "https://bsky.social/xrpc/com.atproto.repo.listRecords?\
repo=did:plc:alice123&\
collection=io.atcr.manifest&\
limit=100" | jq -r '.records[] | select(.value.digest == "sha256:abc123...")'
```
Response includes:
```json
{
"uri": "at://did:plc:alice123/io.atcr.manifest/abc123",
"cid": "bafyreig7...",
"value": {
"$type": "io.atcr.manifest",
"repository": "myapp",
"digest": "sha256:abc123...",
...
}
}
```
### Step 3: Fetch Repository Commit
```bash
# Get current repository state
curl "https://bsky.social/xrpc/com.atproto.sync.getRepo?\
did=did:plc:alice123" --output repo.car
# Extract commit from CAR file (requires ATProto tools)
# Commit includes signature over repository state
```
### Step 4: Resolve DID to Public Key
```bash
# Resolve DID document
curl "https://plc.directory/did:plc:alice123" | jq -r '.verificationMethod[0].publicKeyMultibase'
# Result: zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z
```
### Step 5: Verify Signature
```go
// Pseudocode for verification
import "github.com/bluesky-social/indigo/atproto/crypto"
// 1. Parse commit
commit := parseCommitFromCAR(repoCAR)
// 2. Extract signature bytes
signature := commit.Sig
// 3. Get bytes that were signed
bytesToVerify := commit.Unsigned().BytesForSigning()
// 4. Decode public key from multibase
pubKey := decodeMultibasePublicKey(publicKeyMultibase)
// 5. Verify ECDSA signature
valid := crypto.VerifySignature(pubKey, bytesToVerify, signature)
```
### Step 6: Verify Manifest Integrity
```bash
# Verify the manifest record's CID matches the content
# CID is content-addressed, so tampering changes the CID
```
## Bridging to OCI/ORAS Ecosystem
While ATProto signatures are cryptographically sound, the OCI ecosystem doesn't understand ATProto records. To make signatures discoverable, we create **ORAS signature artifacts** that reference the ATProto signature.
### ORAS Signature Artifact Format
```json
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.atproto.signature.v1+json",
"config": {
"mediaType": "application/vnd.oci.empty.v1+json",
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"size": 2
},
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:abc123...",
"size": 1234
},
"layers": [
{
"mediaType": "application/vnd.atproto.signature.v1+json",
"digest": "sha256:sig789...",
"size": 512,
"annotations": {
"org.opencontainers.image.title": "atproto-signature.json"
}
}
],
"annotations": {
"io.atcr.atproto.did": "did:plc:alice123",
"io.atcr.atproto.pds": "https://bsky.social",
"io.atcr.atproto.recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123",
"io.atcr.atproto.commitCid": "bafyreih8...",
"io.atcr.atproto.signedAt": "2025-10-31T12:34:56.789Z",
"io.atcr.atproto.keyId": "did:plc:alice123#atproto"
}
}
```
**Key elements:**
1. **artifactType**: `application/vnd.atproto.signature.v1+json` - identifies this as an ATProto signature
2. **subject**: Links to the image manifest being signed
3. **layers**: Contains signature metadata blob
4. **annotations**: Quick-access metadata for verification
### Signature Metadata Blob
The layer blob contains detailed verification information:
```json
{
"$type": "io.atcr.atproto.signature",
"version": "1.0",
"subject": {
"digest": "sha256:abc123...",
"mediaType": "application/vnd.oci.image.manifest.v1+json"
},
"atproto": {
"did": "did:plc:alice123",
"handle": "alice.bsky.social",
"pdsEndpoint": "https://bsky.social",
"recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123",
"recordCid": "bafyreig7...",
"commitCid": "bafyreih8...",
"commitRev": "3jzfkjqwdwa2a",
"signedAt": "2025-10-31T12:34:56.789Z"
},
"signature": {
"algorithm": "ECDSA-K256-SHA256",
"keyId": "did:plc:alice123#atproto",
"publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z"
},
"verification": {
"method": "atproto-repo-commit",
"instructions": "Fetch repository commit from PDS and verify signature using public key from DID document"
}
}
```
### Discovery via Referrers API
ORAS artifacts are discoverable via the OCI Referrers API:
```bash
# Query for signature artifacts
curl "https://atcr.io/v2/alice/myapp/referrers/sha256:abc123?\
artifactType=application/vnd.atproto.signature.v1+json"
```
Response:
```json
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:sig789...",
"size": 1234,
"artifactType": "application/vnd.atproto.signature.v1+json",
"annotations": {
"io.atcr.atproto.did": "did:plc:alice123",
"io.atcr.atproto.signedAt": "2025-10-31T12:34:56.789Z"
}
}
]
}
```
## Trust Model
### What ATProto Signatures Prove
**Authenticity**: Image was published by the DID owner
**Integrity**: Image manifest hasn't been tampered with since signing
**Non-repudiation**: Only the DID owner could have created this signature
**Timestamp**: When the image was signed (commit timestamp)
### What ATProto Signatures Don't Prove
**Safety**: Image doesn't contain vulnerabilities (use vulnerability scanning)
**DID trustworthiness**: Whether the DID owner is trustworthy (trust policy decision)
**Key security**: Private key wasn't compromised (same limitation as all PKI)
**PDS honesty**: PDS operator serves correct data (verify across multiple sources)
### Trust Dependencies
1. **DID Resolution**: Must correctly resolve DID to public key
- **Mitigation**: Use multiple resolvers, cache DID documents
2. **PDS Availability**: Must query PDS to verify signatures
- **Mitigation**: Embed signature bytes in ORAS blob for offline verification
3. **PDS Honesty**: PDS could serve fake/unsigned records
- **Mitigation**: Signature verification prevents this (can't forge signature)
4. **Key Security**: User's private key could be compromised
- **Mitigation**: Key rotation via DID document updates, short-lived credentials
5. **Algorithm Security**: ECDSA K-256 must remain secure
- **Status**: Well-studied, same as Bitcoin/Ethereum (widely trusted)
### Comparison with Other Signing Systems
| Aspect | ATProto Signatures | Cosign (Keyless) | Notary v2 |
|--------|-------------------|------------------|-----------|
| **Identity** | DID (decentralized) | OIDC (federated) | X.509 (PKI) |
| **Key Management** | PDS signing keys | Ephemeral (Fulcio) | User-managed |
| **Trust Anchor** | DID resolution | Fulcio CA + Rekor | Certificate chain |
| **Transparency Log** | ATProto firehose | Rekor | Optional |
| **Offline Verification** | Limited* | No | Yes |
| **Decentralization** | High | Medium | Low |
| **Complexity** | Low | High | Medium |
*Can be improved by embedding signature bytes in ORAS blob
### Security Considerations
**Threat: Man-in-the-Middle Attack**
- **Attack**: Intercept PDS queries, serve fake records
- **Defense**: TLS for PDS communication, verify signature with public key from DID document
- **Result**: Attacker can't forge signature without private key
**Threat: Compromised PDS**
- **Attack**: PDS operator serves unsigned/fake manifests
- **Defense**: Signature verification fails (PDS can't sign without user's private key)
- **Result**: Protected
**Threat: Key Compromise**
- **Attack**: Attacker steals user's ATProto signing key
- **Defense**: Key rotation via DID document, revoke old keys
- **Result**: Same as any PKI system (rotate keys quickly)
**Threat: Replay Attack**
- **Attack**: Replay old signed manifest to rollback to vulnerable version
- **Defense**: Check commit timestamp, verify commit is in current repository DAG
- **Result**: Protected (commits form immutable chain)
**Threat: DID Takeover**
- **Attack**: Attacker gains control of user's DID (rotation keys)
- **Defense**: Monitor DID document changes, verify key history
- **Result**: Serious but requires compromising rotation keys (harder than signing keys)
## Implementation Strategy
### Automatic Signature Artifact Creation
When AppView stores a manifest in a user's PDS:
1. **Store manifest record** (existing behavior)
2. **Get commit response** with commit CID and revision
3. **Create ORAS signature artifact**:
- Build metadata blob (JSON)
- Upload blob to hold storage
- Create ORAS manifest with subject = image manifest
- Store ORAS manifest (creates referrer link)
### Storage Location
Signature artifacts follow the same pattern as SBOMs:
- **Metadata blobs**: Stored in hold's blob storage
- **ORAS manifests**: Stored in hold's embedded PDS
- **Discovery**: Via OCI Referrers API
### Verification Tools
**Option 1: Custom CLI tool (`atcr-verify`)**
```bash
atcr-verify atcr.io/alice/myapp:latest
# → Queries referrers API
# → Fetches signature metadata
# → Resolves DID → public key
# → Queries PDS for commit
# → Verifies signature
```
**Option 2: Shell script (curl + jq)**
- See `docs/SIGNATURE_INTEGRATION.md` for examples
**Option 3: Kubernetes admission controller**
- Custom webhook that runs verification
- Rejects pods with unsigned/invalid signatures
## Benefits of ATProto Signatures
### Compared to No Signing
**Cryptographic proof** of image authorship
**Tamper detection** for manifests
**Identity binding** via DIDs
**Audit trail** via ATProto repository history
### Compared to Cosign/Notary
**No additional signing required** (already signed by PDS)
**Decentralized identity** (DIDs, not CAs)
**Simpler infrastructure** (no Fulcio, no Rekor, no TUF)
**Consistent with ATCR's architecture** (ATProto-native)
**Lower operational overhead** (reuse existing PDS infrastructure)
### Trade-offs
⚠️ **Custom verification tools required** (standard tools won't work)
⚠️ **Online verification preferred** (need to query PDS)
⚠️ **Different trust model** (trust DIDs, not CAs)
⚠️ **Ecosystem maturity** (newer approach, less tooling)
## Future Enhancements
### Short-term
1. **Offline verification**: Embed signature bytes in ORAS blob
2. **Multi-PDS verification**: Check signature across multiple PDSs
3. **Key rotation support**: Handle historical key validity
### Medium-term
4. **Timestamp service**: RFC 3161 timestamps for long-term validity
5. **Multi-signature**: Require N signatures from M DIDs
6. **Transparency log integration**: Record verifications in public log
### Long-term
7. **IANA registration**: Register `application/vnd.atproto.signature.v1+json`
8. **Standards proposal**: ATProto signature spec to ORAS/OCI
9. **Cross-ecosystem bridges**: Convert to Cosign/Notary formats
## Conclusion
ATCR images are already cryptographically signed through ATProto's repository commit system. By creating ORAS signature artifacts that reference these existing signatures, we can:
- ✅ Make signatures discoverable to OCI tooling
- ✅ Maintain ATProto as the source of truth
- ✅ Provide verification tools for users and clusters
- ✅ Avoid duplicating signing infrastructure
This approach leverages ATProto's strengths (decentralized identity, built-in signing) while bridging to the OCI ecosystem through standard ORAS artifacts.
## References
### ATProto Specifications
- [ATProto Repository Specification](https://atproto.com/specs/repository)
- [ATProto Data Model](https://atproto.com/specs/data-model)
- [ATProto DID Methods](https://atproto.com/specs/did)
### OCI/ORAS Specifications
- [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec)
- [OCI Referrers API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers)
- [ORAS Artifacts](https://oras.land/docs/)
### Cryptography
- [ECDSA (secp256k1)](https://en.bitcoin.it/wiki/Secp256k1)
- [Multibase Encoding](https://github.com/multiformats/multibase)
- [Multicodec](https://github.com/multiformats/multicodec)
### Related Documentation
- [SBOM Scanning](./SBOM_SCANNING.md) - Similar ORAS artifact pattern
- [Signature Integration](./SIGNATURE_INTEGRATION.md) - Practical integration examples