A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
1// Package atproto implements a Ratify verifier plugin for ATProto signatures.
2package atproto
3
4import (
5 "context"
6 "encoding/json"
7 "fmt"
8 "time"
9
10 "github.com/ratify-project/ratify/pkg/common"
11 "github.com/ratify-project/ratify/pkg/ocispecs"
12 "github.com/ratify-project/ratify/pkg/referrerstore"
13 "github.com/ratify-project/ratify/pkg/verifier"
14)
15
16const (
17 // VerifierName is the name of this verifier
18 VerifierName = "atproto"
19
20 // VerifierType is the type of this verifier
21 VerifierType = "atproto"
22
23 // ATProtoSignatureArtifactType is the OCI artifact type for ATProto signatures
24 ATProtoSignatureArtifactType = "application/vnd.atproto.signature.v1+json"
25)
26
27// ATProtoVerifier implements the Ratify ReferenceVerifier interface for ATProto signatures.
28type ATProtoVerifier struct {
29 name string
30 config ATProtoConfig
31 resolver *Resolver
32 verifier *SignatureVerifier
33 trustStore *TrustStore
34}
35
36// ATProtoConfig holds configuration for the ATProto verifier.
37type ATProtoConfig struct {
38 // TrustPolicyPath is the path to the trust policy YAML file
39 TrustPolicyPath string `json:"trustPolicyPath"`
40
41 // DIDResolverTimeout is the timeout for DID resolution
42 DIDResolverTimeout time.Duration `json:"didResolverTimeout"`
43
44 // PDSTimeout is the timeout for PDS XRPC calls
45 PDSTimeout time.Duration `json:"pdsTimeout"`
46
47 // CacheEnabled enables caching of DID documents and public keys
48 CacheEnabled bool `json:"cacheEnabled"`
49
50 // CacheTTL is the cache TTL for DID documents and public keys
51 CacheTTL time.Duration `json:"cacheTTL"`
52}
53
54// ATProtoSignature represents the ATProto signature metadata stored in the OCI artifact.
55type ATProtoSignature struct {
56 Type string `json:"$type"`
57 Version string `json:"version"`
58 Subject struct {
59 Digest string `json:"digest"`
60 MediaType string `json:"mediaType"`
61 } `json:"subject"`
62 ATProto struct {
63 DID string `json:"did"`
64 Handle string `json:"handle"`
65 PDSEndpoint string `json:"pdsEndpoint"`
66 RecordURI string `json:"recordUri"`
67 CommitCID string `json:"commitCid"`
68 SignedAt time.Time `json:"signedAt"`
69 } `json:"atproto"`
70 Signature struct {
71 Algorithm string `json:"algorithm"`
72 KeyID string `json:"keyId"`
73 PublicKeyMultibase string `json:"publicKeyMultibase"`
74 } `json:"signature"`
75}
76
77// NewATProtoVerifier creates a new ATProto verifier instance.
78func NewATProtoVerifier(name string, config ATProtoConfig) (*ATProtoVerifier, error) {
79 // Load trust policy
80 trustStore, err := LoadTrustStore(config.TrustPolicyPath)
81 if err != nil {
82 return nil, fmt.Errorf("failed to load trust policy: %w", err)
83 }
84
85 // Create resolver with caching
86 resolver := NewResolver(config.DIDResolverTimeout, config.CacheEnabled, config.CacheTTL)
87
88 // Create signature verifier
89 verifier := NewSignatureVerifier(config.PDSTimeout)
90
91 return &ATProtoVerifier{
92 name: name,
93 config: config,
94 resolver: resolver,
95 verifier: verifier,
96 trustStore: trustStore,
97 }, nil
98}
99
100// Name returns the name of this verifier.
101func (v *ATProtoVerifier) Name() string {
102 return v.name
103}
104
105// Type returns the type of this verifier.
106func (v *ATProtoVerifier) Type() string {
107 return VerifierType
108}
109
110// CanVerify returns true if this verifier can verify the given artifact type.
111func (v *ATProtoVerifier) CanVerify(artifactType string) bool {
112 return artifactType == ATProtoSignatureArtifactType
113}
114
115// VerifyReference verifies an ATProto signature artifact.
116func (v *ATProtoVerifier) VerifyReference(
117 ctx context.Context,
118 subjectRef common.Reference,
119 referenceDesc ocispecs.ReferenceDescriptor,
120 store referrerstore.ReferrerStore,
121) (verifier.VerifierResult, error) {
122 // 1. Fetch signature blob from store
123 sigBlob, err := store.GetBlobContent(ctx, subjectRef, referenceDesc.Digest)
124 if err != nil {
125 return v.failureResult(fmt.Sprintf("failed to fetch signature blob: %v", err)), err
126 }
127
128 // 2. Parse ATProto signature metadata
129 var sigData ATProtoSignature
130 if err := json.Unmarshal(sigBlob, &sigData); err != nil {
131 return v.failureResult(fmt.Sprintf("failed to parse signature metadata: %v", err)), err
132 }
133
134 // Validate signature format
135 if err := v.validateSignature(&sigData); err != nil {
136 return v.failureResult(fmt.Sprintf("invalid signature format: %v", err)), err
137 }
138
139 // 3. Check trust policy first (fail fast if DID not trusted)
140 if !v.trustStore.IsTrusted(sigData.ATProto.DID, time.Now()) {
141 return v.failureResult(fmt.Sprintf("DID %s not in trusted list", sigData.ATProto.DID)),
142 fmt.Errorf("untrusted DID")
143 }
144
145 // 4. Resolve DID to public key
146 pubKey, err := v.resolver.ResolveDIDToPublicKey(ctx, sigData.ATProto.DID)
147 if err != nil {
148 return v.failureResult(fmt.Sprintf("failed to resolve DID: %v", err)), err
149 }
150
151 // 5. Fetch repository commit from PDS
152 commit, err := v.verifier.FetchCommit(ctx, sigData.ATProto.PDSEndpoint,
153 sigData.ATProto.DID, sigData.ATProto.CommitCID)
154 if err != nil {
155 return v.failureResult(fmt.Sprintf("failed to fetch commit: %v", err)), err
156 }
157
158 // 6. Verify K-256 signature
159 if err := v.verifier.VerifySignature(pubKey, commit); err != nil {
160 return v.failureResult(fmt.Sprintf("signature verification failed: %v", err)), err
161 }
162
163 // 7. Success - return detailed result
164 return verifier.VerifierResult{
165 IsSuccess: true,
166 Name: v.name,
167 Type: v.Type(),
168 Message: fmt.Sprintf("Successfully verified ATProto signature for DID %s", sigData.ATProto.DID),
169 Extensions: map[string]interface{}{
170 "did": sigData.ATProto.DID,
171 "handle": sigData.ATProto.Handle,
172 "signedAt": sigData.ATProto.SignedAt,
173 "commitCid": sigData.ATProto.CommitCID,
174 "pdsEndpoint": sigData.ATProto.PDSEndpoint,
175 },
176 }, nil
177}
178
179// validateSignature validates the signature metadata format.
180func (v *ATProtoVerifier) validateSignature(sig *ATProtoSignature) error {
181 if sig.Type != "io.atcr.atproto.signature" {
182 return fmt.Errorf("invalid signature type: %s", sig.Type)
183 }
184 if sig.ATProto.DID == "" {
185 return fmt.Errorf("missing DID")
186 }
187 if sig.ATProto.PDSEndpoint == "" {
188 return fmt.Errorf("missing PDS endpoint")
189 }
190 if sig.ATProto.CommitCID == "" {
191 return fmt.Errorf("missing commit CID")
192 }
193 if sig.Signature.Algorithm != "ECDSA-K256-SHA256" {
194 return fmt.Errorf("unsupported signature algorithm: %s", sig.Signature.Algorithm)
195 }
196 return nil
197}
198
199// failureResult creates a failure result with the given message.
200func (v *ATProtoVerifier) failureResult(message string) verifier.VerifierResult {
201 return verifier.VerifierResult{
202 IsSuccess: false,
203 Name: v.name,
204 Type: v.Type(),
205 Message: message,
206 Extensions: map[string]interface{}{
207 "error": message,
208 },
209 }
210}
211
212// TODO: Implement resolver.go with DID resolution logic
213// TODO: Implement crypto.go with K-256 signature verification
214// TODO: Implement config.go with trust policy loading