# Integrating ATProto Signatures with OCI Tools This guide shows how to work with ATProto signatures using standard OCI/ORAS tools and integrate signature verification into your workflows. ## Quick Reference: Tool Compatibility | Tool | Discover Signatures | Fetch Signatures | Verify Signatures | |------|-------------------|------------------|-------------------| | `oras discover` | ✅ Yes | - | - | | `oras pull` | - | ✅ Yes | ❌ No (custom tool needed) | | `oras manifest fetch` | - | ✅ Yes | - | | `cosign tree` | ✅ Yes (as artifacts) | - | - | | `cosign verify` | - | - | ❌ No (different format) | | `crane manifest` | - | ✅ Yes | - | | `skopeo inspect` | ✅ Yes (in referrers) | - | - | | `docker` | ❌ No (not visible) | - | - | | **`atcr-verify`** | ✅ Yes | ✅ Yes | ✅ Yes | **Key Takeaway:** Standard OCI tools can **discover and fetch** ATProto signatures, but **verification requires custom tooling** because ATProto uses a different trust model than Cosign/Notary. ## Understanding What Tools See ### ORAS CLI: Full Support for Discovery ORAS understands referrers and can discover ATProto signature artifacts: ```bash # Discover all artifacts attached to an image $ oras discover atcr.io/alice/myapp:latest Discovered 2 artifacts referencing alice/myapp@sha256:abc123456789...: Digest: sha256:abc123456789... Artifact Type: application/spdx+json Digest: sha256:sbom123... Size: 45678 Artifact Type: application/vnd.atproto.signature.v1+json Digest: sha256:sig789... Size: 512 ``` **What ORAS shows:** - ✅ Artifact type (identifies it as an ATProto signature) - ✅ Digest (can fetch the artifact) - ✅ Size **To filter for signatures only:** ```bash $ oras discover atcr.io/alice/myapp:latest \ --artifact-type application/vnd.atproto.signature.v1+json Discovered 1 artifact referencing alice/myapp@sha256:abc123...: Artifact Type: application/vnd.atproto.signature.v1+json Digest: sha256:sig789... ``` ### Fetching Signature Metadata with ORAS Pull the signature artifact to examine it: ```bash # Pull signature artifact to current directory $ oras pull atcr.io/alice/myapp@sha256:sig789... Downloaded atproto-signature.json Pulled atcr.io/alice/myapp@sha256:sig789... Digest: sha256:sig789... # Examine the signature metadata $ cat atproto-signature.json | jq . { "$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", "commitCid": "bafyreih8...", "signedAt": "2025-10-31T12:34:56.789Z" }, "signature": { "algorithm": "ECDSA-K256-SHA256", "keyId": "did:plc:alice123#atproto", "publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z" } } ``` ### Cosign: Discovers but Cannot Verify Cosign can see ATProto signatures as artifacts but can't verify them: ```bash # Cosign tree shows attached artifacts $ cosign tree atcr.io/alice/myapp:latest 📦 Supply Chain Security Related artifacts for an image: atcr.io/alice/myapp:latest └── 💾 Attestations for an image tag: atcr.io/alice/myapp:sha256-abc123.att ├── 🍒 sha256:sbom123... (application/spdx+json) └── 🍒 sha256:sig789... (application/vnd.atproto.signature.v1+json) # Cosign verify doesn't work (expected) $ cosign verify atcr.io/alice/myapp:latest Error: no matching signatures: main.go:62: error during command execution: no matching signatures: ``` **Why cosign verify fails:** - Cosign expects signatures in its own format (`dev.cosignproject.cosign/signature` annotation) - ATProto signatures use a different format and trust model - This is **intentional** - we're not trying to be Cosign-compatible ### Crane: Fetch Manifests Crane can fetch the signature manifest: ```bash # Get signature artifact manifest $ crane manifest atcr.io/alice/myapp@sha256:sig789... | jq . { "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:meta456...", "size": 512 }], "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" } } ``` ### Skopeo: Inspect Images Skopeo shows referrers in image inspection: ```bash $ skopeo inspect --raw docker://atcr.io/alice/myapp:latest | jq . # Standard manifest (no signature info visible in manifest itself) # To see referrers (if registry supports Referrers API): $ curl -H "Accept: application/vnd.oci.image.index.v1+json" \ "https://atcr.io/v2/alice/myapp/referrers/sha256:abc123" ``` ## Manual Verification with Shell Scripts Until `atcr-verify` is built, you can verify signatures manually: ### Simple Verification Script ```bash #!/bin/bash # verify-atproto-signature.sh # Usage: ./verify-atproto-signature.sh atcr.io/alice/myapp:latest set -e IMAGE="$1" echo "[1/6] Resolving image digest..." DIGEST=$(crane digest "$IMAGE") echo " → $DIGEST" echo "[2/6] Discovering ATProto signature..." REGISTRY=$(echo "$IMAGE" | cut -d/ -f1) REPO=$(echo "$IMAGE" | cut -d/ -f2-) REPO_PATH=$(echo "$REPO" | cut -d: -f1) SIG_ARTIFACTS=$(curl -s -H "Accept: application/vnd.oci.image.index.v1+json" \ "https://${REGISTRY}/v2/${REPO_PATH}/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json") SIG_DIGEST=$(echo "$SIG_ARTIFACTS" | jq -r '.manifests[0].digest') if [ "$SIG_DIGEST" = "null" ]; then echo " ✗ No ATProto signature found" exit 1 fi echo " → Found signature: $SIG_DIGEST" echo "[3/6] Fetching signature metadata..." oras pull "${REGISTRY}/${REPO_PATH}@${SIG_DIGEST}" -o /tmp/sig --quiet DID=$(jq -r '.atproto.did' /tmp/sig/atproto-signature.json) PDS=$(jq -r '.atproto.pdsEndpoint' /tmp/sig/atproto-signature.json) RECORD_URI=$(jq -r '.atproto.recordUri' /tmp/sig/atproto-signature.json) echo " → DID: $DID" echo " → PDS: $PDS" echo " → Record: $RECORD_URI" echo "[4/6] Resolving DID to public key..." DID_DOC=$(curl -s "https://plc.directory/$DID") PUB_KEY_MB=$(echo "$DID_DOC" | jq -r '.verificationMethod[0].publicKeyMultibase') echo " → Public key: $PUB_KEY_MB" echo "[5/6] Querying PDS for signed commit..." # Extract collection and rkey from record URI COLLECTION=$(echo "$RECORD_URI" | sed 's|at://[^/]*/\([^/]*\)/.*|\1|') RKEY=$(echo "$RECORD_URI" | sed 's|at://.*/||') RECORD=$(curl -s "${PDS}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=${COLLECTION}&rkey=${RKEY}") RECORD_CID=$(echo "$RECORD" | jq -r '.cid') echo " → Record CID: $RECORD_CID" echo "[6/6] Verifying signature..." echo " ⚠ Note: Full cryptographic verification requires ATProto crypto library" echo " ⚠ This script verifies record existence and DID resolution only" echo "" echo " ✓ Record exists in PDS" echo " ✓ DID resolved successfully" echo " ✓ Public key retrieved" echo "" echo "To fully verify the cryptographic signature, use: atcr-verify $IMAGE" ``` ### Full Verification (Requires Go + indigo) ```go // verify.go - Full cryptographic verification package main import ( "encoding/json" "fmt" "net/http" "github.com/bluesky-social/indigo/atproto/crypto" "github.com/multiformats/go-multibase" ) func verifyATProtoSignature(did, pds, recordURI string) error { // 1. Resolve DID to public key didDoc, err := fetchDIDDocument(did) if err != nil { return fmt.Errorf("failed to resolve DID: %w", err) } pubKeyMB := didDoc.VerificationMethod[0].PublicKeyMultibase // 2. Decode multibase public key _, pubKeyBytes, err := multibase.Decode(pubKeyMB) if err != nil { return fmt.Errorf("failed to decode public key: %w", err) } // Remove multicodec prefix (first 2 bytes for K-256) pubKeyBytes = pubKeyBytes[2:] // 3. Parse as K-256 public key pubKey, err := crypto.ParsePublicKeyK256(pubKeyBytes) if err != nil { return fmt.Errorf("failed to parse public key: %w", err) } // 4. Fetch repository commit from PDS commit, err := fetchRepoCommit(pds, did) if err != nil { return fmt.Errorf("failed to fetch commit: %w", err) } // 5. Verify signature bytesToVerify := commit.Unsigned().BytesForSigning() err = pubKey.Verify(bytesToVerify, commit.Sig) if err != nil { return fmt.Errorf("signature verification failed: %w", err) } fmt.Println("✓ Signature verified successfully!") return nil } func fetchDIDDocument(did string) (*DIDDocument, error) { resp, err := http.Get(fmt.Sprintf("https://plc.directory/%s", did)) if err != nil { return nil, err } defer resp.Body.Close() var didDoc DIDDocument err = json.NewDecoder(resp.Body).Decode(&didDoc) return &didDoc, err } // ... additional helper functions ``` ## Kubernetes Integration ### Option 1: Admission Webhook (Recommended) Create a validating webhook that verifies ATProto signatures: ```yaml # atcr-verify-webhook.yaml apiVersion: v1 kind: Service metadata: name: atcr-verify namespace: kube-system spec: selector: app: atcr-verify ports: - port: 443 targetPort: 8443 --- apiVersion: apps/v1 kind: Deployment metadata: name: atcr-verify namespace: kube-system spec: replicas: 2 selector: matchLabels: app: atcr-verify template: metadata: labels: app: atcr-verify spec: containers: - name: webhook image: atcr.io/atcr/verify-webhook:latest ports: - containerPort: 8443 env: - name: REQUIRE_SIGNATURE value: "true" - name: TRUSTED_DIDS value: "did:plc:alice123,did:plc:bob456" --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: atcr-verify webhooks: - name: verify.atcr.io clientConfig: service: name: atcr-verify namespace: kube-system path: /validate caBundle: rules: - operations: ["CREATE", "UPDATE"] apiGroups: [""] apiVersions: ["v1"] resources: ["pods"] admissionReviewVersions: ["v1", "v1beta1"] sideEffects: None failurePolicy: Fail # Reject pods if verification fails namespaceSelector: matchExpressions: - key: atcr-verify operator: In values: ["enabled"] ``` **Webhook Server Logic** (pseudocode): ```go func (h *WebhookHandler) ValidatePod(req *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { pod := &corev1.Pod{} json.Unmarshal(req.Object.Raw, pod) for _, container := range pod.Spec.Containers { if !strings.HasPrefix(container.Image, "atcr.io/") { continue // Only verify ATCR images } // Verify ATProto signature err := verifyImageSignature(container.Image) if err != nil { return &admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Message: fmt.Sprintf("Image %s failed ATProto verification: %v", container.Image, err), }, } } } return &admissionv1.AdmissionResponse{Allowed: true} } ``` **Enable verification for specific namespaces:** ```bash # Label namespace to enable verification kubectl label namespace production atcr-verify=enabled # Pods in this namespace must have valid ATProto signatures kubectl apply -f pod.yaml -n production ``` ### Option 2: Kyverno Policy Use Kyverno for policy-based validation: ```yaml # kyverno-atcr-policy.yaml apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: verify-atcr-signatures spec: validationFailureAction: enforce background: false rules: - name: atcr-images-must-be-signed match: any: - resources: kinds: - Pod validate: message: "ATCR images must have valid ATProto signatures" foreach: - list: "request.object.spec.containers" deny: conditions: all: - key: "{{ element.image }}" operator: In value: "atcr.io/*" - key: "{{ atcrVerifySignature(element.image) }}" operator: NotEquals value: true ``` **Note:** Requires custom Kyverno extension for `atcrVerifySignature()` function or external service integration. ### Option 3: Ratify Verifier Plugin (Recommended) ⭐ Ratify is a verification engine that integrates with OPA Gatekeeper. Build a custom verifier plugin for ATProto signatures: **Ratify Plugin Architecture:** ```go // pkg/verifier/atproto/verifier.go package atproto import ( "context" "encoding/json" "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" ) type ATProtoVerifier struct { name string config ATProtoConfig resolver *Resolver } type ATProtoConfig struct { TrustedDIDs []string `json:"trustedDIDs"` } func (v *ATProtoVerifier) Name() string { return v.name } func (v *ATProtoVerifier) Type() string { return "atproto" } func (v *ATProtoVerifier) CanVerify(artifactType string) bool { return artifactType == "application/vnd.atproto.signature.v1+json" } 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 verifier.VerifierResult{IsSuccess: false}, err } // 2. Parse ATProto signature metadata var sigData ATProtoSignature if err := json.Unmarshal(sigBlob, &sigData); err != nil { return verifier.VerifierResult{IsSuccess: false}, err } // 3. Resolve DID to public key pubKey, err := v.resolver.ResolveDIDToPublicKey(ctx, sigData.ATProto.DID) if err != nil { return verifier.VerifierResult{IsSuccess: false}, err } // 4. Fetch repository commit from PDS commit, err := v.resolver.FetchCommit(ctx, sigData.ATProto.PDSEndpoint, sigData.ATProto.DID, sigData.ATProto.CommitCID) if err != nil { return verifier.VerifierResult{IsSuccess: false}, err } // 5. Verify K-256 signature valid := verifyK256Signature(pubKey, commit.Unsigned(), commit.Sig) if !valid { return verifier.VerifierResult{IsSuccess: false}, fmt.Errorf("signature verification failed") } // 6. Check trust policy if !v.isTrusted(sigData.ATProto.DID) { return verifier.VerifierResult{IsSuccess: false}, fmt.Errorf("DID %s not in trusted list", sigData.ATProto.DID) } return verifier.VerifierResult{ IsSuccess: true, Name: v.name, Type: v.Type(), Message: fmt.Sprintf("Verified 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, }, }, nil } func (v *ATProtoVerifier) isTrusted(did string) bool { for _, trustedDID := range v.config.TrustedDIDs { if did == trustedDID { return true } } return false } ``` **Deploy Ratify with ATProto Plugin:** 1. **Build plugin:** ```bash CGO_ENABLED=0 go build -o atproto-verifier ./cmd/ratify-atproto-plugin ``` 2. **Create custom Ratify image:** ```dockerfile FROM ghcr.io/ratify-project/ratify:latest COPY atproto-verifier /.ratify/plugins/atproto-verifier ``` 3. **Deploy Ratify:** ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: ratify namespace: gatekeeper-system spec: replicas: 1 selector: matchLabels: app: ratify template: metadata: labels: app: ratify spec: containers: - name: ratify image: atcr.io/atcr/ratify-with-atproto:latest args: - serve - --config=/config/ratify-config.yaml volumeMounts: - name: config mountPath: /config volumes: - name: config configMap: name: ratify-config ``` 4. **Configure Verifier:** ```yaml apiVersion: config.ratify.deislabs.io/v1beta1 kind: Verifier metadata: name: atproto-verifier spec: name: atproto artifactType: application/vnd.atproto.signature.v1+json address: /.ratify/plugins/atproto-verifier parameters: trustedDIDs: - did:plc:alice123 - did:plc:bob456 ``` 5. **Use with Gatekeeper:** ```yaml apiVersion: constraints.gatekeeper.sh/v1beta1 kind: RatifyVerification metadata: name: atcr-signatures-required spec: enforcementAction: deny match: kinds: - apiGroups: [""] kinds: ["Pod"] ``` **Benefits:** - ✅ Standard plugin interface - ✅ Works with existing Ratify deployments - ✅ Can combine with other verifiers (Notation, Cosign) - ✅ Policy-based enforcement via Gatekeeper **See Also:** [Integration Strategy - Ratify Plugin](./INTEGRATION_STRATEGY.md#ratify-verifier-plugin) --- ### Option 4: OPA Gatekeeper External Data Provider ⭐ Use Gatekeeper's External Data Provider feature to verify ATProto signatures: **Provider Service:** ```go // cmd/gatekeeper-provider/main.go package main import ( "context" "encoding/json" "net/http" "github.com/atcr-io/atcr/pkg/verify" ) type ProviderRequest struct { Keys []string `json:"keys"` Values []string `json:"values"` } type ProviderResponse struct { SystemError string `json:"system_error,omitempty"` Responses []map[string]interface{} `json:"responses"` } func handleProvide(w http.ResponseWriter, r *http.Request) { var req ProviderRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Verify each image responses := make([]map[string]interface{}, 0, len(req.Values)) for _, image := range req.Values { result, err := verifier.Verify(context.Background(), image) response := map[string]interface{}{ "image": image, "verified": false, } if err == nil && result.Verified { response["verified"] = true response["did"] = result.Signature.DID response["signedAt"] = result.Signature.SignedAt } responses = append(responses, response) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(ProviderResponse{ Responses: responses, }) } func main() { http.HandleFunc("/provide", handleProvide) http.ListenAndServe(":8080", nil) } ``` **Deploy Provider:** ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: atcr-provider namespace: gatekeeper-system spec: replicas: 2 selector: matchLabels: app: atcr-provider template: metadata: labels: app: atcr-provider spec: containers: - name: provider image: atcr.io/atcr/gatekeeper-provider:latest ports: - containerPort: 8080 env: - name: ATCR_POLICY_FILE value: /config/trust-policy.yaml volumeMounts: - name: config mountPath: /config volumes: - name: config configMap: name: atcr-trust-policy --- apiVersion: v1 kind: Service metadata: name: atcr-provider namespace: gatekeeper-system spec: selector: app: atcr-provider ports: - port: 80 targetPort: 8080 ``` **Configure Gatekeeper:** ```yaml apiVersion: config.gatekeeper.sh/v1alpha1 kind: Config metadata: name: config namespace: gatekeeper-system spec: sync: syncOnly: - group: "" version: "v1" kind: "Pod" validation: traces: - user: "gatekeeper" dump: "All" --- apiVersion: externaldata.gatekeeper.sh/v1alpha1 kind: Provider metadata: name: atcr-verifier spec: url: http://atcr-provider.gatekeeper-system/provide timeout: 10 ``` **Policy (Rego):** ```rego package verify import future.keywords.contains import future.keywords.if import future.keywords.in # External data call provider := "atcr-verifier" violation[{"msg": msg}] { container := input.review.object.spec.containers[_] startswith(container.image, "atcr.io/") # Call external provider response := external_data({ "provider": provider, "keys": ["image"], "values": [container.image] }) # Check verification result not response[_].verified == true msg := sprintf("Image %v has no valid ATProto signature", [container.image]) } ``` **Benefits:** - ✅ Uses standard Gatekeeper external data API - ✅ Flexible Rego policies - ✅ Can add caching, rate limiting - ✅ Easy to deploy and update **See Also:** [Integration Strategy - Gatekeeper Provider](./INTEGRATION_STRATEGY.md#opa-gatekeeper-external-provider) --- ### Option 5: OPA Gatekeeper Use OPA for policy enforcement: ```yaml # gatekeeper-constraint-template.yaml apiVersion: templates.gatekeeper.sh/v1 kind: ConstraintTemplate metadata: name: atcrverify spec: crd: spec: names: kind: ATCRVerify targets: - target: admission.k8s.gatekeeper.sh rego: | package atcrverify violation[{"msg": msg}] { container := input.review.object.spec.containers[_] startswith(container.image, "atcr.io/") not verified(container.image) msg := sprintf("Image %v has no valid ATProto signature", [container.image]) } verified(image) { # Call external verification service response := http.send({ "method": "GET", "url": sprintf("http://atcr-verify.kube-system.svc/verify?image=%v", [image]), }) response.status_code == 200 } --- apiVersion: constraints.gatekeeper.sh/v1beta1 kind: ATCRVerify metadata: name: atcr-signatures-required spec: match: kinds: - apiGroups: [""] kinds: ["Pod"] ``` ## CI/CD Integration ### GitHub Actions ```yaml # .github/workflows/verify-and-deploy.yml name: Verify and Deploy on: push: branches: [main] jobs: verify-image: runs-on: ubuntu-latest steps: - name: Install ORAS run: | curl -LO https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz tar -xzf oras_1.0.0_linux_amd64.tar.gz sudo mv oras /usr/local/bin/ - name: Install crane run: | curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.15.2/go-containerregistry_Linux_x86_64.tar.gz" > crane.tar.gz tar -xzf crane.tar.gz sudo mv crane /usr/local/bin/ - name: Verify image signature run: | IMAGE="atcr.io/alice/myapp:${{ github.sha }}" # Get image digest DIGEST=$(crane digest "$IMAGE") # Check for ATProto signature REFERRERS=$(curl -s "https://atcr.io/v2/alice/myapp/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json") SIG_COUNT=$(echo "$REFERRERS" | jq '.manifests | length') if [ "$SIG_COUNT" -eq 0 ]; then echo "❌ No ATProto signature found" exit 1 fi echo "✓ Found $SIG_COUNT signature(s)" # TODO: Full verification when atcr-verify is available # atcr-verify "$IMAGE" --policy policy.yaml - name: Deploy to Kubernetes if: success() run: | kubectl set image deployment/myapp myapp=atcr.io/alice/myapp:${{ github.sha }} ``` ### GitLab CI ```yaml # .gitlab-ci.yml verify_image: stage: verify image: alpine:latest before_script: - apk add --no-cache curl jq script: - | IMAGE="atcr.io/alice/myapp:${CI_COMMIT_SHA}" # Install crane wget https://github.com/google/go-containerregistry/releases/download/v0.15.2/go-containerregistry_Linux_x86_64.tar.gz tar -xzf go-containerregistry_Linux_x86_64.tar.gz crane # Get digest DIGEST=$(./crane digest "$IMAGE") # Check signature REFERRERS=$(curl -s "https://atcr.io/v2/alice/myapp/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json") if [ $(echo "$REFERRERS" | jq '.manifests | length') -eq 0 ]; then echo "❌ No signature found" exit 1 fi echo "✓ Signature verified" deploy: stage: deploy dependencies: - verify_image script: - kubectl set image deployment/myapp myapp=atcr.io/alice/myapp:${CI_COMMIT_SHA} ``` ## Integration with Containerd Containerd can be extended with verification plugins: ```go // containerd-atcr-verifier plugin package main import ( "context" "fmt" "github.com/containerd/containerd" "github.com/containerd/containerd/remotes" ) type ATCRVerifier struct { // Configuration } func (v *ATCRVerifier) Verify(ctx context.Context, ref string) error { // 1. Query referrers API for signatures sigs, err := v.discoverSignatures(ctx, ref) if err != nil { return err } if len(sigs) == 0 { return fmt.Errorf("no ATProto signature found for %s", ref) } // 2. Fetch and verify signature for _, sig := range sigs { err := v.verifySignature(ctx, sig) if err == nil { return nil // At least one valid signature } } return fmt.Errorf("all signatures failed verification") } // Use as containerd resolver wrapper func NewVerifyingResolver(base remotes.Resolver, verifier *ATCRVerifier) remotes.Resolver { return &verifyingResolver{ Resolver: base, verifier: verifier, } } ``` **Containerd config** (`/etc/containerd/config.toml`): ```toml [plugins."io.containerd.grpc.v1.cri".registry] [plugins."io.containerd.grpc.v1.cri".registry.configs] [plugins."io.containerd.grpc.v1.cri".registry.configs."atcr.io"] [plugins."io.containerd.grpc.v1.cri".registry.configs."atcr.io".auth] username = "alice" password = "..." # Custom verifier hook (requires plugin) verify_signatures = true signature_type = "atproto" ``` ## Trust Policies Define what signatures you trust: ```yaml # trust-policy.yaml version: 1.0 policies: # Production images must be signed - name: production-images scope: "atcr.io/*/prod-*" require: - signature: true trustedDIDs: - did:plc:alice123 - did:plc:bob456 minSignatures: 1 action: enforce # reject if policy fails # Development images don't require signatures - name: dev-images scope: "atcr.io/*/dev-*" require: - signature: false action: audit # log but don't reject # Staging requires at least 1 signature from any trusted DID - name: staging-images scope: "atcr.io/*/staging-*" require: - signature: true trustedDIDs: - did:plc:alice123 - did:plc:bob456 - did:plc:charlie789 action: enforce # DID trust configuration trustedDIDs: did:plc:alice123: name: "Alice (DevOps Lead)" validFrom: "2024-01-01T00:00:00Z" expiresAt: null did:plc:bob456: name: "Bob (Security Team)" validFrom: "2024-06-01T00:00:00Z" expiresAt: "2025-12-31T23:59:59Z" ``` ## Troubleshooting ### No Signature Found ```bash $ oras discover atcr.io/alice/myapp:latest --artifact-type application/vnd.atproto.signature.v1+json Discovered 0 artifacts ``` **Possible causes:** 1. Image was pushed before signature creation was implemented 2. Signature artifact creation failed 3. Registry doesn't support Referrers API **Solutions:** - Re-push the image to generate signature - Check AppView logs for signature creation errors - Verify Referrers API endpoint: `GET /v2/{repo}/referrers/{digest}` ### Signature Verification Fails **Check DID resolution:** ```bash curl -s "https://plc.directory/did:plc:alice123" | jq . # Should return DID document with verificationMethod ``` **Check PDS connectivity:** ```bash curl -s "https://bsky.social/xrpc/com.atproto.repo.describeRepo?repo=did:plc:alice123" | jq . # Should return repository metadata ``` **Check record exists:** ```bash curl -s "https://bsky.social/xrpc/com.atproto.repo.getRecord?\ repo=did:plc:alice123&\ collection=io.atcr.manifest&\ rkey=abc123" | jq . # Should return manifest record ``` ### Registry Returns 404 for Referrers Some registries don't support OCI Referrers API yet. Fallback to tag-based discovery: ```bash # Look for signature tags (if implemented) crane ls atcr.io/alice/myapp | grep sig ``` ## Best Practices ### 1. Always Verify in Production Enable signature verification for production namespaces: ```bash kubectl label namespace production atcr-verify=enabled ``` ### 2. Use Trust Policies Don't blindly trust all signatures - define which DIDs you trust: ```yaml trustedDIDs: - did:plc:your-org-team - did:plc:your-ci-system ``` ### 3. Monitor Signature Coverage Track which images have signatures: ```bash # Check all images in a namespace kubectl get pods -n production -o json | \ jq -r '.items[].spec.containers[].image' | \ while read image; do echo -n "$image: " oras discover "$image" --artifact-type application/vnd.atproto.signature.v1+json | \ grep -q "Discovered 0" && echo "❌ No signature" || echo "✓ Signed" done ``` ### 4. Automate Verification in CI/CD Never deploy unsigned images to production: ```yaml # GitHub Actions - name: Verify signature run: | if ! atcr-verify $IMAGE; then echo "❌ Image is not signed" exit 1 fi ``` ### 5. Plan for Offline Scenarios For air-gapped environments, cache signature metadata and DID documents: ```bash # Export signatures and DID docs for offline use ./export-verification-bundle.sh atcr.io/alice/myapp:latest > bundle.json # In air-gapped environment atcr-verify --offline --bundle bundle.json atcr.io/alice/myapp:latest ``` ## Next Steps 1. **Try manual verification** using the shell scripts above 2. **Set up admission webhook** for your Kubernetes cluster 3. **Define trust policies** for your organization 4. **Integrate into CI/CD** pipelines 5. **Monitor signature coverage** across your images ## See Also - [ATProto Signatures](./ATPROTO_SIGNATURES.md) - Technical deep-dive - [SBOM Scanning](./SBOM_SCANNING.md) - Similar ORAS artifact pattern - [Example Scripts](../examples/verification/) - Working verification examples