A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go

atcr-verify CLI Tool#

Overview#

atcr-verify is a command-line tool for verifying ATProto signatures on container images stored in ATCR. It provides cryptographic verification of image manifests using ATProto's DID-based trust model.

Features#

  • ✅ Verify ATProto signatures via OCI Referrers API
  • ✅ DID resolution and public key extraction
  • ✅ PDS query and commit signature verification
  • ✅ Trust policy enforcement
  • ✅ Offline verification mode (with cached data)
  • ✅ Multiple output formats (human-readable, JSON, quiet)
  • ✅ Exit codes for CI/CD integration
  • ✅ Kubernetes admission controller integration

Installation#

Binary Release#

# Linux (x86_64)
curl -L https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify-linux-amd64 -o atcr-verify
chmod +x atcr-verify
sudo mv atcr-verify /usr/local/bin/

# macOS (Apple Silicon)
curl -L https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify-darwin-arm64 -o atcr-verify
chmod +x atcr-verify
sudo mv atcr-verify /usr/local/bin/

# Windows
curl -L https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify-windows-amd64.exe -o atcr-verify.exe

From Source#

git clone https://github.com/atcr-io/atcr.git
cd atcr
go install ./cmd/atcr-verify

Container Image#

docker pull atcr.io/atcr/verify:latest

# Run
docker run --rm atcr.io/atcr/verify:latest verify IMAGE

Usage#

Basic Verification#

# Verify an image
atcr-verify atcr.io/alice/myapp:latest

# Output:
# ✓ Image verified successfully
# Signed by: alice.bsky.social (did:plc:alice123)
# Signed at: 2025-10-31T12:34:56.789Z

With Trust Policy#

# Verify against trust policy
atcr-verify atcr.io/alice/myapp:latest --policy trust-policy.yaml

# Output:
# ✓ Image verified successfully
# ✓ Trust policy satisfied
# Policy: production-images
# Trusted DID: did:plc:alice123

JSON Output#

atcr-verify atcr.io/alice/myapp:latest --output json

# Output:
{
  "verified": true,
  "image": "atcr.io/alice/myapp:latest",
  "digest": "sha256:abc123...",
  "signature": {
    "did": "did:plc:alice123",
    "handle": "alice.bsky.social",
    "pds": "https://bsky.social",
    "recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123",
    "commitCid": "bafyreih8...",
    "signedAt": "2025-10-31T12:34:56.789Z",
    "algorithm": "ECDSA-K256-SHA256"
  },
  "trustPolicy": {
    "satisfied": true,
    "policy": "production-images",
    "trustedDID": true
  }
}

Quiet Mode#

# Exit code only (for scripts)
atcr-verify atcr.io/alice/myapp:latest --quiet
echo $?  # 0 = verified, 1 = failed

Offline Mode#

# Export verification bundle
atcr-verify export atcr.io/alice/myapp:latest -o bundle.json

# Verify offline (in air-gapped environment)
atcr-verify atcr.io/alice/myapp:latest --offline --bundle bundle.json

Command Reference#

verify#

Verify ATProto signature for an image.

atcr-verify verify IMAGE [flags]
atcr-verify IMAGE [flags]  # 'verify' subcommand is optional

Arguments:

  • IMAGE - Image reference (registry/owner/repo:tag or @digest)

Flags:

  • --policy FILE - Trust policy file (default: none)
  • --output FORMAT - Output format: text, json, quiet (default: text)
  • --offline - Offline mode (requires --bundle)
  • --bundle FILE - Verification bundle for offline mode
  • --cache-dir DIR - Cache directory for DID documents (default: ~/.atcr/cache)
  • --no-cache - Disable caching
  • --timeout DURATION - Verification timeout (default: 30s)
  • --verbose - Verbose output

Exit Codes:

  • 0 - Verification succeeded
  • 1 - Verification failed
  • 2 - Invalid arguments
  • 3 - Network error
  • 4 - Trust policy violation

Examples:

# Basic verification
atcr-verify atcr.io/alice/myapp:latest

