215 lines
6.7 KiB
Plaintext
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
|