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 succeeded1- Verification failed2- Invalid arguments3- Network error4- 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 imagesatcr.io/myorg/*- All images from myorgatcr.io/*/prod-*- All images with "prod-" prefixatcr.io/myorg/myapp- Specific repositoryatcr.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 pathATCR_POLICY- Default trust policy fileATCR_CACHE_DIR- Cache directoryATCR_OUTPUT- Output format (text, json, quiet)ATCR_TIMEOUT- Verification timeoutHTTP_PROXY/HTTPS_PROXY- Proxy settingsNO_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#
- Enable caching - DID documents change rarely
- Use offline bundles - For air-gapped environments
- Parallel verification - Verify multiple images concurrently
- 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#
- ATProto Signatures - How ATProto signing works
- Integration Strategy - Overview of integration approaches
- Signature Integration - Tool-specific guides
- Trust Policy Examples