# With specific digest
atcr-verify atcr.io/alice/myapp@sha256:abc123...

# With trust policy
atcr-verify atcr.io/alice/myapp:latest --policy production-policy.yaml

# JSON output for scripting
atcr-verify atcr.io/alice/myapp:latest --output json | jq .verified

# Quiet mode for CI/CD
if atcr-verify atcr.io/alice/myapp:latest --quiet; then
  echo "Deploy approved"
fi

export#

Export verification bundle for offline verification.

atcr-verify export IMAGE [flags]

Arguments:

  • IMAGE - Image reference to export bundle for

Flags:

  • -o, --output FILE - Output file (default: stdout)
  • --include-did-docs - Include DID documents in bundle
  • --include-commit - Include ATProto commit data

Examples:

# Export to file
atcr-verify export atcr.io/alice/myapp:latest -o myapp-bundle.json

# Export with all verification data
atcr-verify export atcr.io/alice/myapp:latest \
  --include-did-docs \
  --include-commit \
  -o complete-bundle.json

# Export for multiple images
for img in $(cat images.txt); do
  atcr-verify export $img -o bundles/$(echo $img | tr '/:' '_').json
done

trust#

Manage trust policies and trusted DIDs.

atcr-verify trust COMMAND [flags]

Subcommands:

trust list - List trusted DIDs

atcr-verify trust list

# Output:
# Trusted DIDs:
# - did:plc:alice123 (alice.bsky.social)
# - did:plc:bob456 (bob.example.com)

trust add DID - Add trusted DID

atcr-verify trust add did:plc:alice123
atcr-verify trust add did:plc:alice123 --name "Alice (DevOps)"

trust remove DID - Remove trusted DID

atcr-verify trust remove did:plc:alice123

trust policy validate - Validate trust policy file

atcr-verify trust policy validate policy.yaml

version#

Show version information.

atcr-verify version

# Output:
# atcr-verify version 1.0.0
# Go version: go1.21.5
# Commit: 3b5b89b
# Built: 2025-10-31T12:00:00Z

Trust Policy#

Trust policies define which signatures to trust and what to do when verification fails.

Policy File Format#

version: 1.0

# Global settings
defaultAction: enforce  # enforce, audit, allow
requireSignature: true

# Policies matched by image pattern (first match wins)
policies:
  - name: production-images
    description: "Production images must be signed by DevOps or Security"
    scope: "atcr.io/*/prod-*"
    require:
      signature: true
      trustedDIDs:
        - did:plc:devops-team
        - did:plc:security-team
      minSignatures: 1
      maxAge: 2592000  # 30 days in seconds
    action: enforce

  - name: staging-images
    scope: "atcr.io/*/staging-*"
    require:
      signature: true
      trustedDIDs:
        - did:plc:devops-team
        - did:plc:developers
      minSignatures: 1
    action: enforce

  - name: dev-images
    scope: "atcr.io/*/dev-*"
    require:
      signature: false
    action: audit  # Log but don't fail

# Trusted DID registry
trustedDIDs:
  did:plc:devops-team:
    name: "DevOps Team"
    validFrom: "2024-01-01T00:00:00Z"
    expiresAt: null
    contact: "devops@example.com"

  did:plc:security-team:
    name: "Security Team"
    validFrom: "2024-01-01T00:00:00Z"
    expiresAt: null

  did:plc:developers:
    name: "Developer Team"
    validFrom: "2024-06-01T00:00:00Z"
    expiresAt: "2025-12-31T23:59:59Z"

Policy Matching#

Policies are evaluated in order. First match wins.

Scope patterns:

  • atcr.io/*/* - All ATCR images
  • atcr.io/myorg/* - All images from myorg
  • atcr.io/*/prod-* - All images with "prod-" prefix
  • atcr.io/myorg/myapp - Specific repository
  • atcr.io/myorg/myapp:v* - Tag pattern matching

Policy Actions#

enforce - Reject if policy fails

  • Exit code 4
  • Blocks deployment

audit - Log but allow

  • Exit code 0 (success)
  • Warning message printed

