mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 09:14:16 +00:00
244 lines
7.9 KiB
Bash
Executable File
244 lines
7.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# ATProto Signature Verification Script
|
|
#
|
|
# This script verifies ATProto signatures for container images stored in ATCR.
|
|
# It performs all steps except full cryptographic verification (which requires
|
|
# the indigo library). For production use, use the atcr-verify CLI tool.
|
|
#
|
|
# Usage: ./atcr-verify.sh IMAGE_REF
|
|
# Example: ./atcr-verify.sh atcr.io/alice/myapp:latest
|
|
#
|
|
# Requirements:
|
|
# - curl
|
|
# - jq
|
|
# - crane (https://github.com/google/go-containerregistry/releases)
|
|
# - oras (https://oras.land/docs/installation)
|
|
|
|
set -e
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Check dependencies
|
|
check_dependencies() {
|
|
local missing=0
|
|
|
|
for cmd in curl jq crane oras; do
|
|
if ! command -v $cmd &> /dev/null; then
|
|
echo -e "${RED}✗${NC} Missing dependency: $cmd"
|
|
missing=1
|
|
fi
|
|
done
|
|
|
|
if [ $missing -eq 1 ]; then
|
|
echo ""
|
|
echo "Install missing dependencies:"
|
|
echo " curl: https://curl.se/download.html"
|
|
echo " jq: https://stedolan.github.io/jq/download/"
|
|
echo " crane: https://github.com/google/go-containerregistry/releases"
|
|
echo " oras: https://oras.land/docs/installation"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Print with color
|
|
print_step() {
|
|
echo -e "${BLUE}[$1/${TOTAL_STEPS}]${NC} $2..."
|
|
}
|
|
|
|
print_success() {
|
|
echo -e " ${GREEN}→${NC} $1"
|
|
}
|
|
|
|
print_error() {
|
|
echo -e " ${RED}✗${NC} $1"
|
|
}
|
|
|
|
print_warning() {
|
|
echo -e " ${YELLOW}⚠${NC} $1"
|
|
}
|
|
|
|
# Main verification function
|
|
verify_image() {
|
|
local image="$1"
|
|
|
|
if [ -z "$image" ]; then
|
|
echo "Usage: $0 IMAGE_REF"
|
|
echo "Example: $0 atcr.io/alice/myapp:latest"
|
|
exit 1
|
|
fi
|
|
|
|
TOTAL_STEPS=7
|
|
|
|
echo ""
|
|
echo "═══════════════════════════════════════════════════"
|
|
echo " ATProto Signature Verification"
|
|
echo "═══════════════════════════════════════════════════"
|
|
echo " Image: $image"
|
|
echo "═══════════════════════════════════════════════════"
|
|
echo ""
|
|
|
|
# Step 1: Resolve image digest
|
|
print_step 1 "Resolving image digest"
|
|
DIGEST=$(crane digest "$image" 2>&1)
|
|
if [ $? -ne 0 ]; then
|
|
print_error "Failed to resolve image digest"
|
|
echo "$DIGEST"
|
|
exit 1
|
|
fi
|
|
print_success "$DIGEST"
|
|
|
|
# Extract registry, repository, and tag
|
|
REGISTRY=$(echo "$image" | cut -d/ -f1)
|
|
REPO=$(echo "$image" | cut -d/ -f2-)
|
|
REPO_PATH=$(echo "$REPO" | cut -d: -f1)
|
|
|
|
# Step 2: Discover ATProto signature artifacts
|
|
print_step 2 "Discovering ATProto signature artifacts"
|
|
REFERRERS_URL="https://${REGISTRY}/v2/${REPO_PATH}/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json"
|
|
|
|
SIG_ARTIFACTS=$(curl -s -H "Accept: application/vnd.oci.image.index.v1+json" "$REFERRERS_URL")
|
|
|
|
if [ $? -ne 0 ]; then
|
|
print_error "Failed to query referrers API"
|
|
exit 1
|
|
fi
|
|
|
|
SIG_COUNT=$(echo "$SIG_ARTIFACTS" | jq '.manifests | length')
|
|
if [ "$SIG_COUNT" = "0" ]; then
|
|
print_error "No ATProto signature found"
|
|
echo ""
|
|
echo "This image does not have an ATProto signature."
|
|
echo "Signatures are automatically created when you push to ATCR."
|
|
exit 1
|
|
fi
|
|
|
|
print_success "Found $SIG_COUNT signature(s)"
|
|
|
|
# Get first signature digest
|
|
SIG_DIGEST=$(echo "$SIG_ARTIFACTS" | jq -r '.manifests[0].digest')
|
|
SIG_DID=$(echo "$SIG_ARTIFACTS" | jq -r '.manifests[0].annotations["io.atcr.atproto.did"]')
|
|
print_success "Signature digest: $SIG_DIGEST"
|
|
print_success "Signed by DID: $SIG_DID"
|
|
|
|
# Step 3: Fetch signature metadata
|
|
print_step 3 "Fetching signature metadata"
|
|
|
|
TMPDIR=$(mktemp -d)
|
|
trap "rm -rf $TMPDIR" EXIT
|
|
|
|
oras pull "${REGISTRY}/${REPO_PATH}@${SIG_DIGEST}" -o "$TMPDIR" --quiet 2>&1
|
|
if [ $? -ne 0 ]; then
|
|
print_error "Failed to fetch signature metadata"
|
|
exit 1
|
|
fi
|
|
|
|
# Find the JSON file
|
|
SIG_FILE=$(find "$TMPDIR" -name "*.json" -type f | head -n 1)
|
|
if [ -z "$SIG_FILE" ]; then
|
|
print_error "Signature metadata file not found"
|
|
exit 1
|
|
fi
|
|
|
|
DID=$(jq -r '.atproto.did' "$SIG_FILE")
|
|
HANDLE=$(jq -r '.atproto.handle // "unknown"' "$SIG_FILE")
|
|
PDS=$(jq -r '.atproto.pdsEndpoint' "$SIG_FILE")
|
|
RECORD_URI=$(jq -r '.atproto.recordUri' "$SIG_FILE")
|
|
COMMIT_CID=$(jq -r '.atproto.commitCid' "$SIG_FILE")
|
|
SIGNED_AT=$(jq -r '.atproto.signedAt' "$SIG_FILE")
|
|
|
|
print_success "DID: $DID"
|
|
print_success "Handle: $HANDLE"
|
|
print_success "PDS: $PDS"
|
|
print_success "Record: $RECORD_URI"
|
|
print_success "Signed at: $SIGNED_AT"
|
|
|
|
# Step 4: Resolve DID to public key
|
|
print_step 4 "Resolving DID to public key"
|
|
|
|
DID_DOC=$(curl -s "https://plc.directory/$DID")
|
|
if [ $? -ne 0 ]; then
|
|
print_error "Failed to resolve DID"
|
|
exit 1
|
|
fi
|
|
|
|
PUB_KEY_MB=$(echo "$DID_DOC" | jq -r '.verificationMethod[0].publicKeyMultibase')
|
|
if [ "$PUB_KEY_MB" = "null" ] || [ -z "$PUB_KEY_MB" ]; then
|
|
print_error "Public key not found in DID document"
|
|
exit 1
|
|
fi
|
|
|
|
print_success "Public key: ${PUB_KEY_MB:0:20}...${PUB_KEY_MB: -10}"
|
|
|
|
# Step 5: Query PDS for signed record
|
|
print_step 5 "Querying PDS for signed record"
|
|
|
|
# Extract collection and rkey from record URI (at://did/collection/rkey)
|
|
COLLECTION=$(echo "$RECORD_URI" | sed 's|at://[^/]*/\([^/]*\)/.*|\1|')
|
|
RKEY=$(echo "$RECORD_URI" | sed 's|at://.*/||')
|
|
|
|
RECORD_URL="${PDS}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=${COLLECTION}&rkey=${RKEY}"
|
|
RECORD=$(curl -s "$RECORD_URL")
|
|
|
|
if [ $? -ne 0 ]; then
|
|
print_error "Failed to fetch record from PDS"
|
|
exit 1
|
|
fi
|
|
|
|
RECORD_CID=$(echo "$RECORD" | jq -r '.cid')
|
|
if [ "$RECORD_CID" = "null" ] || [ -z "$RECORD_CID" ]; then
|
|
print_error "Record not found in PDS"
|
|
exit 1
|
|
fi
|
|
|
|
print_success "Record CID: $RECORD_CID"
|
|
|
|
# Step 6: Verify record matches image manifest
|
|
print_step 6 "Verifying record integrity"
|
|
|
|
RECORD_DIGEST=$(echo "$RECORD" | jq -r '.value.digest')
|
|
if [ "$RECORD_DIGEST" != "$DIGEST" ]; then
|
|
print_error "Record digest ($RECORD_DIGEST) doesn't match image digest ($DIGEST)"
|
|
exit 1
|
|
fi
|
|
|
|
print_success "Record digest matches image digest"
|
|
|
|
# Step 7: Signature verification status
|
|
print_step 7 "Cryptographic signature verification"
|
|
|
|
print_warning "Full cryptographic verification requires ATProto crypto library"
|
|
print_warning "This script verifies:"
|
|
echo " • Record exists in PDS"
|
|
echo " • DID resolved successfully"
|
|
echo " • Public key retrieved from DID document"
|
|
echo " • Record digest matches image digest"
|
|
echo ""
|
|
print_warning "For full cryptographic verification, use: atcr-verify $image"
|
|
|
|
# Summary
|
|
echo ""
|
|
echo "═══════════════════════════════════════════════════"
|
|
echo -e " ${GREEN}✓ Verification Completed${NC}"
|
|
echo "═══════════════════════════════════════════════════"
|
|
echo ""
|
|
echo " Signed by: $HANDLE ($DID)"
|
|
echo " Signed at: $SIGNED_AT"
|
|
echo " PDS: $PDS"
|
|
echo " Record: $RECORD_URI"
|
|
echo " Signature: $SIG_DIGEST"
|
|
echo ""
|
|
echo "═══════════════════════════════════════════════════"
|
|
echo ""
|
|
}
|
|
|
|
# Check dependencies first
|
|
check_dependencies
|
|
|
|
# Run verification
|
|
verify_image "$1"
|