A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
1#!/bin/bash
2# ATProto Signature Verification Script
3#
4# This script verifies ATProto signatures for container images stored in ATCR.
5# It performs all steps except full cryptographic verification (which requires
6# the indigo library). For production use, use the atcr-verify CLI tool.
7#
8# Usage: ./atcr-verify.sh IMAGE_REF
9# Example: ./atcr-verify.sh atcr.io/alice/myapp:latest
10#
11# Requirements:
12# - curl
13# - jq
14# - crane (https://github.com/google/go-containerregistry/releases)
15# - oras (https://oras.land/docs/installation)
16
17set -e
18
19# Colors for output
20RED='\033[0;31m'
21GREEN='\033[0;32m'
22YELLOW='\033[1;33m'
23BLUE='\033[0;34m'
24NC='\033[0m' # No Color
25
26# Check dependencies
27check_dependencies() {
28 local missing=0
29
30 for cmd in curl jq crane oras; do
31 if ! command -v $cmd &> /dev/null; then
32 echo -e "${RED}✗${NC} Missing dependency: $cmd"
33 missing=1
34 fi
35 done
36
37 if [ $missing -eq 1 ]; then
38 echo ""
39 echo "Install missing dependencies:"
40 echo " curl: https://curl.se/download.html"
41 echo " jq: https://stedolan.github.io/jq/download/"
42 echo " crane: https://github.com/google/go-containerregistry/releases"
43 echo " oras: https://oras.land/docs/installation"
44 exit 1
45 fi
46}
47
48# Print with color
49print_step() {
50 echo -e "${BLUE}[$1/${TOTAL_STEPS}]${NC} $2..."
51}
52
53print_success() {
54 echo -e " ${GREEN}→${NC} $1"
55}
56
57print_error() {
58 echo -e " ${RED}✗${NC} $1"
59}
60
61print_warning() {
62 echo -e " ${YELLOW}⚠${NC} $1"
63}
64
65# Main verification function
66verify_image() {
67 local image="$1"
68
69 if [ -z "$image" ]; then
70 echo "Usage: $0 IMAGE_REF"
71 echo "Example: $0 atcr.io/alice/myapp:latest"
72 exit 1
73 fi
74
75 TOTAL_STEPS=7
76
77 echo ""
78 echo "═══════════════════════════════════════════════════"
79 echo " ATProto Signature Verification"
80 echo "═══════════════════════════════════════════════════"
81 echo " Image: $image"
82 echo "═══════════════════════════════════════════════════"
83 echo ""
84
85 # Step 1: Resolve image digest
86 print_step 1 "Resolving image digest"
87 DIGEST=$(crane digest "$image" 2>&1)
88 if [ $? -ne 0 ]; then
89 print_error "Failed to resolve image digest"
90 echo "$DIGEST"
91 exit 1
92 fi
93 print_success "$DIGEST"
94
95 # Extract registry, repository, and tag
96 REGISTRY=$(echo "$image" | cut -d/ -f1)
97 REPO=$(echo "$image" | cut -d/ -f2-)
98 REPO_PATH=$(echo "$REPO" | cut -d: -f1)
99
100 # Step 2: Discover ATProto signature artifacts
101 print_step 2 "Discovering ATProto signature artifacts"
102 REFERRERS_URL="https://${REGISTRY}/v2/${REPO_PATH}/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json"
103
104 SIG_ARTIFACTS=$(curl -s -H "Accept: application/vnd.oci.image.index.v1+json" "$REFERRERS_URL")
105
106 if [ $? -ne 0 ]; then
107 print_error "Failed to query referrers API"
108 exit 1
109 fi
110
111 SIG_COUNT=$(echo "$SIG_ARTIFACTS" | jq '.manifests | length')
112 if [ "$SIG_COUNT" = "0" ]; then
113 print_error "No ATProto signature found"
114 echo ""
115 echo "This image does not have an ATProto signature."
116 echo "Signatures are automatically created when you push to ATCR."
117 exit 1
118 fi
119
120 print_success "Found $SIG_COUNT signature(s)"
121
122 # Get first signature digest
123 SIG_DIGEST=$(echo "$SIG_ARTIFACTS" | jq -r '.manifests[0].digest')
124 SIG_DID=$(echo "$SIG_ARTIFACTS" | jq -r '.manifests[0].annotations["io.atcr.atproto.did"]')
125 print_success "Signature digest: $SIG_DIGEST"
126 print_success "Signed by DID: $SIG_DID"
127
128 # Step 3: Fetch signature metadata
129 print_step 3 "Fetching signature metadata"
130
131 TMPDIR=$(mktemp -d)
132 trap "rm -rf $TMPDIR" EXIT
133
134 oras pull "${REGISTRY}/${REPO_PATH}@${SIG_DIGEST}" -o "$TMPDIR" --quiet 2>&1
135 if [ $? -ne 0 ]; then
136 print_error "Failed to fetch signature metadata"
137 exit 1
138 fi
139
140 # Find the JSON file
141 SIG_FILE=$(find "$TMPDIR" -name "*.json" -type f | head -n 1)
142 if [ -z "$SIG_FILE" ]; then
143 print_error "Signature metadata file not found"
144 exit 1
145 fi
146
147 DID=$(jq -r '.atproto.did' "$SIG_FILE")
148 HANDLE=$(jq -r '.atproto.handle // "unknown"' "$SIG_FILE")
149 PDS=$(jq -r '.atproto.pdsEndpoint' "$SIG_FILE")
150 RECORD_URI=$(jq -r '.atproto.recordUri' "$SIG_FILE")
151 COMMIT_CID=$(jq -r '.atproto.commitCid' "$SIG_FILE")
152 SIGNED_AT=$(jq -r '.atproto.signedAt' "$SIG_FILE")
153
154 print_success "DID: $DID"
155 print_success "Handle: $HANDLE"
156 print_success "PDS: $PDS"
157 print_success "Record: $RECORD_URI"
158 print_success "Signed at: $SIGNED_AT"
159
160 # Step 4: Resolve DID to public key
161 print_step 4 "Resolving DID to public key"
162
163 DID_DOC=$(curl -s "https://plc.directory/$DID")
164 if [ $? -ne 0 ]; then
165 print_error "Failed to resolve DID"
166 exit 1
167 fi
168
169 PUB_KEY_MB=$(echo "$DID_DOC" | jq -r '.verificationMethod[0].publicKeyMultibase')
170 if [ "$PUB_KEY_MB" = "null" ] || [ -z "$PUB_KEY_MB" ]; then
171 print_error "Public key not found in DID document"
172 exit 1
173 fi
174
175 print_success "Public key: ${PUB_KEY_MB:0:20}...${PUB_KEY_MB: -10}"
176
177 # Step 5: Query PDS for signed record
178 print_step 5 "Querying PDS for signed record"
179
180 # Extract collection and rkey from record URI (at://did/collection/rkey)
181 COLLECTION=$(echo "$RECORD_URI" | sed 's|at://[^/]*/\([^/]*\)/.*|\1|')
182 RKEY=$(echo "$RECORD_URI" | sed 's|at://.*/||')
183
184 RECORD_URL="${PDS}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=${COLLECTION}&rkey=${RKEY}"
185 RECORD=$(curl -s "$RECORD_URL")
186
187 if [ $? -ne 0 ]; then
188 print_error "Failed to fetch record from PDS"
189 exit 1
190 fi
191
192 RECORD_CID=$(echo "$RECORD" | jq -r '.cid')
193 if [ "$RECORD_CID" = "null" ] || [ -z "$RECORD_CID" ]; then
194 print_error "Record not found in PDS"
195 exit 1
196 fi
197
198 print_success "Record CID: $RECORD_CID"
199
200 # Step 6: Verify record matches image manifest
201 print_step 6 "Verifying record integrity"
202
203 RECORD_DIGEST=$(echo "$RECORD" | jq -r '.value.digest')
204 if [ "$RECORD_DIGEST" != "$DIGEST" ]; then
205 print_error "Record digest ($RECORD_DIGEST) doesn't match image digest ($DIGEST)"
206 exit 1
207 fi
208
209 print_success "Record digest matches image digest"
210
211 # Step 7: Signature verification status
212 print_step 7 "Cryptographic signature verification"
213
214 print_warning "Full cryptographic verification requires ATProto crypto library"
215 print_warning "This script verifies:"
216 echo " • Record exists in PDS"
217 echo " • DID resolved successfully"
218 echo " • Public key retrieved from DID document"
219 echo " • Record digest matches image digest"
220 echo ""
221 print_warning "For full cryptographic verification, use: atcr-verify $image"
222
223 # Summary
224 echo ""
225 echo "═══════════════════════════════════════════════════"
226 echo -e " ${GREEN}✓ Verification Completed${NC}"
227 echo "═══════════════════════════════════════════════════"
228 echo ""
229 echo " Signed by: $HANDLE ($DID)"
230 echo " Signed at: $SIGNED_AT"
231 echo " PDS: $PDS"
232 echo " Record: $RECORD_URI"
233 echo " Signature: $SIG_DIGEST"
234 echo ""
235 echo "═══════════════════════════════════════════════════"
236 echo ""
237}
238
239# Check dependencies first
240check_dependencies
241
242# Run verification
243verify_image "$1"