allow - Always allow

  • No verification performed
  • Exit code 0

Policy Requirements#

signature: true - Require signature present

trustedDIDs - List of trusted DIDs

trustedDIDs:
  - did:plc:alice123
  - did:web:example.com

minSignatures - Minimum number of signatures required

minSignatures: 2  # Require 2 signatures

maxAge - Maximum signature age in seconds

maxAge: 2592000  # 30 days

algorithms - Allowed signature algorithms

algorithms:
  - ECDSA-K256-SHA256

Verification Flow#

1. Image Resolution#

Input: atcr.io/alice/myapp:latest
  ↓
Resolve tag to digest
  ↓
Output: sha256:abc123...

2. Signature Discovery#

Query OCI Referrers API:
  GET /v2/alice/myapp/referrers/sha256:abc123
  ?artifactType=application/vnd.atproto.signature.v1+json
  ↓
Returns: List of signature artifacts
  ↓
Download signature metadata blobs

3. DID Resolution#

Extract DID from signature: did:plc:alice123
  ↓
Query PLC directory:
  GET https://plc.directory/did:plc:alice123
  ↓
Extract public key from DID document

4. PDS Query#

Get PDS endpoint from DID document
  ↓
Query for manifest record:
  GET {pds}/xrpc/com.atproto.repo.getRecord
    ?repo=did:plc:alice123
    &collection=io.atcr.manifest
    &rkey=abc123
  ↓
Get commit CID from record
  ↓
Fetch commit data (includes signature)

5. Signature Verification#

Extract signature bytes from commit
  ↓
Compute commit hash (SHA-256)
  ↓
Verify: ECDSA_K256(hash, signature, publicKey)
  ↓
Result: Valid or Invalid

6. Trust Policy Evaluation#

Check if DID is in trustedDIDs list
  ↓
Check signature age < maxAge
  ↓
Check minSignatures satisfied
  ↓
Apply policy action (enforce/audit/allow)

Integration Examples#

CI/CD Pipeline#

GitHub Actions:

name: Deploy

on:
  push:
    branches: [main]

jobs:
  verify-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Install atcr-verify
        run: |
          curl -L https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify-linux-amd64 -o atcr-verify
          chmod +x atcr-verify
          sudo mv atcr-verify /usr/local/bin/

      - name: Verify image signature
        run: |
          atcr-verify ${{ env.IMAGE }} --policy .github/trust-policy.yaml

      - name: Deploy to production
        if: success()
        run: kubectl set image deployment/app app=${{ env.IMAGE }}

GitLab CI:

verify:
  stage: verify
  image: atcr.io/atcr/verify:latest
  script:
    - atcr-verify ${IMAGE} --policy trust-policy.yaml

deploy:
  stage: deploy
  dependencies:
    - verify
  script:
    - kubectl set image deployment/app app=${IMAGE}

Jenkins:

pipeline {
  agent any

  stages {
    stage('Verify') {
      steps {
        sh 'atcr-verify ${IMAGE} --policy trust-policy.yaml'
      }
    }

    stage('Deploy') {
      when {
        expression { currentBuild.result == 'SUCCESS' }
      }
      steps {
        sh 'kubectl set image deployment/app app=${IMAGE}'
      }
    }
  }
}

Kubernetes Admission Controller#

Using as webhook backend:

// webhook server
func (h *Handler) ValidatePod(w http.ResponseWriter, r *http.Request) {
    var admReq admissionv1.AdmissionReview
    json.NewDecoder(r.Body).Decode(&admReq)

    pod := &corev1.Pod{}
    json.Unmarshal(admReq.Request.Object.Raw, pod)

    // Verify each container image
    for _, container := range pod.Spec.Containers {
        cmd := exec.Command("atcr-verify", container.Image,
            "--policy", "/etc/atcr/trust-policy.yaml",
            "--quiet")

        if err := cmd.Run(); err != nil {
            // Verification failed
            admResp := admissionv1.AdmissionReview{
                Response: &admissionv1.AdmissionResponse{
                    UID: admReq.Request.UID,
                    Allowed: false,
                    Result: &metav1.Status{
                        Message: fmt.Sprintf("Image %s failed signature verification", container.Image),
                    },
                },
            }
            json.NewEncoder(w).Encode(admResp)
            return
        }
    }

    // All images verified
    admResp := admissionv1.AdmissionReview{
        Response: &admissionv1.AdmissionResponse{
            UID: admReq.Request.UID,
            Allowed: true,
        },
    }
    json.NewEncoder(w).Encode(admResp)
}

