Files
2025-11-01 11:08:53 -05:00

215 lines
6.7 KiB
Plaintext

// Package atproto implements a Ratify verifier plugin for ATProto signatures.
package atproto
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/ratify-project/ratify/pkg/common"
"github.com/ratify-project/ratify/pkg/ocispecs"
"github.com/ratify-project/ratify/pkg/referrerstore"
"github.com/ratify-project/ratify/pkg/verifier"
)
const (
// VerifierName is the name of this verifier
VerifierName = "atproto"
// VerifierType is the type of this verifier
VerifierType = "atproto"
// ATProtoSignatureArtifactType is the OCI artifact type for ATProto signatures
ATProtoSignatureArtifactType = "application/vnd.atproto.signature.v1+json"
)
// ATProtoVerifier implements the Ratify ReferenceVerifier interface for ATProto signatures.
type ATProtoVerifier struct {
name string
config ATProtoConfig
resolver *Resolver
verifier *SignatureVerifier
trustStore *TrustStore
}
// ATProtoConfig holds configuration for the ATProto verifier.
type ATProtoConfig struct {
// TrustPolicyPath is the path to the trust policy YAML file
TrustPolicyPath string `json:"trustPolicyPath"`
// DIDResolverTimeout is the timeout for DID resolution
DIDResolverTimeout time.Duration `json:"didResolverTimeout"`
// PDSTimeout is the timeout for PDS XRPC calls
PDSTimeout time.Duration `json:"pdsTimeout"`
// CacheEnabled enables caching of DID documents and public keys
CacheEnabled bool `json:"cacheEnabled"`
// CacheTTL is the cache TTL for DID documents and public keys
CacheTTL time.Duration `json:"cacheTTL"`
}
// ATProtoSignature represents the ATProto signature metadata stored in the OCI artifact.
type ATProtoSignature struct {
Type string `json:"$type"`
Version string `json:"version"`
Subject struct {
Digest string `json:"digest"`
MediaType string `json:"mediaType"`
} `json:"subject"`
ATProto struct {
DID string `json:"did"`
Handle string `json:"handle"`
PDSEndpoint string `json:"pdsEndpoint"`
RecordURI string `json:"recordUri"`
CommitCID string `json:"commitCid"`
SignedAt time.Time `json:"signedAt"`
} `json:"atproto"`
Signature struct {
Algorithm string `json:"algorithm"`
KeyID string `json:"keyId"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
} `json:"signature"`
}
// NewATProtoVerifier creates a new ATProto verifier instance.
func NewATProtoVerifier(name string, config ATProtoConfig) (*ATProtoVerifier, error) {
// Load trust policy
trustStore, err := LoadTrustStore(config.TrustPolicyPath)
if err != nil {
return nil, fmt.Errorf("failed to load trust policy: %w", err)
}
// Create resolver with caching
resolver := NewResolver(config.DIDResolverTimeout, config.CacheEnabled, config.CacheTTL)
// Create signature verifier
verifier := NewSignatureVerifier(config.PDSTimeout)
return &ATProtoVerifier{
name: name,
config: config,
resolver: resolver,
verifier: verifier,
trustStore: trustStore,
}, nil
}
// Name returns the name of this verifier.
func (v *ATProtoVerifier) Name() string {
return v.name
}
// Type returns the type of this verifier.
func (v *ATProtoVerifier) Type() string {
return VerifierType
}
// CanVerify returns true if this verifier can verify the given artifact type.
func (v *ATProtoVerifier) CanVerify(artifactType string) bool {
return artifactType == ATProtoSignatureArtifactType
}
// VerifyReference verifies an ATProto signature artifact.
func (v *ATProtoVerifier) VerifyReference(
ctx context.Context,
subjectRef common.Reference,
referenceDesc ocispecs.ReferenceDescriptor,
store referrerstore.ReferrerStore,
) (verifier.VerifierResult, error) {
// 1. Fetch signature blob from store
sigBlob, err := store.GetBlobContent(ctx, subjectRef, referenceDesc.Digest)
if err != nil {
return v.failureResult(fmt.Sprintf("failed to fetch signature blob: %v", err)), err
}
// 2. Parse ATProto signature metadata
var sigData ATProtoSignature
if err := json.Unmarshal(sigBlob, &sigData); err != nil {
return v.failureResult(fmt.Sprintf("failed to parse signature metadata: %v", err)), err
}
// Validate signature format
if err := v.validateSignature(&sigData); err != nil {
return v.failureResult(fmt.Sprintf("invalid signature format: %v", err)), err
}
// 3. Check trust policy first (fail fast if DID not trusted)
if !v.trustStore.IsTrusted(sigData.ATProto.DID, time.Now()) {
return v.failureResult(fmt.Sprintf("DID %s not in trusted list", sigData.ATProto.DID)),
fmt.Errorf("untrusted DID")
}
// 4. Resolve DID to public key
pubKey, err := v.resolver.ResolveDIDToPublicKey(ctx, sigData.ATProto.DID)
if err != nil {
return v.failureResult(fmt.Sprintf("failed to resolve DID: %v", err)), err
}
// 5. Fetch repository commit from PDS
commit, err := v.verifier.FetchCommit(ctx, sigData.ATProto.PDSEndpoint,
sigData.ATProto.DID, sigData.ATProto.CommitCID)
if err != nil {
return v.failureResult(fmt.Sprintf("failed to fetch commit: %v", err)), err
}
// 6. Verify K-256 signature
if err := v.verifier.VerifySignature(pubKey, commit); err != nil {
return v.failureResult(fmt.Sprintf("signature verification failed: %v", err)), err
}
// 7. Success - return detailed result
return verifier.VerifierResult{
IsSuccess: true,
Name: v.name,
Type: v.Type(),
Message: fmt.Sprintf("Successfully verified ATProto signature for DID %s", sigData.ATProto.DID),
Extensions: map[string]interface{}{
"did": sigData.ATProto.DID,
"handle": sigData.ATProto.Handle,
"signedAt": sigData.ATProto.SignedAt,
"commitCid": sigData.ATProto.CommitCID,
"pdsEndpoint": sigData.ATProto.PDSEndpoint,
},
}, nil
}
// validateSignature validates the signature metadata format.
func (v *ATProtoVerifier) validateSignature(sig *ATProtoSignature) error {
if sig.Type != "io.atcr.atproto.signature" {
return fmt.Errorf("invalid signature type: %s", sig.Type)
}
if sig.ATProto.DID == "" {
return fmt.Errorf("missing DID")
}
if sig.ATProto.PDSEndpoint == "" {
return fmt.Errorf("missing PDS endpoint")
}
if sig.ATProto.CommitCID == "" {
return fmt.Errorf("missing commit CID")
}
if sig.Signature.Algorithm != "ECDSA-K256-SHA256" {
return fmt.Errorf("unsupported signature algorithm: %s", sig.Signature.Algorithm)
}
return nil
}
// failureResult creates a failure result with the given message.
func (v *ATProtoVerifier) failureResult(message string) verifier.VerifierResult {
return verifier.VerifierResult{
IsSuccess: false,
Name: v.name,
Type: v.Type(),
Message: message,
Extensions: map[string]interface{}{
"error": message,
},
}
}
// TODO: Implement resolver.go with DID resolution logic
// TODO: Implement crypto.go with K-256 signature verification
// TODO: Implement config.go with trust policy loading