mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 08:30:29 +00:00
502 lines
17 KiB
Markdown
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
|