Pre-Pull Verification#

Systemd service:

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=docker.service

[Service]
Type=oneshot
ExecStartPre=/usr/local/bin/atcr-verify atcr.io/myorg/myapp:latest --policy /etc/atcr/policy.yaml
ExecStartPre=/usr/bin/docker pull atcr.io/myorg/myapp:latest
ExecStart=/usr/bin/docker run atcr.io/myorg/myapp:latest
Restart=on-failure

[Install]
WantedBy=multi-user.target

Docker wrapper script:

#!/bin/bash
# docker-secure-pull.sh

IMAGE="$1"

# Verify before pulling
if ! atcr-verify "$IMAGE" --policy ~/.atcr/trust-policy.yaml; then
    echo "ERROR: Image signature verification failed"
    exit 1
fi

# Pull if verified
docker pull "$IMAGE"

Configuration#

Config File#

Location: ~/.atcr/config.yaml

# Default trust policy
defaultPolicy: ~/.atcr/trust-policy.yaml

# Cache settings
cache:
  enabled: true
  directory: ~/.atcr/cache
  ttl:
    didDocuments: 3600  # 1 hour
    commits: 600  # 10 minutes

# Network settings
timeout: 30s
retries: 3

# Output settings
output:
  format: text  # text, json, quiet
  color: auto  # auto, always, never

# Registry settings
registries:
  atcr.io:
    insecure: false
    credentialsFile: ~/.docker/config.json

Environment Variables#

  • ATCR_CONFIG - Config file path
  • ATCR_POLICY - Default trust policy file
  • ATCR_CACHE_DIR - Cache directory
  • ATCR_OUTPUT - Output format (text, json, quiet)
  • ATCR_TIMEOUT - Verification timeout
  • HTTP_PROXY / HTTPS_PROXY - Proxy settings
  • NO_CACHE - Disable caching

Library Usage#

atcr-verify can also be used as a Go library:

import "github.com/atcr-io/atcr/pkg/verify"

func main() {
    verifier := verify.NewVerifier(verify.Config{
        Policy: policy,
        Timeout: 30 * time.Second,
    })

    result, err := verifier.Verify(ctx, "atcr.io/alice/myapp:latest")
    if err != nil {
        log.Fatal(err)
    }

    if !result.Verified {
        log.Fatal("Verification failed")
    }

    fmt.Printf("Verified by %s\n", result.Signature.DID)
}

Performance#

Typical Verification Times#

  • First verification: 500-1000ms

    • OCI Referrers API: 50-100ms
    • DID resolution: 50-150ms
    • PDS query: 100-300ms
    • Signature verification: 1-5ms
  • Cached verification: 50-150ms

    • DID document cached
    • Signature metadata cached

Optimization Tips#

  1. Enable caching - DID documents change rarely
  2. Use offline bundles - For air-gapped environments
  3. Parallel verification - Verify multiple images concurrently
  4. Local trust policy - Avoid remote policy fetches

Troubleshooting#

Verification Fails#

atcr-verify atcr.io/alice/myapp:latest --verbose

Common issues:

  • No signature found - Image not signed, check Referrers API
  • DID resolution failed - Network issue, check PLC directory
  • PDS unreachable - Network issue, check PDS endpoint
  • Signature invalid - Tampering detected or key mismatch
  • Trust policy violation - DID not in trusted list

Enable Debug Logging#

ATCR_LOG_LEVEL=debug atcr-verify IMAGE

Clear Cache#

rm -rf ~/.atcr/cache

See Also#