tangled
alpha
login
or
join now
evan.jarrett.net
/
at-container-registry
66
fork
atom
A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
66
fork
atom
overview
issues
1
pulls
pipelines
try and offline holds
evan.jarrett.net
4 months ago
e6b12642
15d2be92
verified
This commit was signed with the committer's
known signature
.
evan.jarrett.net
SSH Key Fingerprint:
SHA256:bznk0uVPp7XFOl67P0uTM1pCjf2A4ojeP/lsUE7uauQ=
0/1
tests.yml
failed
1m 12s
+8321
-815
21 changed files
expand all
collapse all
unified
split
docs
ATCR_VERIFY_CLI.md
ATPROTO_SIGNATURES.md
DEVELOPMENT.md
HOLD_AS_CA.md
IMAGE_SIGNING.md
INTEGRATION_STRATEGY.md
SIGNATURE_INTEGRATION.md
examples
plugins
README.md
ci-cd
github-actions.yml
gitlab-ci.yml
gatekeeper-provider
README.md
main.go
ratify-verifier
README.md
verifier.go
verification
README.md
atcr-verify.sh
kubernetes-webhook.yaml
trust-policy.yaml
verify-and-pull.sh
pkg
appview
handlers
manifest_health.go
templates
pages
repository.html
+728
docs/ATCR_VERIFY_CLI.md
···
1
1
+
# atcr-verify CLI Tool
2
2
+
3
3
+
## Overview
4
4
+
5
5
+
`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.
6
6
+
7
7
+
## Features
8
8
+
9
9
+
- ✅ Verify ATProto signatures via OCI Referrers API
10
10
+
- ✅ DID resolution and public key extraction
11
11
+
- ✅ PDS query and commit signature verification
12
12
+
- ✅ Trust policy enforcement
13
13
+
- ✅ Offline verification mode (with cached data)
14
14
+
- ✅ Multiple output formats (human-readable, JSON, quiet)
15
15
+
- ✅ Exit codes for CI/CD integration
16
16
+
- ✅ Kubernetes admission controller integration
17
17
+
18
18
+
## Installation
19
19
+
20
20
+
### Binary Release
21
21
+
22
22
+
```bash
23
23
+
# Linux (x86_64)
24
24
+
curl -L https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify-linux-amd64 -o atcr-verify
25
25
+
chmod +x atcr-verify
26
26
+
sudo mv atcr-verify /usr/local/bin/
27
27
+
28
28
+
# macOS (Apple Silicon)
29
29
+
curl -L https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify-darwin-arm64 -o atcr-verify
30
30
+
chmod +x atcr-verify
31
31
+
sudo mv atcr-verify /usr/local/bin/
32
32
+
33
33
+
# Windows
34
34
+
curl -L https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify-windows-amd64.exe -o atcr-verify.exe
35
35
+
```
36
36
+
37
37
+
### From Source
38
38
+
39
39
+
```bash
40
40
+
git clone https://github.com/atcr-io/atcr.git
41
41
+
cd atcr
42
42
+
go install ./cmd/atcr-verify
43
43
+
```
44
44
+
45
45
+
### Container Image
46
46
+
47
47
+
```bash
48
48
+
docker pull atcr.io/atcr/verify:latest
49
49
+
50
50
+
# Run
51
51
+
docker run --rm atcr.io/atcr/verify:latest verify IMAGE
52
52
+
```
53
53
+
54
54
+
## Usage
55
55
+
56
56
+
### Basic Verification
57
57
+
58
58
+
```bash
59
59
+
# Verify an image
60
60
+
atcr-verify atcr.io/alice/myapp:latest
61
61
+
62
62
+
# Output:
63
63
+
# ✓ Image verified successfully
64
64
+
# Signed by: alice.bsky.social (did:plc:alice123)
65
65
+
# Signed at: 2025-10-31T12:34:56.789Z
66
66
+
```
67
67
+
68
68
+
### With Trust Policy
69
69
+
70
70
+
```bash
71
71
+
# Verify against trust policy
72
72
+
atcr-verify atcr.io/alice/myapp:latest --policy trust-policy.yaml
73
73
+
74
74
+
# Output:
75
75
+
# ✓ Image verified successfully
76
76
+
# ✓ Trust policy satisfied
77
77
+
# Policy: production-images
78
78
+
# Trusted DID: did:plc:alice123
79
79
+
```
80
80
+
81
81
+
### JSON Output
82
82
+
83
83
+
```bash
84
84
+
atcr-verify atcr.io/alice/myapp:latest --output json
85
85
+
86
86
+
# Output:
87
87
+
{
88
88
+
"verified": true,
89
89
+
"image": "atcr.io/alice/myapp:latest",
90
90
+
"digest": "sha256:abc123...",
91
91
+
"signature": {
92
92
+
"did": "did:plc:alice123",
93
93
+
"handle": "alice.bsky.social",
94
94
+
"pds": "https://bsky.social",
95
95
+
"recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123",
96
96
+
"commitCid": "bafyreih8...",
97
97
+
"signedAt": "2025-10-31T12:34:56.789Z",
98
98
+
"algorithm": "ECDSA-K256-SHA256"
99
99
+
},
100
100
+
"trustPolicy": {
101
101
+
"satisfied": true,
102
102
+
"policy": "production-images",
103
103
+
"trustedDID": true
104
104
+
}
105
105
+
}
106
106
+
```
107
107
+
108
108
+
### Quiet Mode
109
109
+
110
110
+
```bash
111
111
+
# Exit code only (for scripts)
112
112
+
atcr-verify atcr.io/alice/myapp:latest --quiet
113
113
+
echo $? # 0 = verified, 1 = failed
114
114
+
```
115
115
+
116
116
+
### Offline Mode
117
117
+
118
118
+
```bash
119
119
+
# Export verification bundle
120
120
+
atcr-verify export atcr.io/alice/myapp:latest -o bundle.json
121
121
+
122
122
+
# Verify offline (in air-gapped environment)
123
123
+
atcr-verify atcr.io/alice/myapp:latest --offline --bundle bundle.json
124
124
+
```
125
125
+
126
126
+
## Command Reference
127
127
+
128
128
+
### verify
129
129
+
130
130
+
Verify ATProto signature for an image.
131
131
+
132
132
+
```bash
133
133
+
atcr-verify verify IMAGE [flags]
134
134
+
atcr-verify IMAGE [flags] # 'verify' subcommand is optional
135
135
+
```
136
136
+
137
137
+
**Arguments:**
138
138
+
- `IMAGE` - Image reference (registry/owner/repo:tag or @digest)
139
139
+
140
140
+
**Flags:**
141
141
+
- `--policy FILE` - Trust policy file (default: none)
142
142
+
- `--output FORMAT` - Output format: text, json, quiet (default: text)
143
143
+
- `--offline` - Offline mode (requires --bundle)
144
144
+
- `--bundle FILE` - Verification bundle for offline mode
145
145
+
- `--cache-dir DIR` - Cache directory for DID documents (default: ~/.atcr/cache)
146
146
+
- `--no-cache` - Disable caching
147
147
+
- `--timeout DURATION` - Verification timeout (default: 30s)
148
148
+
- `--verbose` - Verbose output
149
149
+
150
150
+
**Exit Codes:**
151
151
+
- `0` - Verification succeeded
152
152
+
- `1` - Verification failed
153
153
+
- `2` - Invalid arguments
154
154
+
- `3` - Network error
155
155
+
- `4` - Trust policy violation
156
156
+
157
157
+
**Examples:**
158
158
+
159
159
+
```bash
160
160
+
# Basic verification
161
161
+
atcr-verify atcr.io/alice/myapp:latest
162
162
+
163
163
+
# With specific digest
164
164
+
atcr-verify atcr.io/alice/myapp@sha256:abc123...
165
165
+
166
166
+
# With trust policy
167
167
+
atcr-verify atcr.io/alice/myapp:latest --policy production-policy.yaml
168
168
+
169
169
+
# JSON output for scripting
170
170
+
atcr-verify atcr.io/alice/myapp:latest --output json | jq .verified
171
171
+
172
172
+
# Quiet mode for CI/CD
173
173
+
if atcr-verify atcr.io/alice/myapp:latest --quiet; then
174
174
+
echo "Deploy approved"
175
175
+
fi
176
176
+
```
177
177
+
178
178
+
### export
179
179
+
180
180
+
Export verification bundle for offline verification.
181
181
+
182
182
+
```bash
183
183
+
atcr-verify export IMAGE [flags]
184
184
+
```
185
185
+
186
186
+
**Arguments:**
187
187
+
- `IMAGE` - Image reference to export bundle for
188
188
+
189
189
+
**Flags:**
190
190
+
- `-o, --output FILE` - Output file (default: stdout)
191
191
+
- `--include-did-docs` - Include DID documents in bundle
192
192
+
- `--include-commit` - Include ATProto commit data
193
193
+
194
194
+
**Examples:**
195
195
+
196
196
+
```bash
197
197
+
# Export to file
198
198
+
atcr-verify export atcr.io/alice/myapp:latest -o myapp-bundle.json
199
199
+
200
200
+
# Export with all verification data
201
201
+
atcr-verify export atcr.io/alice/myapp:latest \
202
202
+
--include-did-docs \
203
203
+
--include-commit \
204
204
+
-o complete-bundle.json
205
205
+
206
206
+
# Export for multiple images
207
207
+
for img in $(cat images.txt); do
208
208
+
atcr-verify export $img -o bundles/$(echo $img | tr '/:' '_').json
209
209
+
done
210
210
+
```
211
211
+
212
212
+
### trust
213
213
+
214
214
+
Manage trust policies and trusted DIDs.
215
215
+
216
216
+
```bash
217
217
+
atcr-verify trust COMMAND [flags]
218
218
+
```
219
219
+
220
220
+
**Subcommands:**
221
221
+
222
222
+
**`trust list`** - List trusted DIDs
223
223
+
```bash
224
224
+
atcr-verify trust list
225
225
+
226
226
+
# Output:
227
227
+
# Trusted DIDs:
228
228
+
# - did:plc:alice123 (alice.bsky.social)
229
229
+
# - did:plc:bob456 (bob.example.com)
230
230
+
```
231
231
+
232
232
+
**`trust add DID`** - Add trusted DID
233
233
+
```bash
234
234
+
atcr-verify trust add did:plc:alice123
235
235
+
atcr-verify trust add did:plc:alice123 --name "Alice (DevOps)"
236
236
+
```
237
237
+
238
238
+
**`trust remove DID`** - Remove trusted DID
239
239
+
```bash
240
240
+
atcr-verify trust remove did:plc:alice123
241
241
+
```
242
242
+
243
243
+
**`trust policy validate`** - Validate trust policy file
244
244
+
```bash
245
245
+
atcr-verify trust policy validate policy.yaml
246
246
+
```
247
247
+
248
248
+
### version
249
249
+
250
250
+
Show version information.
251
251
+
252
252
+
```bash
253
253
+
atcr-verify version
254
254
+
255
255
+
# Output:
256
256
+
# atcr-verify version 1.0.0
257
257
+
# Go version: go1.21.5
258
258
+
# Commit: 3b5b89b
259
259
+
# Built: 2025-10-31T12:00:00Z
260
260
+
```
261
261
+
262
262
+
## Trust Policy
263
263
+
264
264
+
Trust policies define which signatures to trust and what to do when verification fails.
265
265
+
266
266
+
### Policy File Format
267
267
+
268
268
+
```yaml
269
269
+
version: 1.0
270
270
+
271
271
+
# Global settings
272
272
+
defaultAction: enforce # enforce, audit, allow
273
273
+
requireSignature: true
274
274
+
275
275
+
# Policies matched by image pattern (first match wins)
276
276
+
policies:
277
277
+
- name: production-images
278
278
+
description: "Production images must be signed by DevOps or Security"
279
279
+
scope: "atcr.io/*/prod-*"
280
280
+
require:
281
281
+
signature: true
282
282
+
trustedDIDs:
283
283
+
- did:plc:devops-team
284
284
+
- did:plc:security-team
285
285
+
minSignatures: 1
286
286
+
maxAge: 2592000 # 30 days in seconds
287
287
+
action: enforce
288
288
+
289
289
+
- name: staging-images
290
290
+
scope: "atcr.io/*/staging-*"
291
291
+
require:
292
292
+
signature: true
293
293
+
trustedDIDs:
294
294
+
- did:plc:devops-team
295
295
+
- did:plc:developers
296
296
+
minSignatures: 1
297
297
+
action: enforce
298
298
+
299
299
+
- name: dev-images
300
300
+
scope: "atcr.io/*/dev-*"
301
301
+
require:
302
302
+
signature: false
303
303
+
action: audit # Log but don't fail
304
304
+
305
305
+
# Trusted DID registry
306
306
+
trustedDIDs:
307
307
+
did:plc:devops-team:
308
308
+
name: "DevOps Team"
309
309
+
validFrom: "2024-01-01T00:00:00Z"
310
310
+
expiresAt: null
311
311
+
contact: "devops@example.com"
312
312
+
313
313
+
did:plc:security-team:
314
314
+
name: "Security Team"
315
315
+
validFrom: "2024-01-01T00:00:00Z"
316
316
+
expiresAt: null
317
317
+
318
318
+
did:plc:developers:
319
319
+
name: "Developer Team"
320
320
+
validFrom: "2024-06-01T00:00:00Z"
321
321
+
expiresAt: "2025-12-31T23:59:59Z"
322
322
+
```
323
323
+
324
324
+
### Policy Matching
325
325
+
326
326
+
Policies are evaluated in order. First match wins.
327
327
+
328
328
+
**Scope patterns:**
329
329
+
- `atcr.io/*/*` - All ATCR images
330
330
+
- `atcr.io/myorg/*` - All images from myorg
331
331
+
- `atcr.io/*/prod-*` - All images with "prod-" prefix
332
332
+
- `atcr.io/myorg/myapp` - Specific repository
333
333
+
- `atcr.io/myorg/myapp:v*` - Tag pattern matching
334
334
+
335
335
+
### Policy Actions
336
336
+
337
337
+
**`enforce`** - Reject if policy fails
338
338
+
- Exit code 4
339
339
+
- Blocks deployment
340
340
+
341
341
+
**`audit`** - Log but allow
342
342
+
- Exit code 0 (success)
343
343
+
- Warning message printed
344
344
+
345
345
+
**`allow`** - Always allow
346
346
+
- No verification performed
347
347
+
- Exit code 0
348
348
+
349
349
+
### Policy Requirements
350
350
+
351
351
+
**`signature: true`** - Require signature present
352
352
+
353
353
+
**`trustedDIDs`** - List of trusted DIDs
354
354
+
```yaml
355
355
+
trustedDIDs:
356
356
+
- did:plc:alice123
357
357
+
- did:web:example.com
358
358
+
```
359
359
+
360
360
+
**`minSignatures`** - Minimum number of signatures required
361
361
+
```yaml
362
362
+
minSignatures: 2 # Require 2 signatures
363
363
+
```
364
364
+
365
365
+
**`maxAge`** - Maximum signature age in seconds
366
366
+
```yaml
367
367
+
maxAge: 2592000 # 30 days
368
368
+
```
369
369
+
370
370
+
**`algorithms`** - Allowed signature algorithms
371
371
+
```yaml
372
372
+
algorithms:
373
373
+
- ECDSA-K256-SHA256
374
374
+
```
375
375
+
376
376
+
## Verification Flow
377
377
+
378
378
+
### 1. Image Resolution
379
379
+
380
380
+
```
381
381
+
Input: atcr.io/alice/myapp:latest
382
382
+
↓
383
383
+
Resolve tag to digest
384
384
+
↓
385
385
+
Output: sha256:abc123...
386
386
+
```
387
387
+
388
388
+
### 2. Signature Discovery
389
389
+
390
390
+
```
391
391
+
Query OCI Referrers API:
392
392
+
GET /v2/alice/myapp/referrers/sha256:abc123
393
393
+
?artifactType=application/vnd.atproto.signature.v1+json
394
394
+
↓
395
395
+
Returns: List of signature artifacts
396
396
+
↓
397
397
+
Download signature metadata blobs
398
398
+
```
399
399
+
400
400
+
### 3. DID Resolution
401
401
+
402
402
+
```
403
403
+
Extract DID from signature: did:plc:alice123
404
404
+
↓
405
405
+
Query PLC directory:
406
406
+
GET https://plc.directory/did:plc:alice123
407
407
+
↓
408
408
+
Extract public key from DID document
409
409
+
```
410
410
+
411
411
+
### 4. PDS Query
412
412
+
413
413
+
```
414
414
+
Get PDS endpoint from DID document
415
415
+
↓
416
416
+
Query for manifest record:
417
417
+
GET {pds}/xrpc/com.atproto.repo.getRecord
418
418
+
?repo=did:plc:alice123
419
419
+
&collection=io.atcr.manifest
420
420
+
&rkey=abc123
421
421
+
↓
422
422
+
Get commit CID from record
423
423
+
↓
424
424
+
Fetch commit data (includes signature)
425
425
+
```
426
426
+
427
427
+
### 5. Signature Verification
428
428
+
429
429
+
```
430
430
+
Extract signature bytes from commit
431
431
+
↓
432
432
+
Compute commit hash (SHA-256)
433
433
+
↓
434
434
+
Verify: ECDSA_K256(hash, signature, publicKey)
435
435
+
↓
436
436
+
Result: Valid or Invalid
437
437
+
```
438
438
+
439
439
+
### 6. Trust Policy Evaluation
440
440
+
441
441
+
```
442
442
+
Check if DID is in trustedDIDs list
443
443
+
↓
444
444
+
Check signature age < maxAge
445
445
+
↓
446
446
+
Check minSignatures satisfied
447
447
+
↓
448
448
+
Apply policy action (enforce/audit/allow)
449
449
+
```
450
450
+
451
451
+
## Integration Examples
452
452
+
453
453
+
### CI/CD Pipeline
454
454
+
455
455
+
**GitHub Actions:**
456
456
+
```yaml
457
457
+
name: Deploy
458
458
+
459
459
+
on:
460
460
+
push:
461
461
+
branches: [main]
462
462
+
463
463
+
jobs:
464
464
+
verify-and-deploy:
465
465
+
runs-on: ubuntu-latest
466
466
+
steps:
467
467
+
- name: Install atcr-verify
468
468
+
run: |
469
469
+
curl -L https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify-linux-amd64 -o atcr-verify
470
470
+
chmod +x atcr-verify
471
471
+
sudo mv atcr-verify /usr/local/bin/
472
472
+
473
473
+
- name: Verify image signature
474
474
+
run: |
475
475
+
atcr-verify ${{ env.IMAGE }} --policy .github/trust-policy.yaml
476
476
+
477
477
+
- name: Deploy to production
478
478
+
if: success()
479
479
+
run: kubectl set image deployment/app app=${{ env.IMAGE }}
480
480
+
```
481
481
+
482
482
+
**GitLab CI:**
483
483
+
```yaml
484
484
+
verify:
485
485
+
stage: verify
486
486
+
image: atcr.io/atcr/verify:latest
487
487
+
script:
488
488
+
- atcr-verify ${IMAGE} --policy trust-policy.yaml
489
489
+
490
490
+
deploy:
491
491
+
stage: deploy
492
492
+
dependencies:
493
493
+
- verify
494
494
+
script:
495
495
+
- kubectl set image deployment/app app=${IMAGE}
496
496
+
```
497
497
+
498
498
+
**Jenkins:**
499
499
+
```groovy
500
500
+
pipeline {
501
501
+
agent any
502
502
+
503
503
+
stages {
504
504
+
stage('Verify') {
505
505
+
steps {
506
506
+
sh 'atcr-verify ${IMAGE} --policy trust-policy.yaml'
507
507
+
}
508
508
+
}
509
509
+
510
510
+
stage('Deploy') {
511
511
+
when {
512
512
+
expression { currentBuild.result == 'SUCCESS' }
513
513
+
}
514
514
+
steps {
515
515
+
sh 'kubectl set image deployment/app app=${IMAGE}'
516
516
+
}
517
517
+
}
518
518
+
}
519
519
+
}
520
520
+
```
521
521
+
522
522
+
### Kubernetes Admission Controller
523
523
+
524
524
+
**Using as webhook backend:**
525
525
+
526
526
+
```go
527
527
+
// webhook server
528
528
+
func (h *Handler) ValidatePod(w http.ResponseWriter, r *http.Request) {
529
529
+
var admReq admissionv1.AdmissionReview
530
530
+
json.NewDecoder(r.Body).Decode(&admReq)
531
531
+
532
532
+
pod := &corev1.Pod{}
533
533
+
json.Unmarshal(admReq.Request.Object.Raw, pod)
534
534
+
535
535
+
// Verify each container image
536
536
+
for _, container := range pod.Spec.Containers {
537
537
+
cmd := exec.Command("atcr-verify", container.Image,
538
538
+
"--policy", "/etc/atcr/trust-policy.yaml",
539
539
+
"--quiet")
540
540
+
541
541
+
if err := cmd.Run(); err != nil {
542
542
+
// Verification failed
543
543
+
admResp := admissionv1.AdmissionReview{
544
544
+
Response: &admissionv1.AdmissionResponse{
545
545
+
UID: admReq.Request.UID,
546
546
+
Allowed: false,
547
547
+
Result: &metav1.Status{
548
548
+
Message: fmt.Sprintf("Image %s failed signature verification", container.Image),
549
549
+
},
550
550
+
},
551
551
+
}
552
552
+
json.NewEncoder(w).Encode(admResp)
553
553
+
return
554
554
+
}
555
555
+
}
556
556
+
557
557
+
// All images verified
558
558
+
admResp := admissionv1.AdmissionReview{
559
559
+
Response: &admissionv1.AdmissionResponse{
560
560
+
UID: admReq.Request.UID,
561
561
+
Allowed: true,
562
562
+
},
563
563
+
}
564
564
+
json.NewEncoder(w).Encode(admResp)
565
565
+
}
566
566
+
```
567
567
+
568
568
+
### Pre-Pull Verification
569
569
+
570
570
+
**Systemd service:**
571
571
+
```ini
572
572
+
# /etc/systemd/system/myapp.service
573
573
+
[Unit]
574
574
+
Description=My Application
575
575
+
After=docker.service
576
576
+
577
577
+
[Service]
578
578
+
Type=oneshot
579
579
+
ExecStartPre=/usr/local/bin/atcr-verify atcr.io/myorg/myapp:latest --policy /etc/atcr/policy.yaml
580
580
+
ExecStartPre=/usr/bin/docker pull atcr.io/myorg/myapp:latest
581
581
+
ExecStart=/usr/bin/docker run atcr.io/myorg/myapp:latest
582
582
+
Restart=on-failure
583
583
+
584
584
+
[Install]
585
585
+
WantedBy=multi-user.target
586
586
+
```
587
587
+
588
588
+
**Docker wrapper script:**
589
589
+
```bash
590
590
+
#!/bin/bash
591
591
+
# docker-secure-pull.sh
592
592
+
593
593
+
IMAGE="$1"
594
594
+
595
595
+
# Verify before pulling
596
596
+
if ! atcr-verify "$IMAGE" --policy ~/.atcr/trust-policy.yaml; then
597
597
+
echo "ERROR: Image signature verification failed"
598
598
+
exit 1
599
599
+
fi
600
600
+
601
601
+
# Pull if verified
602
602
+
docker pull "$IMAGE"
603
603
+
```
604
604
+
605
605
+
## Configuration
606
606
+
607
607
+
### Config File
608
608
+
609
609
+
Location: `~/.atcr/config.yaml`
610
610
+
611
611
+
```yaml
612
612
+
# Default trust policy
613
613
+
defaultPolicy: ~/.atcr/trust-policy.yaml
614
614
+
615
615
+
# Cache settings
616
616
+
cache:
617
617
+
enabled: true
618
618
+
directory: ~/.atcr/cache
619
619
+
ttl:
620
620
+
didDocuments: 3600 # 1 hour
621
621
+
commits: 600 # 10 minutes
622
622
+
623
623
+
# Network settings
624
624
+
timeout: 30s
625
625
+
retries: 3
626
626
+
627
627
+
# Output settings
628
628
+
output:
629
629
+
format: text # text, json, quiet
630
630
+
color: auto # auto, always, never
631
631
+
632
632
+
# Registry settings
633
633
+
registries:
634
634
+
atcr.io:
635
635
+
insecure: false
636
636
+
credentialsFile: ~/.docker/config.json
637
637
+
```
638
638
+
639
639
+
### Environment Variables
640
640
+
641
641
+
- `ATCR_CONFIG` - Config file path
642
642
+
- `ATCR_POLICY` - Default trust policy file
643
643
+
- `ATCR_CACHE_DIR` - Cache directory
644
644
+
- `ATCR_OUTPUT` - Output format (text, json, quiet)
645
645
+
- `ATCR_TIMEOUT` - Verification timeout
646
646
+
- `HTTP_PROXY` / `HTTPS_PROXY` - Proxy settings
647
647
+
- `NO_CACHE` - Disable caching
648
648
+
649
649
+
## Library Usage
650
650
+
651
651
+
`atcr-verify` can also be used as a Go library:
652
652
+
653
653
+
```go
654
654
+
import "github.com/atcr-io/atcr/pkg/verify"
655
655
+
656
656
+
func main() {
657
657
+
verifier := verify.NewVerifier(verify.Config{
658
658
+
Policy: policy,
659
659
+
Timeout: 30 * time.Second,
660
660
+
})
661
661
+
662
662
+
result, err := verifier.Verify(ctx, "atcr.io/alice/myapp:latest")
663
663
+
if err != nil {
664
664
+
log.Fatal(err)
665
665
+
}
666
666
+
667
667
+
if !result.Verified {
668
668
+
log.Fatal("Verification failed")
669
669
+
}
670
670
+
671
671
+
fmt.Printf("Verified by %s\n", result.Signature.DID)
672
672
+
}
673
673
+
```
674
674
+
675
675
+
## Performance
676
676
+
677
677
+
### Typical Verification Times
678
678
+
679
679
+
- **First verification:** 500-1000ms
680
680
+
- OCI Referrers API: 50-100ms
681
681
+
- DID resolution: 50-150ms
682
682
+
- PDS query: 100-300ms
683
683
+
- Signature verification: 1-5ms
684
684
+
685
685
+
- **Cached verification:** 50-150ms
686
686
+
- DID document cached
687
687
+
- Signature metadata cached
688
688
+
689
689
+
### Optimization Tips
690
690
+
691
691
+
1. **Enable caching** - DID documents change rarely
692
692
+
2. **Use offline bundles** - For air-gapped environments
693
693
+
3. **Parallel verification** - Verify multiple images concurrently
694
694
+
4. **Local trust policy** - Avoid remote policy fetches
695
695
+
696
696
+
## Troubleshooting
697
697
+
698
698
+
### Verification Fails
699
699
+
700
700
+
```bash
701
701
+
atcr-verify atcr.io/alice/myapp:latest --verbose
702
702
+
```
703
703
+
704
704
+
Common issues:
705
705
+
- **No signature found** - Image not signed, check Referrers API
706
706
+
- **DID resolution failed** - Network issue, check PLC directory
707
707
+
- **PDS unreachable** - Network issue, check PDS endpoint
708
708
+
- **Signature invalid** - Tampering detected or key mismatch
709
709
+
- **Trust policy violation** - DID not in trusted list
710
710
+
711
711
+
### Enable Debug Logging
712
712
+
713
713
+
```bash
714
714
+
ATCR_LOG_LEVEL=debug atcr-verify IMAGE
715
715
+
```
716
716
+
717
717
+
### Clear Cache
718
718
+
719
719
+
```bash
720
720
+
rm -rf ~/.atcr/cache
721
721
+
```
722
722
+
723
723
+
## See Also
724
724
+
725
725
+
- [ATProto Signatures](./ATPROTO_SIGNATURES.md) - How ATProto signing works
726
726
+
- [Integration Strategy](./INTEGRATION_STRATEGY.md) - Overview of integration approaches
727
727
+
- [Signature Integration](./SIGNATURE_INTEGRATION.md) - Tool-specific guides
728
728
+
- [Trust Policy Examples](../examples/verification/trust-policy.yaml)
+501
docs/ATPROTO_SIGNATURES.md
···
1
1
+
# ATProto Signatures for Container Images
2
2
+
3
3
+
## Overview
4
4
+
5
5
+
ATCR container images are **already cryptographically signed** through ATProto's repository commit system. Every manifest stored in a user's PDS is signed with the user's ATProto signing key, providing cryptographic proof of authorship and integrity.
6
6
+
7
7
+
This document explains:
8
8
+
- How ATProto signing works
9
9
+
- Why additional signing tools aren't needed
10
10
+
- How to bridge ATProto signatures to the OCI/ORAS ecosystem
11
11
+
- Trust model and security considerations
12
12
+
13
13
+
## Key Insight: Manifests Are Already Signed
14
14
+
15
15
+
When you push an image to ATCR:
16
16
+
17
17
+
```bash
18
18
+
docker push atcr.io/alice/myapp:latest
19
19
+
```
20
20
+
21
21
+
The following happens:
22
22
+
23
23
+
1. **AppView stores manifest** as an `io.atcr.manifest` record in alice's PDS
24
24
+
2. **PDS creates repository commit** containing the manifest record
25
25
+
3. **PDS signs the commit** with alice's ATProto signing key (ECDSA K-256)
26
26
+
4. **Signature is stored** in the repository commit object
27
27
+
28
28
+
**Result:** The manifest is cryptographically signed with alice's private key, and anyone can verify it using alice's public key from her DID document.
29
29
+
30
30
+
## ATProto Signing Mechanism
31
31
+
32
32
+
### Repository Commit Signing
33
33
+
34
34
+
ATProto uses a Merkle Search Tree (MST) to store records, and every modification creates a signed commit:
35
35
+
36
36
+
```
37
37
+
┌─────────────────────────────────────────────┐
38
38
+
│ Repository Commit │
39
39
+
├─────────────────────────────────────────────┤
40
40
+
│ DID: did:plc:alice123 │
41
41
+
│ Version: 3jzfkjqwdwa2a │
42
42
+
│ Previous: bafyreig7... (parent commit) │
43
43
+
│ Data CID: bafyreih8... (MST root) │
44
44
+
│ ┌───────────────────────────────────────┐ │
45
45
+
│ │ Signature (ECDSA K-256 + SHA-256) │ │
46
46
+
│ │ Signed with: alice's private key │ │
47
47
+
│ │ Value: 0x3045022100... (DER format) │ │
48
48
+
│ └───────────────────────────────────────┘ │
49
49
+
└─────────────────────────────────────────────┘
50
50
+
│
51
51
+
↓
52
52
+
┌─────────────────────┐
53
53
+
│ Merkle Search Tree │
54
54
+
│ (contains records) │
55
55
+
└─────────────────────┘
56
56
+
│
57
57
+
↓
58
58
+
┌────────────────────────────┐
59
59
+
│ io.atcr.manifest record │
60
60
+
│ Repository: myapp │
61
61
+
│ Digest: sha256:abc123... │
62
62
+
│ Layers: [...] │
63
63
+
└────────────────────────────┘
64
64
+
```
65
65
+
66
66
+
### Signature Algorithm
67
67
+
68
68
+
**Algorithm:** ECDSA with K-256 (secp256k1) curve + SHA-256 hash
69
69
+
- **Curve:** secp256k1 (same as Bitcoin, Ethereum)
70
70
+
- **Hash:** SHA-256
71
71
+
- **Format:** DER-encoded signature bytes
72
72
+
- **Variant:** "low-S" signatures (per BIP-0062)
73
73
+
74
74
+
**Signing process:**
75
75
+
1. Serialize commit data as DAG-CBOR
76
76
+
2. Hash with SHA-256
77
77
+
3. Sign hash with ECDSA K-256 private key
78
78
+
4. Store signature in commit object
79
79
+
80
80
+
### Public Key Distribution
81
81
+
82
82
+
Public keys are distributed via DID documents, accessible through DID resolution:
83
83
+
84
84
+
**DID Resolution Flow:**
85
85
+
```
86
86
+
did:plc:alice123
87
87
+
↓
88
88
+
Query PLC directory: https://plc.directory/did:plc:alice123
89
89
+
↓
90
90
+
DID Document:
91
91
+
{
92
92
+
"@context": ["https://www.w3.org/ns/did/v1"],
93
93
+
"id": "did:plc:alice123",
94
94
+
"verificationMethod": [{
95
95
+
"id": "did:plc:alice123#atproto",
96
96
+
"type": "Multikey",
97
97
+
"controller": "did:plc:alice123",
98
98
+
"publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z"
99
99
+
}],
100
100
+
"service": [{
101
101
+
"id": "#atproto_pds",
102
102
+
"type": "AtprotoPersonalDataServer",
103
103
+
"serviceEndpoint": "https://bsky.social"
104
104
+
}]
105
105
+
}
106
106
+
```
107
107
+
108
108
+
**Public key format:**
109
109
+
- **Encoding:** Multibase (base58btc with `z` prefix)
110
110
+
- **Codec:** Multicodec `0xE701` for K-256 keys
111
111
+
- **Example:** `zQ3sh...` decodes to 33-byte compressed public key
112
112
+
113
113
+
## Verification Process
114
114
+
115
115
+
To verify a manifest's signature:
116
116
+
117
117
+
### Step 1: Resolve Image to Manifest Digest
118
118
+
119
119
+
```bash
120
120
+
# Get manifest digest
121
121
+
DIGEST=$(crane digest atcr.io/alice/myapp:latest)
122
122
+
# Result: sha256:abc123...
123
123
+
```
124
124
+
125
125
+
### Step 2: Fetch Manifest Record from PDS
126
126
+
127
127
+
```bash
128
128
+
# Extract repository name from image reference
129
129
+
REPO="myapp"
130
130
+
131
131
+
# Query PDS for manifest record
132
132
+
curl "https://bsky.social/xrpc/com.atproto.repo.listRecords?\
133
133
+
repo=did:plc:alice123&\
134
134
+
collection=io.atcr.manifest&\
135
135
+
limit=100" | jq -r '.records[] | select(.value.digest == "sha256:abc123...")'
136
136
+
```
137
137
+
138
138
+
Response includes:
139
139
+
```json
140
140
+
{
141
141
+
"uri": "at://did:plc:alice123/io.atcr.manifest/abc123",
142
142
+
"cid": "bafyreig7...",
143
143
+
"value": {
144
144
+
"$type": "io.atcr.manifest",
145
145
+
"repository": "myapp",
146
146
+
"digest": "sha256:abc123...",
147
147
+
...
148
148
+
}
149
149
+
}
150
150
+
```
151
151
+
152
152
+
### Step 3: Fetch Repository Commit
153
153
+
154
154
+
```bash
155
155
+
# Get current repository state
156
156
+
curl "https://bsky.social/xrpc/com.atproto.sync.getRepo?\
157
157
+
did=did:plc:alice123" --output repo.car
158
158
+
159
159
+
# Extract commit from CAR file (requires ATProto tools)
160
160
+
# Commit includes signature over repository state
161
161
+
```
162
162
+
163
163
+
### Step 4: Resolve DID to Public Key
164
164
+
165
165
+
```bash
166
166
+
# Resolve DID document
167
167
+
curl "https://plc.directory/did:plc:alice123" | jq -r '.verificationMethod[0].publicKeyMultibase'
168
168
+
# Result: zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z
169
169
+
```
170
170
+
171
171
+
### Step 5: Verify Signature
172
172
+
173
173
+
```go
174
174
+
// Pseudocode for verification
175
175
+
import "github.com/bluesky-social/indigo/atproto/crypto"
176
176
+
177
177
+
// 1. Parse commit
178
178
+
commit := parseCommitFromCAR(repoCAR)
179
179
+
180
180
+
// 2. Extract signature bytes
181
181
+
signature := commit.Sig
182
182
+
183
183
+
// 3. Get bytes that were signed
184
184
+
bytesToVerify := commit.Unsigned().BytesForSigning()
185
185
+
186
186
+
// 4. Decode public key from multibase
187
187
+
pubKey := decodeMultibasePublicKey(publicKeyMultibase)
188
188
+
189
189
+
// 5. Verify ECDSA signature
190
190
+
valid := crypto.VerifySignature(pubKey, bytesToVerify, signature)
191
191
+
```
192
192
+
193
193
+
### Step 6: Verify Manifest Integrity
194
194
+
195
195
+
```bash
196
196
+
# Verify the manifest record's CID matches the content
197
197
+
# CID is content-addressed, so tampering changes the CID
198
198
+
```
199
199
+
200
200
+
## Bridging to OCI/ORAS Ecosystem
201
201
+
202
202
+
While ATProto signatures are cryptographically sound, the OCI ecosystem doesn't understand ATProto records. To make signatures discoverable, we create **ORAS signature artifacts** that reference the ATProto signature.
203
203
+
204
204
+
### ORAS Signature Artifact Format
205
205
+
206
206
+
```json
207
207
+
{
208
208
+
"schemaVersion": 2,
209
209
+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
210
210
+
"artifactType": "application/vnd.atproto.signature.v1+json",
211
211
+
"config": {
212
212
+
"mediaType": "application/vnd.oci.empty.v1+json",
213
213
+
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
214
214
+
"size": 2
215
215
+
},
216
216
+
"subject": {
217
217
+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
218
218
+
"digest": "sha256:abc123...",
219
219
+
"size": 1234
220
220
+
},
221
221
+
"layers": [
222
222
+
{
223
223
+
"mediaType": "application/vnd.atproto.signature.v1+json",
224
224
+
"digest": "sha256:sig789...",
225
225
+
"size": 512,
226
226
+
"annotations": {
227
227
+
"org.opencontainers.image.title": "atproto-signature.json"
228
228
+
}
229
229
+
}
230
230
+
],
231
231
+
"annotations": {
232
232
+
"io.atcr.atproto.did": "did:plc:alice123",
233
233
+
"io.atcr.atproto.pds": "https://bsky.social",
234
234
+
"io.atcr.atproto.recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123",
235
235
+
"io.atcr.atproto.commitCid": "bafyreih8...",
236
236
+
"io.atcr.atproto.signedAt": "2025-10-31T12:34:56.789Z",
237
237
+
"io.atcr.atproto.keyId": "did:plc:alice123#atproto"
238
238
+
}
239
239
+
}
240
240
+
```
241
241
+
242
242
+
**Key elements:**
243
243
+
244
244
+
1. **artifactType**: `application/vnd.atproto.signature.v1+json` - identifies this as an ATProto signature
245
245
+
2. **subject**: Links to the image manifest being signed
246
246
+
3. **layers**: Contains signature metadata blob
247
247
+
4. **annotations**: Quick-access metadata for verification
248
248
+
249
249
+
### Signature Metadata Blob
250
250
+
251
251
+
The layer blob contains detailed verification information:
252
252
+
253
253
+
```json
254
254
+
{
255
255
+
"$type": "io.atcr.atproto.signature",
256
256
+
"version": "1.0",
257
257
+
"subject": {
258
258
+
"digest": "sha256:abc123...",
259
259
+
"mediaType": "application/vnd.oci.image.manifest.v1+json"
260
260
+
},
261
261
+
"atproto": {
262
262
+
"did": "did:plc:alice123",
263
263
+
"handle": "alice.bsky.social",
264
264
+
"pdsEndpoint": "https://bsky.social",
265
265
+
"recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123",
266
266
+
"recordCid": "bafyreig7...",
267
267
+
"commitCid": "bafyreih8...",
268
268
+
"commitRev": "3jzfkjqwdwa2a",
269
269
+
"signedAt": "2025-10-31T12:34:56.789Z"
270
270
+
},
271
271
+
"signature": {
272
272
+
"algorithm": "ECDSA-K256-SHA256",
273
273
+
"keyId": "did:plc:alice123#atproto",
274
274
+
"publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z"
275
275
+
},
276
276
+
"verification": {
277
277
+
"method": "atproto-repo-commit",
278
278
+
"instructions": "Fetch repository commit from PDS and verify signature using public key from DID document"
279
279
+
}
280
280
+
}
281
281
+
```
282
282
+
283
283
+
### Discovery via Referrers API
284
284
+
285
285
+
ORAS artifacts are discoverable via the OCI Referrers API:
286
286
+
287
287
+
```bash
288
288
+
# Query for signature artifacts
289
289
+
curl "https://atcr.io/v2/alice/myapp/referrers/sha256:abc123?\
290
290
+
artifactType=application/vnd.atproto.signature.v1+json"
291
291
+
```
292
292
+
293
293
+
Response:
294
294
+
```json
295
295
+
{
296
296
+
"schemaVersion": 2,
297
297
+
"mediaType": "application/vnd.oci.image.index.v1+json",
298
298
+
"manifests": [
299
299
+
{
300
300
+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
301
301
+
"digest": "sha256:sig789...",
302
302
+
"size": 1234,
303
303
+
"artifactType": "application/vnd.atproto.signature.v1+json",
304
304
+
"annotations": {
305
305
+
"io.atcr.atproto.did": "did:plc:alice123",
306
306
+
"io.atcr.atproto.signedAt": "2025-10-31T12:34:56.789Z"
307
307
+
}
308
308
+
}
309
309
+
]
310
310
+
}
311
311
+
```
312
312
+
313
313
+
## Trust Model
314
314
+
315
315
+
### What ATProto Signatures Prove
316
316
+
317
317
+
✅ **Authenticity**: Image was published by the DID owner
318
318
+
✅ **Integrity**: Image manifest hasn't been tampered with since signing
319
319
+
✅ **Non-repudiation**: Only the DID owner could have created this signature
320
320
+
✅ **Timestamp**: When the image was signed (commit timestamp)
321
321
+
322
322
+
### What ATProto Signatures Don't Prove
323
323
+
324
324
+
❌ **Safety**: Image doesn't contain vulnerabilities (use vulnerability scanning)
325
325
+
❌ **DID trustworthiness**: Whether the DID owner is trustworthy (trust policy decision)
326
326
+
❌ **Key security**: Private key wasn't compromised (same limitation as all PKI)
327
327
+
❌ **PDS honesty**: PDS operator serves correct data (verify across multiple sources)
328
328
+
329
329
+
### Trust Dependencies
330
330
+
331
331
+
1. **DID Resolution**: Must correctly resolve DID to public key
332
332
+
- **Mitigation**: Use multiple resolvers, cache DID documents
333
333
+
334
334
+
2. **PDS Availability**: Must query PDS to verify signatures
335
335
+
- **Mitigation**: Embed signature bytes in ORAS blob for offline verification
336
336
+
337
337
+
3. **PDS Honesty**: PDS could serve fake/unsigned records
338
338
+
- **Mitigation**: Signature verification prevents this (can't forge signature)
339
339
+
340
340
+
4. **Key Security**: User's private key could be compromised
341
341
+
- **Mitigation**: Key rotation via DID document updates, short-lived credentials
342
342
+
343
343
+
5. **Algorithm Security**: ECDSA K-256 must remain secure
344
344
+
- **Status**: Well-studied, same as Bitcoin/Ethereum (widely trusted)
345
345
+
346
346
+
### Comparison with Other Signing Systems
347
347
+
348
348
+
| Aspect | ATProto Signatures | Cosign (Keyless) | Notary v2 |
349
349
+
|--------|-------------------|------------------|-----------|
350
350
+
| **Identity** | DID (decentralized) | OIDC (federated) | X.509 (PKI) |
351
351
+
| **Key Management** | PDS signing keys | Ephemeral (Fulcio) | User-managed |
352
352
+
| **Trust Anchor** | DID resolution | Fulcio CA + Rekor | Certificate chain |
353
353
+
| **Transparency Log** | ATProto firehose | Rekor | Optional |
354
354
+
| **Offline Verification** | Limited* | No | Yes |
355
355
+
| **Decentralization** | High | Medium | Low |
356
356
+
| **Complexity** | Low | High | Medium |
357
357
+
358
358
+
*Can be improved by embedding signature bytes in ORAS blob
359
359
+
360
360
+
### Security Considerations
361
361
+
362
362
+
**Threat: Man-in-the-Middle Attack**
363
363
+
- **Attack**: Intercept PDS queries, serve fake records
364
364
+
- **Defense**: TLS for PDS communication, verify signature with public key from DID document
365
365
+
- **Result**: Attacker can't forge signature without private key
366
366
+
367
367
+
**Threat: Compromised PDS**
368
368
+
- **Attack**: PDS operator serves unsigned/fake manifests
369
369
+
- **Defense**: Signature verification fails (PDS can't sign without user's private key)
370
370
+
- **Result**: Protected
371
371
+
372
372
+
**Threat: Key Compromise**
373
373
+
- **Attack**: Attacker steals user's ATProto signing key
374
374
+
- **Defense**: Key rotation via DID document, revoke old keys
375
375
+
- **Result**: Same as any PKI system (rotate keys quickly)
376
376
+
377
377
+
**Threat: Replay Attack**
378
378
+
- **Attack**: Replay old signed manifest to rollback to vulnerable version
379
379
+
- **Defense**: Check commit timestamp, verify commit is in current repository DAG
380
380
+
- **Result**: Protected (commits form immutable chain)
381
381
+
382
382
+
**Threat: DID Takeover**
383
383
+
- **Attack**: Attacker gains control of user's DID (rotation keys)
384
384
+
- **Defense**: Monitor DID document changes, verify key history
385
385
+
- **Result**: Serious but requires compromising rotation keys (harder than signing keys)
386
386
+
387
387
+
## Implementation Strategy
388
388
+
389
389
+
### Automatic Signature Artifact Creation
390
390
+
391
391
+
When AppView stores a manifest in a user's PDS:
392
392
+
393
393
+
1. **Store manifest record** (existing behavior)
394
394
+
2. **Get commit response** with commit CID and revision
395
395
+
3. **Create ORAS signature artifact**:
396
396
+
- Build metadata blob (JSON)
397
397
+
- Upload blob to hold storage
398
398
+
- Create ORAS manifest with subject = image manifest
399
399
+
- Store ORAS manifest (creates referrer link)
400
400
+
401
401
+
### Storage Location
402
402
+
403
403
+
Signature artifacts follow the same pattern as SBOMs:
404
404
+
- **Metadata blobs**: Stored in hold's blob storage
405
405
+
- **ORAS manifests**: Stored in hold's embedded PDS
406
406
+
- **Discovery**: Via OCI Referrers API
407
407
+
408
408
+
### Verification Tools
409
409
+
410
410
+
**Option 1: Custom CLI tool (`atcr-verify`)**
411
411
+
```bash
412
412
+
atcr-verify atcr.io/alice/myapp:latest
413
413
+
# → Queries referrers API
414
414
+
# → Fetches signature metadata
415
415
+
# → Resolves DID → public key
416
416
+
# → Queries PDS for commit
417
417
+
# → Verifies signature
418
418
+
```
419
419
+
420
420
+
**Option 2: Shell script (curl + jq)**
421
421
+
- See `docs/SIGNATURE_INTEGRATION.md` for examples
422
422
+
423
423
+
**Option 3: Kubernetes admission controller**
424
424
+
- Custom webhook that runs verification
425
425
+
- Rejects pods with unsigned/invalid signatures
426
426
+
427
427
+
## Benefits of ATProto Signatures
428
428
+
429
429
+
### Compared to No Signing
430
430
+
431
431
+
✅ **Cryptographic proof** of image authorship
432
432
+
✅ **Tamper detection** for manifests
433
433
+
✅ **Identity binding** via DIDs
434
434
+
✅ **Audit trail** via ATProto repository history
435
435
+
436
436
+
### Compared to Cosign/Notary
437
437
+
438
438
+
✅ **No additional signing required** (already signed by PDS)
439
439
+
✅ **Decentralized identity** (DIDs, not CAs)
440
440
+
✅ **Simpler infrastructure** (no Fulcio, no Rekor, no TUF)
441
441
+
✅ **Consistent with ATCR's architecture** (ATProto-native)
442
442
+
✅ **Lower operational overhead** (reuse existing PDS infrastructure)
443
443
+
444
444
+
### Trade-offs
445
445
+
446
446
+
⚠️ **Custom verification tools required** (standard tools won't work)
447
447
+
⚠️ **Online verification preferred** (need to query PDS)
448
448
+
⚠️ **Different trust model** (trust DIDs, not CAs)
449
449
+
⚠️ **Ecosystem maturity** (newer approach, less tooling)
450
450
+
451
451
+
## Future Enhancements
452
452
+
453
453
+
### Short-term
454
454
+
455
455
+
1. **Offline verification**: Embed signature bytes in ORAS blob
456
456
+
2. **Multi-PDS verification**: Check signature across multiple PDSs
457
457
+
3. **Key rotation support**: Handle historical key validity
458
458
+
459
459
+
### Medium-term
460
460
+
461
461
+
4. **Timestamp service**: RFC 3161 timestamps for long-term validity
462
462
+
5. **Multi-signature**: Require N signatures from M DIDs
463
463
+
6. **Transparency log integration**: Record verifications in public log
464
464
+
465
465
+
### Long-term
466
466
+
467
467
+
7. **IANA registration**: Register `application/vnd.atproto.signature.v1+json`
468
468
+
8. **Standards proposal**: ATProto signature spec to ORAS/OCI
469
469
+
9. **Cross-ecosystem bridges**: Convert to Cosign/Notary formats
470
470
+
471
471
+
## Conclusion
472
472
+
473
473
+
ATCR images are already cryptographically signed through ATProto's repository commit system. By creating ORAS signature artifacts that reference these existing signatures, we can:
474
474
+
475
475
+
- ✅ Make signatures discoverable to OCI tooling
476
476
+
- ✅ Maintain ATProto as the source of truth
477
477
+
- ✅ Provide verification tools for users and clusters
478
478
+
- ✅ Avoid duplicating signing infrastructure
479
479
+
480
480
+
This approach leverages ATProto's strengths (decentralized identity, built-in signing) while bridging to the OCI ecosystem through standard ORAS artifacts.
481
481
+
482
482
+
## References
483
483
+
484
484
+
### ATProto Specifications
485
485
+
- [ATProto Repository Specification](https://atproto.com/specs/repository)
486
486
+
- [ATProto Data Model](https://atproto.com/specs/data-model)
487
487
+
- [ATProto DID Methods](https://atproto.com/specs/did)
488
488
+
489
489
+
### OCI/ORAS Specifications
490
490
+
- [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec)
491
491
+
- [OCI Referrers API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers)
492
492
+
- [ORAS Artifacts](https://oras.land/docs/)
493
493
+
494
494
+
### Cryptography
495
495
+
- [ECDSA (secp256k1)](https://en.bitcoin.it/wiki/Secp256k1)
496
496
+
- [Multibase Encoding](https://github.com/multiformats/multibase)
497
497
+
- [Multicodec](https://github.com/multiformats/multicodec)
498
498
+
499
499
+
### Related Documentation
500
500
+
- [SBOM Scanning](./SBOM_SCANNING.md) - Similar ORAS artifact pattern
501
501
+
- [Signature Integration](./SIGNATURE_INTEGRATION.md) - Practical integration examples
+728
docs/DEVELOPMENT.md
···
1
1
+
# Development Workflow for ATCR
2
2
+
3
3
+
## The Problem
4
4
+
5
5
+
**Current development cycle with Docker:**
6
6
+
1. Edit CSS, JS, template, or Go file
7
7
+
2. Run `docker compose build` (rebuilds entire image)
8
8
+
3. Run `docker compose up` (restart container)
9
9
+
4. Wait **2-3 minutes** for changes to appear
10
10
+
5. Test, find issue, repeat...
11
11
+
12
12
+
**Why it's slow:**
13
13
+
- All assets embedded via `embed.FS` at compile time
14
14
+
- Multi-stage Docker build compiles everything from scratch
15
15
+
- No development mode exists
16
16
+
- Final image uses `scratch` base (no tools, no hot reload)
17
17
+
18
18
+
## The Solution
19
19
+
20
20
+
**Development setup combining:**
21
21
+
1. **Dockerfile.devel** - Development-focused container (golang base, not scratch)
22
22
+
2. **Volume mounts** - Live code editing (changes appear instantly in container)
23
23
+
3. **DirFS** - Skip embed, read templates/CSS/JS from filesystem
24
24
+
4. **Air** - Auto-rebuild on Go code changes
25
25
+
26
26
+
**Results:**
27
27
+
- CSS/JS/Template changes: **Instant** (0 seconds, just refresh browser)
28
28
+
- Go code changes: **2-5 seconds** (vs 2-3 minutes)
29
29
+
- Production builds: **Unchanged** (still optimized with embed.FS)
30
30
+
31
31
+
## How It Works
32
32
+
33
33
+
### Architecture Flow
34
34
+
35
35
+
```
36
36
+
┌─────────────────────────────────────────────────────┐
37
37
+
│ Your Editor (VSCode, etc) │
38
38
+
│ Edit: style.css, app.js, *.html, *.go files │
39
39
+
└─────────────────┬───────────────────────────────────┘
40
40
+
│ (files saved to disk)
41
41
+
▼
42
42
+
┌─────────────────────────────────────────────────────┐
43
43
+
│ Volume Mount (docker-compose.dev.yml) │
44
44
+
│ volumes: │
45
45
+
│ - .:/app (entire codebase mounted) │
46
46
+
└─────────────────┬───────────────────────────────────┘
47
47
+
│ (changes appear instantly in container)
48
48
+
▼
49
49
+
┌─────────────────────────────────────────────────────┐
50
50
+
│ Container (golang:1.25.2 base, has all tools) │
51
51
+
│ │
52
52
+
│ ┌──────────────────────────────────────┐ │
53
53
+
│ │ Air (hot reload tool) │ │
54
54
+
│ │ Watches: *.go, *.html, *.css, *.js │ │
55
55
+
│ │ │ │
56
56
+
│ │ On change: │ │
57
57
+
│ │ - *.go → rebuild binary (2-5s) │ │
58
58
+
│ │ - templates/css/js → restart only │ │
59
59
+
│ └──────────────────────────────────────┘ │
60
60
+
│ │ │
61
61
+
│ ▼ │
62
62
+
│ ┌──────────────────────────────────────┐ │
63
63
+
│ │ ATCR AppView (ATCR_DEV_MODE=true) │ │
64
64
+
│ │ │ │
65
65
+
│ │ ui.go checks DEV_MODE: │ │
66
66
+
│ │ if DEV_MODE: │ │
67
67
+
│ │ templatesFS = os.DirFS("...") │ │
68
68
+
│ │ staticFS = os.DirFS("...") │ │
69
69
+
│ │ else: │ │
70
70
+
│ │ use embed.FS (production) │ │
71
71
+
│ │ │ │
72
72
+
│ │ Result: Reads from mounted files │ │
73
73
+
│ └──────────────────────────────────────┘ │
74
74
+
└─────────────────────────────────────────────────────┘
75
75
+
```
76
76
+
77
77
+
### Change Scenarios
78
78
+
79
79
+
#### Scenario 1: Edit CSS/JS/Templates
80
80
+
```
81
81
+
1. Edit pkg/appview/static/css/style.css in VSCode
82
82
+
2. Save file
83
83
+
3. Change appears in container via volume mount (instant)
84
84
+
4. App uses os.DirFS → reads new file from disk (instant)
85
85
+
5. Refresh browser → see changes
86
86
+
```
87
87
+
**Time:** **Instant** (0 seconds)
88
88
+
**No rebuild, no restart!**
89
89
+
90
90
+
#### Scenario 2: Edit Go Code
91
91
+
```
92
92
+
1. Edit pkg/appview/handlers/home.go
93
93
+
2. Save file
94
94
+
3. Air detects .go file change
95
95
+
4. Air runs: go build -o ./tmp/atcr-appview ./cmd/appview
96
96
+
5. Air kills old process and starts new binary
97
97
+
6. App runs with new code
98
98
+
```
99
99
+
**Time:** **2-5 seconds**
100
100
+
**Fast incremental build!**
101
101
+
102
102
+
## Implementation
103
103
+
104
104
+
### Step 1: Create Dockerfile.devel
105
105
+
106
106
+
Create `Dockerfile.devel` in project root:
107
107
+
108
108
+
```dockerfile
109
109
+
# Development Dockerfile with hot reload support
110
110
+
FROM golang:1.25.2-trixie
111
111
+
112
112
+
# Install Air for hot reload
113
113
+
RUN go install github.com/cosmtrek/air@latest
114
114
+
115
115
+
# Install SQLite (required for CGO in ATCR)
116
116
+
RUN apt-get update && apt-get install -y \
117
117
+
sqlite3 \
118
118
+
libsqlite3-dev \
119
119
+
&& rm -rf /var/lib/apt/lists/*
120
120
+
121
121
+
WORKDIR /app
122
122
+
123
123
+
# Copy dependency files and download (cached layer)
124
124
+
COPY go.mod go.sum ./
125
125
+
RUN go mod download
126
126
+
127
127
+
# Note: Source code comes from volume mount
128
128
+
# (no COPY . . needed - that's the whole point!)
129
129
+
130
130
+
# Air will handle building and running
131
131
+
CMD ["air", "-c", ".air.toml"]
132
132
+
```
133
133
+
134
134
+
### Step 2: Create docker-compose.dev.yml
135
135
+
136
136
+
Create `docker-compose.dev.yml` in project root:
137
137
+
138
138
+
```yaml
139
139
+
version: '3.8'
140
140
+
141
141
+
services:
142
142
+
atcr-appview:
143
143
+
build:
144
144
+
context: .
145
145
+
dockerfile: Dockerfile.devel
146
146
+
volumes:
147
147
+
# Mount entire codebase (live editing)
148
148
+
- .:/app
149
149
+
# Cache Go modules (faster rebuilds)
150
150
+
- go-cache:/go/pkg/mod
151
151
+
# Persist SQLite database
152
152
+
- atcr-ui-dev:/var/lib/atcr
153
153
+
environment:
154
154
+
# Enable development mode (uses os.DirFS)
155
155
+
ATCR_DEV_MODE: "true"
156
156
+
157
157
+
# AppView configuration
158
158
+
ATCR_HTTP_ADDR: ":5000"
159
159
+
ATCR_BASE_URL: "http://localhost:5000"
160
160
+
ATCR_DEFAULT_HOLD_DID: "did:web:hold01.atcr.io"
161
161
+
162
162
+
# Database
163
163
+
ATCR_UI_DATABASE_PATH: "/var/lib/atcr/ui.db"
164
164
+
165
165
+
# Auth
166
166
+
ATCR_AUTH_KEY_PATH: "/var/lib/atcr/auth/private-key.pem"
167
167
+
168
168
+
# UI
169
169
+
ATCR_UI_ENABLED: "true"
170
170
+
171
171
+
# Jetstream (optional)
172
172
+
# JETSTREAM_URL: "wss://jetstream2.us-east.bsky.network/subscribe"
173
173
+
# ATCR_BACKFILL_ENABLED: "false"
174
174
+
ports:
175
175
+
- "5000:5000"
176
176
+
networks:
177
177
+
- atcr-dev
178
178
+
179
179
+
# Add other services as needed (postgres, hold, etc)
180
180
+
# atcr-hold:
181
181
+
# ...
182
182
+
183
183
+
networks:
184
184
+
atcr-dev:
185
185
+
driver: bridge
186
186
+
187
187
+
volumes:
188
188
+
go-cache:
189
189
+
atcr-ui-dev:
190
190
+
```
191
191
+
192
192
+
### Step 3: Create .air.toml
193
193
+
194
194
+
Create `.air.toml` in project root:
195
195
+
196
196
+
```toml
197
197
+
# Air configuration for hot reload
198
198
+
# https://github.com/cosmtrek/air
199
199
+
200
200
+
root = "."
201
201
+
testdata_dir = "testdata"
202
202
+
tmp_dir = "tmp"
203
203
+
204
204
+
[build]
205
205
+
# Arguments to pass to binary (AppView needs "serve")
206
206
+
args_bin = ["serve"]
207
207
+
208
208
+
# Where to output the built binary
209
209
+
bin = "./tmp/atcr-appview"
210
210
+
211
211
+
# Build command
212
212
+
cmd = "go build -o ./tmp/atcr-appview ./cmd/appview"
213
213
+
214
214
+
# Delay before rebuilding (ms) - debounce rapid saves
215
215
+
delay = 1000
216
216
+
217
217
+
# Directories to exclude from watching
218
218
+
exclude_dir = [
219
219
+
"tmp",
220
220
+
"vendor",
221
221
+
"bin",
222
222
+
".git",
223
223
+
"node_modules",
224
224
+
"testdata"
225
225
+
]
226
226
+
227
227
+
# Files to exclude from watching
228
228
+
exclude_file = []
229
229
+
230
230
+
# Regex patterns to exclude
231
231
+
exclude_regex = ["_test\\.go"]
232
232
+
233
233
+
# Don't rebuild if file content unchanged
234
234
+
exclude_unchanged = false
235
235
+
236
236
+
# Follow symlinks
237
237
+
follow_symlink = false
238
238
+
239
239
+
# Full command to run (leave empty to use cmd + bin)
240
240
+
full_bin = ""
241
241
+
242
242
+
# Directories to include (empty = all)
243
243
+
include_dir = []
244
244
+
245
245
+
# File extensions to watch
246
246
+
include_ext = ["go", "html", "css", "js"]
247
247
+
248
248
+
# Specific files to watch
249
249
+
include_file = []
250
250
+
251
251
+
# Delay before killing old process (s)
252
252
+
kill_delay = "0s"
253
253
+
254
254
+
# Log file for build errors
255
255
+
log = "build-errors.log"
256
256
+
257
257
+
# Use polling instead of fsnotify (for Docker/VM)
258
258
+
poll = false
259
259
+
poll_interval = 0
260
260
+
261
261
+
# Rerun binary if it exits
262
262
+
rerun = false
263
263
+
rerun_delay = 500
264
264
+
265
265
+
# Send interrupt signal instead of kill
266
266
+
send_interrupt = false
267
267
+
268
268
+
# Stop on build error
269
269
+
stop_on_error = false
270
270
+
271
271
+
[color]
272
272
+
# Colorize output
273
273
+
app = ""
274
274
+
build = "yellow"
275
275
+
main = "magenta"
276
276
+
runner = "green"
277
277
+
watcher = "cyan"
278
278
+
279
279
+
[log]
280
280
+
# Show only app logs (not build logs)
281
281
+
main_only = false
282
282
+
283
283
+
# Add timestamp to logs
284
284
+
time = false
285
285
+
286
286
+
[misc]
287
287
+
# Clean tmp directory on exit
288
288
+
clean_on_exit = false
289
289
+
290
290
+
[screen]
291
291
+
# Clear screen on rebuild
292
292
+
clear_on_rebuild = false
293
293
+
294
294
+
# Keep scrollback
295
295
+
keep_scroll = true
296
296
+
```
297
297
+
298
298
+
### Step 4: Modify pkg/appview/ui.go
299
299
+
300
300
+
Add conditional filesystem loading to `pkg/appview/ui.go`:
301
301
+
302
302
+
```go
303
303
+
package appview
304
304
+
305
305
+
import (
306
306
+
"embed"
307
307
+
"html/template"
308
308
+
"io/fs"
309
309
+
"log"
310
310
+
"net/http"
311
311
+
"os"
312
312
+
)
313
313
+
314
314
+
// Embedded assets (used in production)
315
315
+
//go:embed templates/**/*.html
316
316
+
var embeddedTemplatesFS embed.FS
317
317
+
318
318
+
//go:embed static
319
319
+
var embeddedStaticFS embed.FS
320
320
+
321
321
+
// Actual filesystems used at runtime (conditional)
322
322
+
var templatesFS fs.FS
323
323
+
var staticFS fs.FS
324
324
+
325
325
+
func init() {
326
326
+
// Development mode: read from filesystem for instant updates
327
327
+
if os.Getenv("ATCR_DEV_MODE") == "true" {
328
328
+
log.Println("🔧 DEV MODE: Using filesystem for templates and static assets")
329
329
+
templatesFS = os.DirFS("pkg/appview/templates")
330
330
+
staticFS = os.DirFS("pkg/appview/static")
331
331
+
} else {
332
332
+
// Production mode: use embedded assets
333
333
+
log.Println("📦 PRODUCTION MODE: Using embedded assets")
334
334
+
templatesFS = embeddedTemplatesFS
335
335
+
staticFS = embeddedStaticFS
336
336
+
}
337
337
+
}
338
338
+
339
339
+
// Templates returns parsed HTML templates
340
340
+
func Templates() *template.Template {
341
341
+
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
342
342
+
if err != nil {
343
343
+
log.Fatalf("Failed to parse templates: %v", err)
344
344
+
}
345
345
+
return tmpl
346
346
+
}
347
347
+
348
348
+
// StaticHandler returns a handler for static files
349
349
+
func StaticHandler() http.Handler {
350
350
+
sub, err := fs.Sub(staticFS, "static")
351
351
+
if err != nil {
352
352
+
log.Fatalf("Failed to create static sub-filesystem: %v", err)
353
353
+
}
354
354
+
return http.FileServer(http.FS(sub))
355
355
+
}
356
356
+
```
357
357
+
358
358
+
**Important:** Update the `Templates()` function to NOT cache templates in dev mode:
359
359
+
360
360
+
```go
361
361
+
// Templates returns parsed HTML templates
362
362
+
func Templates() *template.Template {
363
363
+
// In dev mode, reparse templates on every request (instant updates)
364
364
+
// In production, this could be cached
365
365
+
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
366
366
+
if err != nil {
367
367
+
log.Fatalf("Failed to parse templates: %v", err)
368
368
+
}
369
369
+
return tmpl
370
370
+
}
371
371
+
```
372
372
+
373
373
+
If you're caching templates, wrap it with a dev mode check:
374
374
+
375
375
+
```go
376
376
+
var templateCache *template.Template
377
377
+
378
378
+
func Templates() *template.Template {
379
379
+
// Development: reparse every time (instant updates)
380
380
+
if os.Getenv("ATCR_DEV_MODE") == "true" {
381
381
+
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
382
382
+
if err != nil {
383
383
+
log.Printf("Template parse error: %v", err)
384
384
+
return template.New("error")
385
385
+
}
386
386
+
return tmpl
387
387
+
}
388
388
+
389
389
+
// Production: use cached templates
390
390
+
if templateCache == nil {
391
391
+
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
392
392
+
if err != nil {
393
393
+
log.Fatalf("Failed to parse templates: %v", err)
394
394
+
}
395
395
+
templateCache = tmpl
396
396
+
}
397
397
+
return templateCache
398
398
+
}
399
399
+
```
400
400
+
401
401
+
### Step 5: Add to .gitignore
402
402
+
403
403
+
Add Air's temporary directory to `.gitignore`:
404
404
+
405
405
+
```
406
406
+
# Air hot reload
407
407
+
tmp/
408
408
+
build-errors.log
409
409
+
```
410
410
+
411
411
+
## Usage
412
412
+
413
413
+
### Starting Development Environment
414
414
+
415
415
+
```bash
416
416
+
# Build and start dev container
417
417
+
docker compose -f docker-compose.dev.yml up --build
418
418
+
419
419
+
# Or run in background
420
420
+
docker compose -f docker-compose.dev.yml up -d
421
421
+
422
422
+
# View logs
423
423
+
docker compose -f docker-compose.dev.yml logs -f atcr-appview
424
424
+
```
425
425
+
426
426
+
You should see Air starting:
427
427
+
428
428
+
```
429
429
+
atcr-appview | 🔧 DEV MODE: Using filesystem for templates and static assets
430
430
+
atcr-appview |
431
431
+
atcr-appview | __ _ ___
432
432
+
atcr-appview | / /\ | | | |_)
433
433
+
atcr-appview | /_/--\ |_| |_| \_ , built with Go
434
434
+
atcr-appview |
435
435
+
atcr-appview | watching .
436
436
+
atcr-appview | !exclude tmp
437
437
+
atcr-appview | building...
438
438
+
atcr-appview | running...
439
439
+
```
440
440
+
441
441
+
### Development Workflow
442
442
+
443
443
+
#### 1. Edit Templates/CSS/JS (Instant Updates)
444
444
+
445
445
+
```bash
446
446
+
# Edit any template, CSS, or JS file
447
447
+
vim pkg/appview/templates/pages/home.html
448
448
+
vim pkg/appview/static/css/style.css
449
449
+
vim pkg/appview/static/js/app.js
450
450
+
451
451
+
# Save file → changes appear instantly
452
452
+
# Just refresh browser (Cmd+R / Ctrl+R)
453
453
+
```
454
454
+
455
455
+
**No rebuild, no restart!** Air might restart the app, but it's instant since no compilation is needed.
456
456
+
457
457
+
#### 2. Edit Go Code (Fast Rebuild)
458
458
+
459
459
+
```bash
460
460
+
# Edit any Go file
461
461
+
vim pkg/appview/handlers/home.go
462
462
+
463
463
+
# Save file → Air detects change
464
464
+
# Air output shows:
465
465
+
# building...
466
466
+
# build successful in 2.3s
467
467
+
# restarting...
468
468
+
469
469
+
# Refresh browser to see changes
470
470
+
```
471
471
+
472
472
+
**2-5 second rebuild** instead of 2-3 minutes!
473
473
+
474
474
+
### Stopping Development Environment
475
475
+
476
476
+
```bash
477
477
+
# Stop containers
478
478
+
docker compose -f docker-compose.dev.yml down
479
479
+
480
480
+
# Stop and remove volumes (fresh start)
481
481
+
docker compose -f docker-compose.dev.yml down -v
482
482
+
```
483
483
+
484
484
+
## Production Builds
485
485
+
486
486
+
**Production builds are completely unchanged:**
487
487
+
488
488
+
```bash
489
489
+
# Production uses normal Dockerfile (embed.FS, scratch base)
490
490
+
docker compose build
491
491
+
492
492
+
# Or specific service
493
493
+
docker compose build atcr-appview
494
494
+
495
495
+
# Run production
496
496
+
docker compose up
497
497
+
```
498
498
+
499
499
+
**Why it works:**
500
500
+
- Production doesn't set `ATCR_DEV_MODE=true`
501
501
+
- `ui.go` defaults to embedded assets when env var is unset
502
502
+
- Production Dockerfile still uses multi-stage build to scratch
503
503
+
- No development dependencies in production image
504
504
+
505
505
+
## Comparison
506
506
+
507
507
+
| Change Type | Before (docker compose) | After (dev setup) | Improvement |
508
508
+
|-------------|------------------------|-------------------|-------------|
509
509
+
| Edit CSS | 2-3 minutes | **Instant (0s)** | ♾️x faster |
510
510
+
| Edit JS | 2-3 minutes | **Instant (0s)** | ♾️x faster |
511
511
+
| Edit Template | 2-3 minutes | **Instant (0s)** | ♾️x faster |
512
512
+
| Edit Go Code | 2-3 minutes | **2-5 seconds** | 24-90x faster |
513
513
+
| Production Build | Same | **Same** | No change |
514
514
+
515
515
+
## Advanced: Local Development (No Docker)
516
516
+
517
517
+
For even faster development, run locally without Docker:
518
518
+
519
519
+
```bash
520
520
+
# Set environment variables
521
521
+
export ATCR_DEV_MODE=true
522
522
+
export ATCR_HTTP_ADDR=:5000
523
523
+
export ATCR_BASE_URL=http://localhost:5000
524
524
+
export ATCR_DEFAULT_HOLD_DID=did:web:hold01.atcr.io
525
525
+
export ATCR_UI_DATABASE_PATH=/tmp/atcr-ui.db
526
526
+
export ATCR_AUTH_KEY_PATH=/tmp/atcr-auth-key.pem
527
527
+
export ATCR_UI_ENABLED=true
528
528
+
529
529
+
# Or use .env file
530
530
+
source .env.appview
531
531
+
532
532
+
# Run with Air
533
533
+
air -c .air.toml
534
534
+
535
535
+
# Or run directly (no hot reload)
536
536
+
go run ./cmd/appview serve
537
537
+
```
538
538
+
539
539
+
**Advantages:**
540
540
+
- Even faster (no Docker overhead)
541
541
+
- Native debugging with delve
542
542
+
- Direct filesystem access
543
543
+
- Full IDE integration
544
544
+
545
545
+
**Disadvantages:**
546
546
+
- Need to manage dependencies locally (SQLite, etc)
547
547
+
- May differ from production environment
548
548
+
549
549
+
## Troubleshooting
550
550
+
551
551
+
### Air Not Rebuilding
552
552
+
553
553
+
**Problem:** Air doesn't detect changes
554
554
+
555
555
+
**Solution:**
556
556
+
```bash
557
557
+
# Check if Air is actually running
558
558
+
docker compose -f docker-compose.dev.yml logs atcr-appview
559
559
+
560
560
+
# Check .air.toml include_ext includes your file type
561
561
+
# Default: ["go", "html", "css", "js"]
562
562
+
563
563
+
# Restart container
564
564
+
docker compose -f docker-compose.dev.yml restart atcr-appview
565
565
+
```
566
566
+
567
567
+
### Templates Not Updating
568
568
+
569
569
+
**Problem:** Template changes don't appear
570
570
+
571
571
+
**Solution:**
572
572
+
```bash
573
573
+
# Check ATCR_DEV_MODE is set
574
574
+
docker compose -f docker-compose.dev.yml exec atcr-appview env | grep DEV_MODE
575
575
+
576
576
+
# Should output: ATCR_DEV_MODE=true
577
577
+
578
578
+
# Check templates aren't cached (see Step 4 above)
579
579
+
# Templates() should reparse in dev mode
580
580
+
```
581
581
+
582
582
+
### Go Build Failing
583
583
+
584
584
+
**Problem:** Air shows build errors
585
585
+
586
586
+
**Solution:**
587
587
+
```bash
588
588
+
# Check build logs
589
589
+
docker compose -f docker-compose.dev.yml logs atcr-appview
590
590
+
591
591
+
# Or check build-errors.log in container
592
592
+
docker compose -f docker-compose.dev.yml exec atcr-appview cat build-errors.log
593
593
+
594
594
+
# Fix the Go error, save file, Air will retry
595
595
+
```
596
596
+
597
597
+
### Volume Mount Not Working
598
598
+
599
599
+
**Problem:** Changes don't appear in container
600
600
+
601
601
+
**Solution:**
602
602
+
```bash
603
603
+
# Verify volume mount
604
604
+
docker compose -f docker-compose.dev.yml exec atcr-appview ls -la /app
605
605
+
606
606
+
# Should show your source files
607
607
+
608
608
+
# On Windows/Mac, check Docker Desktop file sharing settings
609
609
+
# Settings → Resources → File Sharing → add project directory
610
610
+
```
611
611
+
612
612
+
### Permission Errors
613
613
+
614
614
+
**Problem:** Cannot write to /var/lib/atcr
615
615
+
616
616
+
**Solution:**
617
617
+
```bash
618
618
+
# In Dockerfile.devel, add:
619
619
+
RUN mkdir -p /var/lib/atcr && chmod 777 /var/lib/atcr
620
620
+
621
621
+
# Or use named volumes (already in docker-compose.dev.yml)
622
622
+
volumes:
623
623
+
- atcr-ui-dev:/var/lib/atcr
624
624
+
```
625
625
+
626
626
+
### Slow Builds Even with Air
627
627
+
628
628
+
**Problem:** Air rebuilds slowly
629
629
+
630
630
+
**Solution:**
631
631
+
```bash
632
632
+
# Use Go module cache volume (already in docker-compose.dev.yml)
633
633
+
volumes:
634
634
+
- go-cache:/go/pkg/mod
635
635
+
636
636
+
# Increase Air delay to debounce rapid saves
637
637
+
# In .air.toml:
638
638
+
delay = 2000 # 2 seconds
639
639
+
640
640
+
# Or check if CGO is slowing builds
641
641
+
# AppView needs CGO for SQLite, but you can try:
642
642
+
CGO_ENABLED=0 go build # (won't work for ATCR, but good to know)
643
643
+
```
644
644
+
645
645
+
## Tips & Tricks
646
646
+
647
647
+
### Browser Auto-Reload (LiveReload)
648
648
+
649
649
+
Add LiveReload for automatic browser refresh:
650
650
+
651
651
+
```bash
652
652
+
# Install browser extension
653
653
+
# Chrome: https://chrome.google.com/webstore/detail/livereload
654
654
+
# Firefox: https://addons.mozilla.org/en-US/firefox/addon/livereload-web-extension/
655
655
+
656
656
+
# Add livereload to .air.toml (future Air feature)
657
657
+
# Or use a separate tool like browsersync
658
658
+
```
659
659
+
660
660
+
### Database Resets
661
661
+
662
662
+
Development database is in a named volume:
663
663
+
664
664
+
```bash
665
665
+
# Reset database (fresh start)
666
666
+
docker compose -f docker-compose.dev.yml down -v
667
667
+
docker compose -f docker-compose.dev.yml up
668
668
+
669
669
+
# Or delete specific volume
670
670
+
docker volume rm atcr_atcr-ui-dev
671
671
+
```
672
672
+
673
673
+
### Multiple Environments
674
674
+
675
675
+
Run dev and production side-by-side:
676
676
+
677
677
+
```bash
678
678
+
# Development on port 5000
679
679
+
docker compose -f docker-compose.dev.yml up -d
680
680
+
681
681
+
# Production on port 5001
682
682
+
docker compose up -d
683
683
+
684
684
+
# Now you can compare behavior
685
685
+
```
686
686
+
687
687
+
### Debugging with Delve
688
688
+
689
689
+
Add delve to Dockerfile.devel:
690
690
+
691
691
+
```dockerfile
692
692
+
RUN go install github.com/go-delve/delve/cmd/dlv@latest
693
693
+
694
694
+
# Change CMD to use delve
695
695
+
CMD ["dlv", "debug", "./cmd/appview", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient", "--", "serve"]
696
696
+
```
697
697
+
698
698
+
Then connect with VSCode or GoLand.
699
699
+
700
700
+
## Summary
701
701
+
702
702
+
**Development Setup (One-Time):**
703
703
+
1. Create `Dockerfile.devel`
704
704
+
2. Create `docker-compose.dev.yml`
705
705
+
3. Create `.air.toml`
706
706
+
4. Modify `pkg/appview/ui.go` for conditional DirFS
707
707
+
5. Add `tmp/` to `.gitignore`
708
708
+
709
709
+
**Daily Development:**
710
710
+
```bash
711
711
+
# Start
712
712
+
docker compose -f docker-compose.dev.yml up
713
713
+
714
714
+
# Edit files in your editor
715
715
+
# Changes appear instantly (CSS/JS/templates)
716
716
+
# Or in 2-5 seconds (Go code)
717
717
+
718
718
+
# Stop
719
719
+
docker compose -f docker-compose.dev.yml down
720
720
+
```
721
721
+
722
722
+
**Production (Unchanged):**
723
723
+
```bash
724
724
+
docker compose build
725
725
+
docker compose up
726
726
+
```
727
727
+
728
728
+
**Result:** 100x faster development iteration! 🚀
+756
docs/HOLD_AS_CA.md
···
1
1
+
# Hold-as-Certificate-Authority Architecture
2
2
+
3
3
+
## ⚠️ Important Notice
4
4
+
5
5
+
This document describes an **optional enterprise feature** for X.509 PKI compliance. The hold-as-CA approach introduces **centralization trade-offs** that contradict ATProto's decentralized philosophy.
6
6
+
7
7
+
**Default Recommendation:** Use [plugin-based integration](./INTEGRATION_STRATEGY.md) instead. Only implement hold-as-CA if your organization has specific X.509 PKI compliance requirements.
8
8
+
9
9
+
## Overview
10
10
+
11
11
+
The hold-as-CA architecture allows ATCR to generate Notation/Notary v2-compatible signatures by having hold services act as Certificate Authorities that issue X.509 certificates for users.
12
12
+
13
13
+
### The Problem
14
14
+
15
15
+
- **ATProto signatures** use K-256 (secp256k1) elliptic curve
16
16
+
- **Notation** only supports P-256, P-384, P-521 elliptic curves
17
17
+
- **Cannot convert** K-256 signatures to P-256 (different cryptographic curves)
18
18
+
- **Must re-sign** with P-256 keys for Notation compatibility
19
19
+
20
20
+
### The Solution
21
21
+
22
22
+
Hold services act as trusted Certificate Authorities (CAs):
23
23
+
24
24
+
1. User pushes image → Manifest signed by PDS with K-256 (ATProto)
25
25
+
2. Hold verifies ATProto signature is valid
26
26
+
3. Hold generates ephemeral P-256 key pair for user
27
27
+
4. Hold issues X.509 certificate to user's DID
28
28
+
5. Hold signs manifest with P-256 key
29
29
+
6. Hold creates Notation signature envelope (JWS format)
30
30
+
7. Stores both ATProto and Notation signatures
31
31
+
32
32
+
**Result:** Images have two signatures:
33
33
+
- **ATProto signature** (K-256) - Decentralized, DID-based
34
34
+
- **Notation signature** (P-256) - Centralized, X.509 PKI
35
35
+
36
36
+
## Architecture
37
37
+
38
38
+
### Certificate Chain
39
39
+
40
40
+
```
41
41
+
Hold Root CA Certificate (self-signed, P-256)
42
42
+
└── User Certificate (issued to DID, P-256)
43
43
+
└── Image Manifest Signature
44
44
+
```
45
45
+
46
46
+
**Hold Root CA:**
47
47
+
```
48
48
+
Subject: CN=ATCR Hold CA - did:web:hold01.atcr.io
49
49
+
Issuer: Self (self-signed)
50
50
+
Key Usage: Digital Signature, Certificate Sign
51
51
+
Basic Constraints: CA=true, pathLen=1
52
52
+
Algorithm: ECDSA P-256
53
53
+
Validity: 10 years
54
54
+
```
55
55
+
56
56
+
**User Certificate:**
57
57
+
```
58
58
+
Subject: CN=did:plc:alice123
59
59
+
SAN: URI:did:plc:alice123
60
60
+
Issuer: Hold Root CA
61
61
+
Key Usage: Digital Signature
62
62
+
Extended Key Usage: Code Signing
63
63
+
Algorithm: ECDSA P-256
64
64
+
Validity: 24 hours (short-lived)
65
65
+
```
66
66
+
67
67
+
### Push Flow
68
68
+
69
69
+
```
70
70
+
┌──────────────────────────────────────────────────────┐
71
71
+
│ 1. User: docker push atcr.io/alice/myapp:latest │
72
72
+
└────────────────────┬─────────────────────────────────┘
73
73
+
↓
74
74
+
┌──────────────────────────────────────────────────────┐
75
75
+
│ 2. AppView stores manifest in alice's PDS │
76
76
+
│ - PDS signs with K-256 (ATProto standard) │
77
77
+
│ - Signature stored in repository commit │
78
78
+
└────────────────────┬─────────────────────────────────┘
79
79
+
↓
80
80
+
┌──────────────────────────────────────────────────────┐
81
81
+
│ 3. AppView requests hold to co-sign │
82
82
+
│ POST /xrpc/io.atcr.hold.coSignManifest │
83
83
+
│ { │
84
84
+
│ "userDid": "did:plc:alice123", │
85
85
+
│ "manifestDigest": "sha256:abc123...", │
86
86
+
│ "atprotoSignature": {...} │
87
87
+
│ } │
88
88
+
└────────────────────┬─────────────────────────────────┘
89
89
+
↓
90
90
+
┌──────────────────────────────────────────────────────┐
91
91
+
│ 4. Hold verifies ATProto signature │
92
92
+
│ a. Resolve alice's DID → public key │
93
93
+
│ b. Fetch commit from alice's PDS │
94
94
+
│ c. Verify K-256 signature │
95
95
+
│ d. Ensure signature is valid │
96
96
+
│ │
97
97
+
│ If verification fails → REJECT │
98
98
+
└────────────────────┬─────────────────────────────────┘
99
99
+
↓
100
100
+
┌──────────────────────────────────────────────────────┐
101
101
+
│ 5. Hold generates ephemeral P-256 key pair │
102
102
+
│ privateKey := ecdsa.GenerateKey(elliptic.P256()) │
103
103
+
└────────────────────┬─────────────────────────────────┘
104
104
+
↓
105
105
+
┌──────────────────────────────────────────────────────┐
106
106
+
│ 6. Hold issues X.509 certificate │
107
107
+
│ Subject: CN=did:plc:alice123 │
108
108
+
│ SAN: URI:did:plc:alice123 │
109
109
+
│ Issuer: Hold CA │
110
110
+
│ NotBefore: now │
111
111
+
│ NotAfter: now + 24 hours │
112
112
+
│ KeyUsage: Digital Signature │
113
113
+
│ ExtKeyUsage: Code Signing │
114
114
+
│ │
115
115
+
│ Sign certificate with hold's CA private key │
116
116
+
└────────────────────┬─────────────────────────────────┘
117
117
+
↓
118
118
+
┌──────────────────────────────────────────────────────┐
119
119
+
│ 7. Hold signs manifest digest │
120
120
+
│ hash := SHA256(manifestBytes) │
121
121
+
│ signature := ECDSA_P256(hash, privateKey) │
122
122
+
└────────────────────┬─────────────────────────────────┘
123
123
+
↓
124
124
+
┌──────────────────────────────────────────────────────┐
125
125
+
│ 8. Hold creates Notation JWS envelope │
126
126
+
│ { │
127
127
+
│ "protected": {...}, │
128
128
+
│ "payload": "base64(manifestDigest)", │
129
129
+
│ "signature": "base64(p256Signature)", │
130
130
+
│ "header": { │
131
131
+
│ "x5c": [ │
132
132
+
│ "base64(userCert)", │
133
133
+
│ "base64(holdCACert)" │
134
134
+
│ ] │
135
135
+
│ } │
136
136
+
│ } │
137
137
+
└────────────────────┬─────────────────────────────────┘
138
138
+
↓
139
139
+
┌──────────────────────────────────────────────────────┐
140
140
+
│ 9. Hold returns signature to AppView │
141
141
+
└────────────────────┬─────────────────────────────────┘
142
142
+
↓
143
143
+
┌──────────────────────────────────────────────────────┐
144
144
+
│ 10. AppView stores Notation signature │
145
145
+
│ - Create ORAS artifact manifest │
146
146
+
│ - Upload JWS envelope as layer blob │
147
147
+
│ - Link to image via subject field │
148
148
+
│ - artifactType: application/vnd.cncf.notary... │
149
149
+
└──────────────────────────────────────────────────────┘
150
150
+
```
151
151
+
152
152
+
### Verification Flow
153
153
+
154
154
+
```
155
155
+
┌──────────────────────────────────────────────────────┐
156
156
+
│ User: notation verify atcr.io/alice/myapp:latest │
157
157
+
└────────────────────┬─────────────────────────────────┘
158
158
+
↓
159
159
+
┌──────────────────────────────────────────────────────┐
160
160
+
│ 1. Notation queries Referrers API │
161
161
+
│ GET /v2/alice/myapp/referrers/sha256:abc123 │
162
162
+
│ → Discovers Notation signature artifact │
163
163
+
└────────────────────┬─────────────────────────────────┘
164
164
+
↓
165
165
+
┌──────────────────────────────────────────────────────┐
166
166
+
│ 2. Notation downloads JWS envelope │
167
167
+
│ - Parses JSON Web Signature │
168
168
+
│ - Extracts certificate chain from x5c header │
169
169
+
└────────────────────┬─────────────────────────────────┘
170
170
+
↓
171
171
+
┌──────────────────────────────────────────────────────┐
172
172
+
│ 3. Notation validates certificate chain │
173
173
+
│ a. User cert issued by Hold CA? ✓ │
174
174
+
│ b. Hold CA cert in trust store? ✓ │
175
175
+
│ c. Certificate not expired? ✓ │
176
176
+
│ d. Key usage correct? ✓ │
177
177
+
│ e. Subject matches policy? ✓ │
178
178
+
└────────────────────┬─────────────────────────────────┘
179
179
+
↓
180
180
+
┌──────────────────────────────────────────────────────┐
181
181
+
│ 4. Notation verifies signature │
182
182
+
│ a. Extract public key from user certificate │
183
183
+
│ b. Compute manifest hash: SHA256(manifest) │
184
184
+
│ c. Verify: ECDSA_P256(hash, sig, pubKey) ✓ │
185
185
+
└────────────────────┬─────────────────────────────────┘
186
186
+
↓
187
187
+
┌──────────────────────────────────────────────────────┐
188
188
+
│ 5. Success: Image verified ✓ │
189
189
+
│ Signed by: did:plc:alice123 (via Hold CA) │
190
190
+
└──────────────────────────────────────────────────────┘
191
191
+
```
192
192
+
193
193
+
## Implementation
194
194
+
195
195
+
### Hold CA Certificate Generation
196
196
+
197
197
+
```go
198
198
+
// cmd/hold/main.go - CA initialization
199
199
+
func (h *Hold) initializeCA(ctx context.Context) error {
200
200
+
caKeyPath := filepath.Join(h.config.DataDir, "ca-private-key.pem")
201
201
+
caCertPath := filepath.Join(h.config.DataDir, "ca-certificate.pem")
202
202
+
203
203
+
// Load existing CA or generate new one
204
204
+
if exists(caKeyPath) && exists(caCertPath) {
205
205
+
h.caKey = loadPrivateKey(caKeyPath)
206
206
+
h.caCert = loadCertificate(caCertPath)
207
207
+
return nil
208
208
+
}
209
209
+
210
210
+
// Generate P-256 key pair for CA
211
211
+
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
212
212
+
if err != nil {
213
213
+
return fmt.Errorf("failed to generate CA key: %w", err)
214
214
+
}
215
215
+
216
216
+
// Create CA certificate template
217
217
+
serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
218
218
+
219
219
+
template := &x509.Certificate{
220
220
+
SerialNumber: serialNumber,
221
221
+
Subject: pkix.Name{
222
222
+
CommonName: fmt.Sprintf("ATCR Hold CA - %s", h.DID),
223
223
+
},
224
224
+
NotBefore: time.Now(),
225
225
+
NotAfter: time.Now().AddDate(10, 0, 0), // 10 years
226
226
+
227
227
+
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
228
228
+
BasicConstraintsValid: true,
229
229
+
IsCA: true,
230
230
+
MaxPathLen: 1, // Can only issue end-entity certificates
231
231
+
}
232
232
+
233
233
+
// Self-sign
234
234
+
certDER, err := x509.CreateCertificate(
235
235
+
rand.Reader,
236
236
+
template,
237
237
+
template, // Self-signed: issuer = subject
238
238
+
&caKey.PublicKey,
239
239
+
caKey,
240
240
+
)
241
241
+
if err != nil {
242
242
+
return fmt.Errorf("failed to create CA certificate: %w", err)
243
243
+
}
244
244
+
245
245
+
caCert, _ := x509.ParseCertificate(certDER)
246
246
+
247
247
+
// Save to disk (0600 permissions)
248
248
+
savePrivateKey(caKeyPath, caKey)
249
249
+
saveCertificate(caCertPath, caCert)
250
250
+
251
251
+
h.caKey = caKey
252
252
+
h.caCert = caCert
253
253
+
254
254
+
log.Info("Generated new CA certificate", "did", h.DID, "expires", caCert.NotAfter)
255
255
+
return nil
256
256
+
}
257
257
+
```
258
258
+
259
259
+
### User Certificate Issuance
260
260
+
261
261
+
```go
262
262
+
// pkg/hold/cosign.go
263
263
+
func (h *Hold) issueUserCertificate(userDID string) (*x509.Certificate, *ecdsa.PrivateKey, error) {
264
264
+
// Generate ephemeral P-256 key for user
265
265
+
userKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
266
266
+
if err != nil {
267
267
+
return nil, nil, fmt.Errorf("failed to generate user key: %w", err)
268
268
+
}
269
269
+
270
270
+
serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
271
271
+
272
272
+
// Parse DID for SAN
273
273
+
sanURI, _ := url.Parse(userDID)
274
274
+
275
275
+
template := &x509.Certificate{
276
276
+
SerialNumber: serialNumber,
277
277
+
Subject: pkix.Name{
278
278
+
CommonName: userDID,
279
279
+
},
280
280
+
URIs: []*url.URL{sanURI}, // Subject Alternative Name
281
281
+
282
282
+
NotBefore: time.Now(),
283
283
+
NotAfter: time.Now().Add(24 * time.Hour), // Short-lived: 24 hours
284
284
+
285
285
+
KeyUsage: x509.KeyUsageDigitalSignature,
286
286
+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
287
287
+
BasicConstraintsValid: true,
288
288
+
IsCA: false,
289
289
+
}
290
290
+
291
291
+
// Sign with hold's CA key
292
292
+
certDER, err := x509.CreateCertificate(
293
293
+
rand.Reader,
294
294
+
template,
295
295
+
h.caCert, // Issuer: Hold CA
296
296
+
&userKey.PublicKey,
297
297
+
h.caKey, // Sign with CA private key
298
298
+
)
299
299
+
if err != nil {
300
300
+
return nil, nil, fmt.Errorf("failed to create user certificate: %w", err)
301
301
+
}
302
302
+
303
303
+
userCert, _ := x509.ParseCertificate(certDER)
304
304
+
305
305
+
return userCert, userKey, nil
306
306
+
}
307
307
+
```
308
308
+
309
309
+
### Co-Signing XRPC Endpoint
310
310
+
311
311
+
```go
312
312
+
// pkg/hold/oci/xrpc.go
313
313
+
func (s *Server) handleCoSignManifest(ctx context.Context, req *CoSignRequest) (*CoSignResponse, error) {
314
314
+
// 1. Verify caller is authenticated
315
315
+
did, err := s.auth.VerifyToken(ctx, req.Token)
316
316
+
if err != nil {
317
317
+
return nil, fmt.Errorf("authentication failed: %w", err)
318
318
+
}
319
319
+
320
320
+
// 2. Verify ATProto signature
321
321
+
valid, err := s.verifyATProtoSignature(ctx, req.UserDID, req.ManifestDigest, req.ATProtoSignature)
322
322
+
if err != nil || !valid {
323
323
+
return nil, fmt.Errorf("ATProto signature verification failed: %w", err)
324
324
+
}
325
325
+
326
326
+
// 3. Issue certificate for user
327
327
+
userCert, userKey, err := s.hold.issueUserCertificate(req.UserDID)
328
328
+
if err != nil {
329
329
+
return nil, fmt.Errorf("failed to issue certificate: %w", err)
330
330
+
}
331
331
+
332
332
+
// 4. Sign manifest with user's key
333
333
+
manifestHash := sha256.Sum256([]byte(req.ManifestDigest))
334
334
+
signature, err := ecdsa.SignASN1(rand.Reader, userKey, manifestHash[:])
335
335
+
if err != nil {
336
336
+
return nil, fmt.Errorf("failed to sign manifest: %w", err)
337
337
+
}
338
338
+
339
339
+
// 5. Create JWS envelope
340
340
+
jws, err := s.createJWSEnvelope(signature, userCert, s.hold.caCert, req.ManifestDigest)
341
341
+
if err != nil {
342
342
+
return nil, fmt.Errorf("failed to create JWS: %w", err)
343
343
+
}
344
344
+
345
345
+
return &CoSignResponse{
346
346
+
JWS: jws,
347
347
+
Certificate: encodeCertificate(userCert),
348
348
+
CACertificate: encodeCertificate(s.hold.caCert),
349
349
+
}, nil
350
350
+
}
351
351
+
```
352
352
+
353
353
+
## Trust Model
354
354
+
355
355
+
### Centralization Analysis
356
356
+
357
357
+
**ATProto Model (Decentralized):**
358
358
+
- Each PDS is independent
359
359
+
- User controls which PDS to use
360
360
+
- Trust user's DID, not specific infrastructure
361
361
+
- PDS compromise affects only that PDS's users
362
362
+
- Multiple PDSs provide redundancy
363
363
+
364
364
+
**Hold-as-CA Model (Centralized):**
365
365
+
- Hold acts as single Certificate Authority
366
366
+
- All users must trust hold's CA certificate
367
367
+
- Hold compromise = attacker can issue certificates for ANY user
368
368
+
- Hold becomes single point of failure
369
369
+
- Users depend on hold operator honesty
370
370
+
371
371
+
### What Hold Vouches For
372
372
+
373
373
+
When hold issues a certificate, it attests:
374
374
+
375
375
+
✅ **"I verified that [DID] signed this manifest with ATProto"**
376
376
+
- Hold validated ATProto signature
377
377
+
- Hold confirmed signature matches user's DID
378
378
+
- Hold checked signature at specific time
379
379
+
380
380
+
❌ **"This image is safe"**
381
381
+
- Hold does NOT audit image contents
382
382
+
- Certificate ≠ vulnerability scan
383
383
+
- Signature ≠ security guarantee
384
384
+
385
385
+
❌ **"I control this DID"**
386
386
+
- Hold does NOT control user's DID
387
387
+
- DID ownership is independent
388
388
+
- Hold cannot revoke DIDs
389
389
+
390
390
+
### Threat Model
391
391
+
392
392
+
**Scenario 1: Hold Private Key Compromise**
393
393
+
394
394
+
**Attack:**
395
395
+
- Attacker steals hold's CA private key
396
396
+
- Can issue certificates for any DID
397
397
+
- Can sign malicious images as any user
398
398
+
399
399
+
**Impact:**
400
400
+
- **CRITICAL** - All users affected
401
401
+
- Attacker can impersonate any user
402
402
+
- All signatures become untrustworthy
403
403
+
404
404
+
**Detection:**
405
405
+
- Certificate Transparency logs (if implemented)
406
406
+
- Unusual certificate issuance patterns
407
407
+
- Users report unexpected signatures
408
408
+
409
409
+
**Mitigation:**
410
410
+
- Store CA key in Hardware Security Module (HSM)
411
411
+
- Strict access controls
412
412
+
- Audit logging
413
413
+
- Regular key rotation
414
414
+
415
415
+
**Recovery:**
416
416
+
- Revoke compromised CA certificate
417
417
+
- Generate new CA certificate
418
418
+
- Re-issue all active certificates
419
419
+
- Notify all users
420
420
+
- Update trust stores
421
421
+
422
422
+
---
423
423
+
424
424
+
**Scenario 2: Malicious Hold Operator**
425
425
+
426
426
+
**Attack:**
427
427
+
- Hold operator issues certificates without verifying ATProto signatures
428
428
+
- Hold operator signs malicious images
429
429
+
- Hold operator backdates certificates
430
430
+
431
431
+
**Impact:**
432
432
+
- **HIGH** - Trust model broken
433
433
+
- Users receive signed malicious images
434
434
+
- Difficult to detect without ATProto cross-check
435
435
+
436
436
+
**Detection:**
437
437
+
- Compare Notation signature timestamp with ATProto commit time
438
438
+
- Verify ATProto signature exists independently
439
439
+
- Monitor hold's signing patterns
440
440
+
441
441
+
**Mitigation:**
442
442
+
- Audit trail linking certificates to ATProto signatures
443
443
+
- Public transparency logs
444
444
+
- Multi-signature requirements
445
445
+
- Periodically verify ATProto signatures
446
446
+
447
447
+
**Recovery:**
448
448
+
- Identify malicious certificates
449
449
+
- Revoke hold's CA trust
450
450
+
- Switch to different hold
451
451
+
- Re-verify all images
452
452
+
453
453
+
---
454
454
+
455
455
+
**Scenario 3: Certificate Theft**
456
456
+
457
457
+
**Attack:**
458
458
+
- Attacker steals issued user certificate + private key
459
459
+
- Uses it to sign malicious images
460
460
+
461
461
+
**Impact:**
462
462
+
- **LOW-MEDIUM** - Limited scope
463
463
+
- Affects only specific user/image
464
464
+
- Short validity period (24 hours)
465
465
+
466
466
+
**Detection:**
467
467
+
- Unexpected signature timestamps
468
468
+
- Images signed from unknown locations
469
469
+
470
470
+
**Mitigation:**
471
471
+
- Short certificate validity (24 hours)
472
472
+
- Ephemeral keys (not stored long-term)
473
473
+
- Certificate revocation if detected
474
474
+
475
475
+
**Recovery:**
476
476
+
- Wait for certificate expiration (24 hours)
477
477
+
- Revoke specific certificate
478
478
+
- Investigate compromise source
479
479
+
480
480
+
## Certificate Management
481
481
+
482
482
+
### Expiration Strategy
483
483
+
484
484
+
**Short-Lived Certificates (24 hours):**
485
485
+
486
486
+
**Pros:**
487
487
+
- ✅ Minimal revocation infrastructure needed
488
488
+
- ✅ Compromise window is tiny
489
489
+
- ✅ Automatic cleanup
490
490
+
- ✅ Lower CRL/OCSP overhead
491
491
+
492
492
+
**Cons:**
493
493
+
- ❌ Old images become unverifiable quickly
494
494
+
- ❌ Requires re-signing for historical verification
495
495
+
- ❌ Storage: multiple signatures for same image
496
496
+
497
497
+
**Solution: On-Demand Re-Signing**
498
498
+
```
499
499
+
User pulls old image → Notation verification fails (expired cert)
500
500
+
→ User requests re-signing: POST /xrpc/io.atcr.hold.reSignManifest
501
501
+
→ Hold verifies ATProto signature still valid
502
502
+
→ Hold issues new certificate (24 hours)
503
503
+
→ Hold creates new Notation signature
504
504
+
→ User can verify with fresh certificate
505
505
+
```
506
506
+
507
507
+
### Revocation
508
508
+
509
509
+
**Certificate Revocation List (CRL):**
510
510
+
```
511
511
+
Hold publishes CRL at: https://hold01.atcr.io/ca.crl
512
512
+
513
513
+
Notation configured to check CRL:
514
514
+
{
515
515
+
"trustPolicies": [{
516
516
+
"name": "atcr-images",
517
517
+
"signatureVerification": {
518
518
+
"verificationLevel": "strict",
519
519
+
"override": {
520
520
+
"revocationValidation": "strict"
521
521
+
}
522
522
+
}
523
523
+
}]
524
524
+
}
525
525
+
```
526
526
+
527
527
+
**OCSP (Online Certificate Status Protocol):**
528
528
+
- Hold runs OCSP responder: `https://hold01.atcr.io/ocsp`
529
529
+
- Real-time certificate status checks
530
530
+
- Lower overhead than CRL downloads
531
531
+
532
532
+
**Revocation Triggers:**
533
533
+
- Key compromise detected
534
534
+
- Malicious signing detected
535
535
+
- User request
536
536
+
- DID ownership change
537
537
+
538
538
+
### CA Key Rotation
539
539
+
540
540
+
**Rotation Procedure:**
541
541
+
542
542
+
1. **Generate new CA key pair**
543
543
+
2. **Create new CA certificate**
544
544
+
3. **Cross-sign old CA with new CA** (transition period)
545
545
+
4. **Distribute new CA certificate** to all users
546
546
+
5. **Begin issuing with new CA** for new signatures
547
547
+
6. **Grace period** (30 days): Accept both old and new CA
548
548
+
7. **Retire old CA** after grace period
549
549
+
550
550
+
**Frequency:** Every 2-3 years (longer than short-lived certs)
551
551
+
552
552
+
## Trust Store Distribution
553
553
+
554
554
+
### Problem
555
555
+
556
556
+
Users must add hold's CA certificate to their Notation trust store for verification to work.
557
557
+
558
558
+
### Manual Distribution
559
559
+
560
560
+
```bash
561
561
+
# 1. Download hold's CA certificate
562
562
+
curl https://hold01.atcr.io/ca.crt -o hold01-ca.crt
563
563
+
564
564
+
# 2. Verify fingerprint (out-of-band)
565
565
+
openssl x509 -in hold01-ca.crt -fingerprint -noout
566
566
+
# Compare with published fingerprint
567
567
+
568
568
+
# 3. Add to Notation trust store
569
569
+
notation cert add --type ca --store atcr-holds hold01-ca.crt
570
570
+
```
571
571
+
572
572
+
### Automated Distribution
573
573
+
574
574
+
**ATCR CLI tool:**
575
575
+
```bash
576
576
+
atcr trust add hold01.atcr.io
577
577
+
# → Fetches CA certificate
578
578
+
# → Verifies via HTTPS + DNSSEC
579
579
+
# → Adds to Notation trust store
580
580
+
# → Configures trust policy
581
581
+
582
582
+
atcr trust list
583
583
+
# → Shows trusted holds with fingerprints
584
584
+
```
585
585
+
586
586
+
### System-Wide Trust
587
587
+
588
588
+
**For enterprise deployments:**
589
589
+
590
590
+
**Debian/Ubuntu:**
591
591
+
```bash
592
592
+
# Install CA certificate system-wide
593
593
+
cp hold01-ca.crt /usr/local/share/ca-certificates/atcr-hold01.crt
594
594
+
update-ca-certificates
595
595
+
```
596
596
+
597
597
+
**RHEL/CentOS:**
598
598
+
```bash
599
599
+
cp hold01-ca.crt /etc/pki/ca-trust/source/anchors/
600
600
+
update-ca-trust
601
601
+
```
602
602
+
603
603
+
**Container images:**
604
604
+
```dockerfile
605
605
+
FROM ubuntu:22.04
606
606
+
COPY hold01-ca.crt /usr/local/share/ca-certificates/
607
607
+
RUN update-ca-certificates
608
608
+
```
609
609
+
610
610
+
## Configuration
611
611
+
612
612
+
### Hold Service
613
613
+
614
614
+
**Environment variables:**
615
615
+
```bash
616
616
+
# Enable co-signing feature
617
617
+
HOLD_COSIGN_ENABLED=true
618
618
+
619
619
+
# CA certificate and key paths
620
620
+
HOLD_CA_CERT_PATH=/var/lib/atcr/hold/ca-certificate.pem
621
621
+
HOLD_CA_KEY_PATH=/var/lib/atcr/hold/ca-private-key.pem
622
622
+
623
623
+
# Certificate validity
624
624
+
HOLD_CERT_VALIDITY_HOURS=24
625
625
+
626
626
+
# OCSP responder
627
627
+
HOLD_OCSP_ENABLED=true
628
628
+
HOLD_OCSP_URL=https://hold01.atcr.io/ocsp
629
629
+
630
630
+
# CRL distribution
631
631
+
HOLD_CRL_ENABLED=true
632
632
+
HOLD_CRL_URL=https://hold01.atcr.io/ca.crl
633
633
+
```
634
634
+
635
635
+
### Notation Trust Policy
636
636
+
637
637
+
```json
638
638
+
{
639
639
+
"version": "1.0",
640
640
+
"trustPolicies": [{
641
641
+
"name": "atcr-images",
642
642
+
"registryScopes": ["atcr.io/*/*"],
643
643
+
"signatureVerification": {
644
644
+
"level": "strict",
645
645
+
"override": {
646
646
+
"revocationValidation": "strict"
647
647
+
}
648
648
+
},
649
649
+
"trustStores": ["ca:atcr-holds"],
650
650
+
"trustedIdentities": [
651
651
+
"x509.subject: CN=did:plc:*",
652
652
+
"x509.subject: CN=did:web:*"
653
653
+
]
654
654
+
}]
655
655
+
}
656
656
+
```
657
657
+
658
658
+
## When to Use Hold-as-CA
659
659
+
660
660
+
### ✅ Use When
661
661
+
662
662
+
**Enterprise X.509 PKI Compliance:**
663
663
+
- Organization requires standard X.509 certificates
664
664
+
- Existing security policies mandate PKI
665
665
+
- Audit requirements for certificate chains
666
666
+
- Integration with existing CA infrastructure
667
667
+
668
668
+
**Tool Compatibility:**
669
669
+
- Must use standard Notation without plugins
670
670
+
- Cannot deploy custom verification tools
671
671
+
- Existing tooling expects X.509 signatures
672
672
+
673
673
+
**Centralized Trust Acceptable:**
674
674
+
- Organization already uses centralized trust model
675
675
+
- Hold operator is internal/trusted team
676
676
+
- Centralization risk is acceptable trade-off
677
677
+
678
678
+
### ❌ Don't Use When
679
679
+
680
680
+
**Default Deployment:**
681
681
+
- Most users should use [plugin-based approach](./INTEGRATION_STRATEGY.md)
682
682
+
- Plugins maintain decentralization
683
683
+
- Plugins reuse existing ATProto signatures
684
684
+
685
685
+
**Small Teams / Startups:**
686
686
+
- Certificate management overhead too high
687
687
+
- Don't need X.509 compliance
688
688
+
- Prefer simpler architecture
689
689
+
690
690
+
**Maximum Decentralization Required:**
691
691
+
- Cannot accept hold as single trust point
692
692
+
- Must maintain pure ATProto model
693
693
+
- Centralization contradicts project goals
694
694
+
695
695
+
## Comparison: Hold-as-CA vs. Plugins
696
696
+
697
697
+
| Aspect | Hold-as-CA | Plugin Approach |
698
698
+
|--------|------------|----------------|
699
699
+
| **Standard compliance** | ✅ Full X.509/PKI | ⚠️ Custom verification |
700
700
+
| **Tool compatibility** | ✅ Notation works unchanged | ❌ Requires plugin install |
701
701
+
| **Decentralization** | ❌ Centralized (hold CA) | ✅ Decentralized (DIDs) |
702
702
+
| **ATProto alignment** | ❌ Against philosophy | ✅ ATProto-native |
703
703
+
| **Signature reuse** | ❌ Must re-sign (P-256) | ✅ Reuses ATProto (K-256) |
704
704
+
| **Certificate mgmt** | 🔴 High overhead | 🟢 None |
705
705
+
| **Trust distribution** | 🔴 Must distribute CA cert | 🟢 DID resolution |
706
706
+
| **Hold compromise** | 🔴 All users affected | 🟢 Metadata only |
707
707
+
| **Operational cost** | 🔴 High | 🟢 Low |
708
708
+
| **Use case** | Enterprise PKI | General purpose |
709
709
+
710
710
+
## Recommendations
711
711
+
712
712
+
### Default Approach: Plugins
713
713
+
714
714
+
For most deployments, use plugin-based verification:
715
715
+
- **Ratify plugin** for Kubernetes
716
716
+
- **OPA Gatekeeper provider** for policy enforcement
717
717
+
- **Containerd verifier** for runtime checks
718
718
+
- **atcr-verify CLI** for general purpose
719
719
+
720
720
+
See [Integration Strategy](./INTEGRATION_STRATEGY.md) for details.
721
721
+
722
722
+
### Optional: Hold-as-CA for Enterprise
723
723
+
724
724
+
Only implement hold-as-CA if you have specific requirements:
725
725
+
- Enterprise X.509 PKI mandates
726
726
+
- Cannot use plugins (restricted environments)
727
727
+
- Accept centralization trade-off
728
728
+
729
729
+
**Implement as opt-in feature:**
730
730
+
```bash
731
731
+
# Users explicitly enable co-signing
732
732
+
docker push atcr.io/alice/myapp:latest --sign=notation
733
733
+
734
734
+
# Or via environment variable
735
735
+
export ATCR_ENABLE_COSIGN=true
736
736
+
docker push atcr.io/alice/myapp:latest
737
737
+
```
738
738
+
739
739
+
### Security Best Practices
740
740
+
741
741
+
**If implementing hold-as-CA:**
742
742
+
743
743
+
1. **Store CA key in HSM** - Never on filesystem
744
744
+
2. **Audit all certificate issuance** - Log every cert
745
745
+
3. **Public transparency log** - Publish all certificates
746
746
+
4. **Short certificate validity** - 24 hours max
747
747
+
5. **Monitor unusual patterns** - Alert on anomalies
748
748
+
6. **Regular CA key rotation** - Every 2-3 years
749
749
+
7. **Cross-check ATProto** - Verify both signatures match
750
750
+
8. **Incident response plan** - Prepare for compromise
751
751
+
752
752
+
## See Also
753
753
+
754
754
+
- [ATProto Signatures](./ATPROTO_SIGNATURES.md) - How ATProto signing works
755
755
+
- [Integration Strategy](./INTEGRATION_STRATEGY.md) - Overview of integration approaches
756
756
+
- [Signature Integration](./SIGNATURE_INTEGRATION.md) - Tool-specific integration guides
+356
-808
docs/IMAGE_SIGNING.md
···
1
1
# Image Signing with ATProto
2
2
3
3
-
ATCR supports cryptographic signing of container images to ensure authenticity and integrity. Users have two options:
4
4
-
5
5
-
1. **Automatic signing (recommended)**: Credential helper signs images automatically on every push
6
6
-
2. **Manual signing**: Use standard Cosign tools yourself
7
7
-
8
8
-
Both approaches use the OCI Referrers API bridge for verification with standard tools (Cosign, Notary, Kubernetes admission controllers).
9
9
-
10
10
-
## Design Constraints
11
11
-
12
12
-
### Why Server-Side Signing Doesn't Work
13
13
-
14
14
-
It's tempting to implement automatic signing on the AppView or hold (like GitHub's automatic Cosign signing), but this breaks the fundamental trust model:
15
15
-
16
16
-
**The problem: Signing "on behalf of" isn't real signing**
17
17
-
18
18
-
```
19
19
-
❌ AppView signs image → Proves "AppView vouches for this"
20
20
-
❌ Hold signs image → Proves "Hold vouches for this"
21
21
-
❌ PDS signs image → Proves "PDS vouches for this"
22
22
-
✅ Alice signs image → Proves "Alice created/approved this"
23
23
-
```
24
24
-
25
25
-
**Why GitHub can do it:**
26
26
-
- GitHub Actions runs with your GitHub identity
27
27
-
- OIDC token proves "this workflow runs as alice on GitHub"
28
28
-
- Fulcio certificate authority issues cert based on that proof
29
29
-
- Still "alice" signing, just via GitHub's infrastructure
30
30
-
31
31
-
**Why ATCR can't replicate this:**
32
32
-
- ATProto doesn't have OIDC/Fulcio equivalent
33
33
-
- AppView can't sign "as alice" - only alice can
34
34
-
- No secure server-side storage for user private keys
35
35
-
- ATProto doesn't have encrypted record storage yet
36
36
-
- Storing keys in AppView database = AppView controls keys, not alice
37
37
-
- Hold's PDS has its own private key, but signing with it proves hold ownership, not user ownership
38
38
-
39
39
-
**Conclusion:** Signing must happen **client-side with user-controlled keys**.
40
40
-
41
41
-
### Why ATProto Record Signatures Aren't Sufficient
42
42
-
43
43
-
ATProto already signs all records stored in PDSs. When a manifest is stored as an `io.atcr.manifest` record, it includes:
44
44
-
45
45
-
```json
46
46
-
{
47
47
-
"uri": "at://did:plc:alice123/io.atcr.manifest/abc123",
48
48
-
"cid": "bafyrei...",
49
49
-
"value": { /* manifest data */ },
50
50
-
"sig": "..." // ← PDS signature over record
51
51
-
}
52
52
-
```
53
53
-
54
54
-
**What this proves:**
55
55
-
- ✅ Alice's PDS created and signed this record
56
56
-
- ✅ Record hasn't been tampered with since signing
57
57
-
- ✅ CID correctly represents the record content
58
58
-
59
59
-
**What this doesn't prove:**
60
60
-
- ❌ Alice personally approved this image
61
61
-
- ❌ Alice's private key was involved (only PDS key)
62
62
-
63
63
-
**The gap:**
64
64
-
- A compromised or malicious PDS could create fake manifest records and sign them validly
65
65
-
- PDS operator could sign manifests without user's knowledge
66
66
-
- No proof that the *user* (not just their PDS) approved the image
67
67
-
68
68
-
**For true image signing, we need:**
69
69
-
- User-controlled private keys (not PDS keys)
70
70
-
- Client-side signing (where user has key access)
71
71
-
- Separate signature records proving user approval
72
72
-
73
73
-
**Important nuance - PDS Trust Spectrum:**
74
74
-
75
75
-
While ATProto records are always signed by the PDS, this doesn't provide user-level signing for image verification:
3
3
+
ATCR provides cryptographic verification of container images through ATProto's native signature system. Every manifest stored in a PDS is cryptographically signed, providing tamper-proof image verification.
76
4
77
77
-
1. **Self-hosted PDS with user-controlled keys:**
78
78
-
- User runs their own PDS and controls PDS rotation keys
79
79
-
- PDS signature ≈ user signature (trusted operator)
80
80
-
- Still doesn't work with standard tools (Cosign/Notary)
5
5
+
## Overview
81
6
82
82
-
2. **Shared/managed PDS (e.g., Bluesky):**
83
83
-
- PDS operated by third party (bsky.social)
84
84
-
- Auto-generated keys controlled by operator
85
85
-
- User doesn't have access to PDS rotation keys
86
86
-
- PDS signature ≠ user signature
7
7
+
**Key Fact:** Every image pushed to ATCR is automatically signed via ATProto's repository commit signing. No additional signing tools or steps are required.
87
8
88
88
-
**For ATCR:**
89
89
-
- Credential helper signing works for all users (self-hosted or shared PDS)
90
90
-
- Provides user-controlled keys separate from PDS keys
91
91
-
- Works with standard verification tools via OCI Referrers API bridge
9
9
+
When you push an image:
10
10
+
1. Manifest stored in your PDS as an `io.atcr.manifest` record
11
11
+
2. PDS signs the repository commit containing the manifest (ECDSA K-256)
12
12
+
3. Signature is part of the ATProto repository chain
13
13
+
4. Verification proves the manifest came from your DID and hasn't been tampered with
92
14
93
93
-
## Signing Options
15
15
+
**This document explains:**
16
16
+
- How ATProto signatures work for ATCR images
17
17
+
- How to verify signatures using standard and custom tools
18
18
+
- Integration options for different use cases
19
19
+
- When to use optional X.509 certificates (Hold-as-CA)
94
20
95
95
-
### Option 1: Automatic Signing (Recommended)
21
21
+
## ATProto Signature Model
96
22
97
97
-
The credential helper automatically signs images on every push - no extra commands needed.
23
23
+
### How It Works
98
24
99
99
-
**How it works:**
100
100
-
- Credential helper runs on every `docker push` for authentication
101
101
-
- Extended to also sign the manifest digest with user's private key
102
102
-
- Private key stored securely in OS keychain
103
103
-
- Signature sent to AppView and stored in ATProto
104
104
-
- Completely transparent to the user
105
105
-
106
106
-
### Architecture
107
107
-
108
108
-
```
109
109
-
┌─────────────────────────────────────────────────────┐
110
110
-
│ docker push atcr.io/alice/myapp:latest │
111
111
-
└────────────────────┬────────────────────────────────┘
112
112
-
↓
113
113
-
┌─────────────────────────────────────────────────────┐
114
114
-
│ docker-credential-atcr (runs automatically) │
115
115
-
│ │
116
116
-
│ 1. Authenticate to AppView (OAuth) │
117
117
-
│ 2. Get registry JWT │
118
118
-
│ 3. Sign manifest digest with local private key ← NEW
119
119
-
│ 4. Send signature to AppView ← NEW
120
120
-
│ │
121
121
-
│ Private key stored in OS keychain │
122
122
-
│ (macOS Keychain, Windows Credential Manager, etc.) │
123
123
-
└────────────────────┬────────────────────────────────┘
124
124
-
↓
125
125
-
┌─────────────────────────────────────────────────────┐
126
126
-
│ AppView │
127
127
-
│ │
128
128
-
│ 1. Receives signature from credential helper │
129
129
-
│ 2. Stores in user's PDS (io.atcr.signature) │
130
130
-
│ │
131
131
-
│ OR stores in hold's PDS for BYOS scenarios │
132
132
-
└─────────────────────────────────────────────────────┘
133
133
-
```
134
134
-
135
135
-
**User experience:**
136
136
-
137
137
-
```bash
138
138
-
# One-time setup
139
139
-
docker login atcr.io
140
140
-
# → Credential helper generates ECDSA key pair
141
141
-
# → Private key stored in OS keychain
142
142
-
# → Public key published to user's PDS
143
143
-
144
144
-
# Every push (automatic signing)
145
145
-
docker push atcr.io/alice/myapp:latest
146
146
-
# → Image pushed
147
147
-
# → Automatically signed by credential helper
148
148
-
# → No extra commands!
149
149
-
150
150
-
# Verification (standard Cosign)
151
151
-
cosign verify atcr.io/alice/myapp:latest --key alice.pub
152
152
-
```
153
153
-
154
154
-
### Option 2: Manual Signing (DIY)
155
155
-
156
156
-
Use standard Cosign tools yourself if you prefer manual control.
157
157
-
158
158
-
**How it works:**
159
159
-
- You manage your own signing keys
160
160
-
- You run `cosign sign` manually after pushing
161
161
-
- Signatures stored in ATProto via OCI Referrers API
162
162
-
- Full control over signing workflow
163
163
-
164
164
-
**User experience:**
165
165
-
166
166
-
```bash
167
167
-
# Push image
168
168
-
docker push atcr.io/alice/myapp:latest
169
169
-
170
170
-
# Sign manually with Cosign
171
171
-
cosign sign atcr.io/alice/myapp:latest --key cosign.key
172
172
-
173
173
-
# Cosign stores signature via registry's OCI API
174
174
-
# AppView receives signature and stores in ATProto
175
175
-
176
176
-
# Verification (same as automatic)
177
177
-
cosign verify atcr.io/alice/myapp:latest --key cosign.pub
178
178
-
```
179
179
-
180
180
-
**When to use:**
181
181
-
- Need specific signing workflows (e.g., CI/CD pipelines)
182
182
-
- Want to use hardware tokens (YubiKey)
183
183
-
- Prefer manual control over automatic signing
184
184
-
- Already using Cosign in your organization
185
185
-
186
186
-
### Key Management
187
187
-
188
188
-
**Key generation (first run):**
189
189
-
1. Credential helper checks for existing signing key in OS keychain
190
190
-
2. If not found, generates new ECDSA P-256 key pair (or Ed25519)
191
191
-
3. Stores private key in OS keychain with access control
192
192
-
4. Derives public key for publishing
193
193
-
194
194
-
**Public key publishing:**
195
195
-
```json
196
196
-
{
197
197
-
"$type": "io.atcr.signing.key",
198
198
-
"keyId": "credential-helper-default",
199
199
-
"keyType": "ecdsa-p256",
200
200
-
"publicKey": "-----BEGIN PUBLIC KEY-----\nMFkw...",
201
201
-
"validFrom": "2025-10-20T12:00:00Z",
202
202
-
"expiresAt": null,
203
203
-
"revoked": false,
204
204
-
"purpose": ["image-signing"],
205
205
-
"deviceId": "alice-macbook-pro",
206
206
-
"createdAt": "2025-10-20T12:00:00Z"
207
207
-
}
208
208
-
```
209
209
-
210
210
-
**Record stored in:** User's PDS at `io.atcr.signing.key/credential-helper-default`
211
211
-
212
212
-
**Key storage locations:**
213
213
-
- **macOS:** Keychain Access (secure enclave on modern Macs)
214
214
-
- **Windows:** Credential Manager / Windows Data Protection API
215
215
-
- **Linux:** Secret Service API (gnome-keyring, kwallet)
216
216
-
- **Fallback:** Encrypted file with restrictive permissions (0600)
217
217
-
218
218
-
### Signing Flow
25
25
+
ATProto uses a **repository commit signing** model similar to Git:
219
26
220
27
```
221
28
1. docker push atcr.io/alice/myapp:latest
222
29
↓
223
223
-
2. Docker daemon calls credential helper:
224
224
-
docker-credential-atcr get atcr.io
225
225
-
↓
226
226
-
3. Credential helper flow:
227
227
-
a. Authenticate via OAuth (existing)
228
228
-
b. Receive registry JWT from AppView (existing)
229
229
-
c. Fetch manifest digest from registry (NEW)
230
230
-
d. Load private key from OS keychain (NEW)
231
231
-
e. Sign manifest digest (NEW)
232
232
-
f. Send signature to AppView via XRPC (NEW)
30
30
+
2. AppView stores manifest in alice's PDS as io.atcr.manifest record
233
31
↓
234
234
-
4. AppView stores signature:
235
235
-
{
236
236
-
"$type": "io.atcr.signature",
237
237
-
"repository": "alice/myapp",
238
238
-
"digest": "sha256:abc123...",
239
239
-
"signature": "MEUCIQDx...",
240
240
-
"keyId": "credential-helper-default",
241
241
-
"signatureAlgorithm": "ecdsa-p256-sha256",
242
242
-
"signedAt": "2025-10-20T12:34:56Z"
243
243
-
}
32
32
+
3. PDS creates repository commit containing the new record
244
33
↓
245
245
-
5. Return registry JWT to Docker
34
34
+
4. PDS signs commit with alice's private key (ECDSA K-256)
246
35
↓
247
247
-
6. Docker proceeds with push
36
36
+
5. Commit becomes part of alice's cryptographically signed repo chain
248
37
```
249
38
250
250
-
### Signature Storage
251
251
-
252
252
-
**Option 1: User's PDS (Default)**
253
253
-
- Signature stored in alice's PDS
254
254
-
- Collection: `io.atcr.signature`
255
255
-
- Discoverable via alice's ATProto repo
256
256
-
- User owns all signing metadata
257
257
-
258
258
-
**Option 2: Hold's PDS (BYOS)**
259
259
-
- Signature stored in hold's embedded PDS
260
260
-
- Useful for shared holds with multiple users
261
261
-
- Hold acts as signature repository
262
262
-
- Parallel to SBOM storage model
263
263
-
264
264
-
**Decision logic:**
265
265
-
```go
266
266
-
// In AppView signature handler
267
267
-
if manifest.HoldDid != "" && manifest.HoldDid != appview.DefaultHoldDid {
268
268
-
// BYOS scenario - store in hold's PDS
269
269
-
storeSignatureInHold(manifest.HoldDid, signature)
270
270
-
} else {
271
271
-
// Default - store in user's PDS
272
272
-
storeSignatureInUserPDS(userDid, signature)
273
273
-
}
274
274
-
```
39
39
+
**What this proves:**
40
40
+
- ✅ Manifest came from alice's PDS (DID-based identity)
41
41
+
- ✅ Manifest content hasn't been tampered with
42
42
+
- ✅ Manifest was created at a specific time (commit timestamp)
43
43
+
- ✅ Manifest is part of alice's verifiable repository history
275
44
276
276
-
## Signature Format
45
45
+
**Trust model:**
46
46
+
- Public keys distributed via DID documents (PLC directory, did:web)
47
47
+
- Signatures use ECDSA K-256 (secp256k1)
48
48
+
- Verification is decentralized (no central CA required)
49
49
+
- Users control their own DIDs and can rotate keys
277
50
278
278
-
Signatures are stored in a simple format in ATProto and transformed to Cosign-compatible format when served via the OCI Referrers API:
51
51
+
### Signature Metadata
279
52
280
280
-
**ATProto storage format:**
281
281
-
```json
282
282
-
{
283
283
-
"$type": "io.atcr.signature",
284
284
-
"repository": "alice/myapp",
285
285
-
"digest": "sha256:abc123...",
286
286
-
"signature": "base64-encoded-signature-bytes",
287
287
-
"keyId": "credential-helper-default",
288
288
-
"signatureAlgorithm": "ecdsa-p256-sha256",
289
289
-
"signedAt": "2025-10-20T12:34:56Z",
290
290
-
"format": "simple"
291
291
-
}
292
292
-
```
53
53
+
In addition to ATProto's native commit signatures, ATCR creates **ORAS signature artifacts** that bridge ATProto signatures to the OCI ecosystem:
293
54
294
294
-
**OCI Referrers format (served by AppView):**
295
55
```json
296
56
{
297
297
-
"schemaVersion": 2,
298
298
-
"mediaType": "application/vnd.oci.image.index.v1+json",
299
299
-
"manifests": [{
300
300
-
"mediaType": "application/vnd.oci.image.manifest.v1+json",
301
301
-
"digest": "sha256:...",
302
302
-
"artifactType": "application/vnd.dev.cosign.simplesigning.v1+json",
303
303
-
"annotations": {
304
304
-
"dev.sigstore.cosign.signature": "MEUCIQDx...",
305
305
-
"io.atcr.keyId": "credential-helper-default",
306
306
-
"io.atcr.signedAt": "2025-10-20T12:34:56Z"
307
307
-
}
308
308
-
}]
57
57
+
"$type": "io.atcr.atproto.signature",
58
58
+
"version": "1.0",
59
59
+
"subject": {
60
60
+
"digest": "sha256:abc123...",
61
61
+
"mediaType": "application/vnd.oci.image.manifest.v1+json"
62
62
+
},
63
63
+
"atproto": {
64
64
+
"did": "did:plc:alice123",
65
65
+
"handle": "alice.bsky.social",
66
66
+
"pdsEndpoint": "https://bsky.social",
67
67
+
"recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123",
68
68
+
"commitCid": "bafyreih8...",
69
69
+
"signedAt": "2025-10-31T12:34:56.789Z"
70
70
+
},
71
71
+
"signature": {
72
72
+
"algorithm": "ECDSA-K256-SHA256",
73
73
+
"keyId": "did:plc:alice123#atproto",
74
74
+
"publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z"
75
75
+
}
309
76
}
310
77
```
311
78
312
312
-
This allows:
313
313
-
- Simple storage in ATProto
314
314
-
- Compatible with Cosign verification
315
315
-
- No duplicate storage needed
316
316
-
317
317
-
## ATProto Records
318
318
-
319
319
-
### io.atcr.signing.key - Public Signing Keys
79
79
+
**Stored as:**
80
80
+
- OCI artifact with `artifactType: application/vnd.atproto.signature.v1+json`
81
81
+
- Linked to image manifest via OCI Referrers API
82
82
+
- Discoverable by standard OCI tools (ORAS, Cosign, Crane)
320
83
321
321
-
```json
322
322
-
{
323
323
-
"$type": "io.atcr.signing.key",
324
324
-
"keyId": "credential-helper-default",
325
325
-
"keyType": "ecdsa-p256",
326
326
-
"publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZI...",
327
327
-
"validFrom": "2025-10-20T12:00:00Z",
328
328
-
"expiresAt": "2026-10-20T12:00:00Z",
329
329
-
"revoked": false,
330
330
-
"purpose": ["image-signing"],
331
331
-
"deviceId": "alice-macbook-pro",
332
332
-
"comment": "Generated by docker-credential-atcr",
333
333
-
"createdAt": "2025-10-20T12:00:00Z"
334
334
-
}
335
335
-
```
84
84
+
## Verification
336
85
337
337
-
**Record key:** `keyId` (user-chosen identifier)
86
86
+
### Quick Verification (Shell Script)
338
87
339
339
-
**Fields:**
340
340
-
- `keyId`: Unique identifier (e.g., `credential-helper-default`, `ci-key-1`)
341
341
-
- `keyType`: Algorithm (ecdsa-p256, ed25519, rsa-2048, rsa-4096)
342
342
-
- `publicKey`: PEM-encoded public key
343
343
-
- `validFrom`: Key becomes valid at this time
344
344
-
- `expiresAt`: Key expires (null = no expiry)
345
345
-
- `revoked`: Revocation status
346
346
-
- `purpose`: Key purposes (image-signing, sbom-signing, etc.)
347
347
-
- `deviceId`: Optional device identifier
348
348
-
- `comment`: Optional human-readable comment
88
88
+
For manual verification, use the provided shell scripts:
349
89
350
350
-
### io.atcr.signature - Image Signatures
90
90
+
```bash
91
91
+
# Verify an image
92
92
+
./examples/verification/atcr-verify.sh atcr.io/alice/myapp:latest
351
93
352
352
-
```json
353
353
-
{
354
354
-
"$type": "io.atcr.signature",
355
355
-
"repository": "alice/myapp",
356
356
-
"digest": "sha256:abc123...",
357
357
-
"signature": "MEUCIQDxH7...",
358
358
-
"keyId": "credential-helper-default",
359
359
-
"signatureAlgorithm": "ecdsa-p256-sha256",
360
360
-
"signedAt": "2025-10-20T12:34:56Z",
361
361
-
"format": "simple",
362
362
-
"createdAt": "2025-10-20T12:34:56Z"
363
363
-
}
94
94
+
# Output shows:
95
95
+
# - DID and handle of signer
96
96
+
# - PDS endpoint
97
97
+
# - ATProto record URI
98
98
+
# - Signature verification status
364
99
```
365
100
366
366
-
**Record key:** SHA256 hash of `(digest || keyId)` for deduplication
367
367
-
368
368
-
**Fields:**
369
369
-
- `repository`: Image repository (alice/myapp)
370
370
-
- `digest`: Manifest digest being signed (sha256:...)
371
371
-
- `signature`: Base64-encoded signature bytes
372
372
-
- `keyId`: Reference to signing key record
373
373
-
- `signatureAlgorithm`: Algorithm used
374
374
-
- `signedAt`: Timestamp of signature creation
375
375
-
- `format`: Signature format (simple, cosign, notary)
101
101
+
**See:** [examples/verification/README.md](../examples/verification/README.md) for complete examples including:
102
102
+
- Standalone verification script
103
103
+
- Secure pull wrapper (verify before pull)
104
104
+
- Kubernetes webhook deployment
105
105
+
- CI/CD integration examples
376
106
377
377
-
## Verification
378
378
-
379
379
-
Image signatures are verified using standard tools (Cosign, Notary) via the OCI Referrers API bridge. AppView transparently serves ATProto signatures as OCI artifacts, so verification "just works" with existing tooling.
380
380
-
381
381
-
### Integration with Docker/Kubernetes Workflows
382
382
-
383
383
-
**The challenge:** Cosign and Notary plugins are for **key management** (custom KMS, HSMs), not **signature storage**. Both tools expect signatures stored as OCI artifacts in the registry itself.
107
107
+
### Standard Tools (Discovery Only)
384
108
385
385
-
**Reality check:**
386
386
-
- Cosign looks for signatures as OCI referrers or attached manifests
387
387
-
- Notary looks for signatures in registry's `_notary` endpoint
388
388
-
- Kubernetes admission controllers (Sigstore Policy Controller, Ratify) use these tools
389
389
-
- They won't find signatures stored only in ATProto
109
109
+
Standard OCI tools can **discover** ATProto signature artifacts but cannot **verify** them (different signature format):
390
110
391
391
-
**The solution:** AppView implements the **OCI Referrers API** and serves ATProto signatures as OCI artifacts on-demand.
111
111
+
```bash
112
112
+
# Discover signatures with ORAS
113
113
+
oras discover atcr.io/alice/myapp:latest \
114
114
+
--artifact-type application/vnd.atproto.signature.v1+json
392
115
393
393
-
### How It Works: OCI Referrers API Bridge
116
116
+
# Fetch signature metadata
117
117
+
oras pull atcr.io/alice/myapp@sha256:sig789...
394
118
395
395
-
When Cosign/Notary verify an image, they call the OCI Referrers API:
396
396
-
397
397
-
```
398
398
-
cosign verify atcr.io/alice/myapp:latest
399
399
-
↓
400
400
-
GET /v2/alice/myapp/referrers/sha256:abc123
401
401
-
↓
402
402
-
AppView:
403
403
-
1. Queries alice's PDS for io.atcr.signature records
404
404
-
2. Filters signatures matching digest sha256:abc123
405
405
-
3. Transforms to OCI referrers format
406
406
-
4. Returns as JSON
407
407
-
↓
408
408
-
Cosign receives OCI referrer manifest
409
409
-
↓
410
410
-
Verifies signature (works normally)
119
119
+
# View with Cosign (discovery only)
120
120
+
cosign tree atcr.io/alice/myapp:latest
411
121
```
412
122
413
413
-
**AppView endpoint implementation:**
123
123
+
**Note:** Cosign/Notary cannot verify ATProto signatures directly because they use a different signature format and trust model. Use integration plugins or the `atcr-verify` CLI tool instead.
414
124
415
415
-
```go
416
416
-
// GET /v2/{owner}/{repo}/referrers/{digest}
417
417
-
func (h *Handler) GetReferrers(w http.ResponseWriter, r *http.Request) {
418
418
-
owner := mux.Vars(r)["owner"]
419
419
-
digest := mux.Vars(r)["digest"]
125
125
+
## Integration Options
420
126
421
421
-
// 1. Resolve owner → DID → PDS
422
422
-
did, pds, err := h.resolver.ResolveIdentity(owner)
127
127
+
ATCR supports multiple integration approaches depending on your use case:
423
128
424
424
-
// 2. Query PDS for signatures matching digest
425
425
-
signatures, err := h.atproto.ListRecords(pds, did, "io.atcr.signature")
426
426
-
filtered := filterByDigest(signatures, digest)
129
129
+
### 1. **Plugins (Recommended for Kubernetes)** ⭐
427
130
428
428
-
// 3. Transform to OCI Index format
429
429
-
index := &ocispec.Index{
430
430
-
SchemaVersion: 2,
431
431
-
MediaType: ocispec.MediaTypeImageIndex,
432
432
-
Manifests: []ocispec.Descriptor{},
433
433
-
}
131
131
+
Build plugins for existing policy/verification engines:
434
132
435
435
-
for _, sig := range filtered {
436
436
-
index.Manifests = append(index.Manifests, ocispec.Descriptor{
437
437
-
MediaType: "application/vnd.oci.image.manifest.v1+json",
438
438
-
Digest: sig.Digest,
439
439
-
Size: sig.Size,
440
440
-
ArtifactType: "application/vnd.dev.cosign.simplesigning.v1+json",
441
441
-
Annotations: map[string]string{
442
442
-
"dev.sigstore.cosign.signature": sig.Signature,
443
443
-
"io.atcr.keyId": sig.KeyId,
444
444
-
"io.atcr.signedAt": sig.SignedAt,
445
445
-
"io.atcr.source": fmt.Sprintf("at://%s/io.atcr.signature/%s", did, sig.Rkey),
446
446
-
},
447
447
-
})
448
448
-
}
133
133
+
**Ratify Verifier Plugin:**
134
134
+
- Integrates with OPA Gatekeeper
135
135
+
- Verifies ATProto signatures using Ratify's plugin interface
136
136
+
- Policy-based enforcement for Kubernetes
449
137
450
450
-
// 4. Return as JSON
451
451
-
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
452
452
-
json.NewEncoder(w).Encode(index)
453
453
-
}
454
454
-
```
138
138
+
**OPA Gatekeeper External Provider:**
139
139
+
- HTTP service that verifies ATProto signatures
140
140
+
- Rego policies call external provider
141
141
+
- Flexible and easy to deploy
455
142
456
456
-
**Benefits:**
457
457
-
- ✅ **No dual storage** - signatures only in ATProto
458
458
-
- ✅ **Standard tools work** - Cosign, Notary, Kubernetes admission controllers
459
459
-
- ✅ **Single source of truth** - ATProto PDS
460
460
-
- ✅ **On-demand transformation** - only when needed
461
461
-
- ✅ **Offline verification** - can cache public keys
143
143
+
**Containerd 2.0 Bindir Plugin:**
144
144
+
- Verifies signatures at containerd level
145
145
+
- Works with any CRI-compatible runtime
146
146
+
- No Kubernetes required
462
147
463
463
-
**Trade-offs:**
464
464
-
- ⚠️ AppView must be reachable during verification (but already required for image pulls)
465
465
-
- ⚠️ Transformation overhead (minimal - just JSON formatting)
466
466
-
467
467
-
### Alternative Approaches
468
468
-
469
469
-
#### Option 1: Dual Storage (Not Recommended)
148
148
+
**See:** [docs/SIGNATURE_INTEGRATION.md](./SIGNATURE_INTEGRATION.md) for complete plugin implementation examples
470
149
471
471
-
Store signatures in BOTH ATProto AND OCI registry:
150
150
+
### 2. **CLI Tool (atcr-verify)**
472
151
473
473
-
```go
474
474
-
// In credential helper or AppView
475
475
-
func StoreSignature(sig Signature) error {
476
476
-
// 1. Store in ATProto (user's PDS or hold's PDS)
477
477
-
err := storeInATProto(sig)
152
152
+
Standalone CLI tool for signature verification:
478
153
479
479
-
// 2. ALSO store as OCI artifact in registry
480
480
-
err = storeAsOCIReferrer(sig)
154
154
+
```bash
155
155
+
# Install
156
156
+
go install github.com/atcr-io/atcr/cmd/atcr-verify@latest
481
157
482
482
-
return err
483
483
-
}
484
484
-
```
158
158
+
# Verify image
159
159
+
atcr-verify atcr.io/alice/myapp:latest --policy trust-policy.yaml
485
160
486
486
-
**OCI Referrer format:**
487
487
-
```json
488
488
-
{
489
489
-
"schemaVersion": 2,
490
490
-
"mediaType": "application/vnd.oci.image.manifest.v1+json",
491
491
-
"artifactType": "application/vnd.dev.cosign.simplesigning.v1+json",
492
492
-
"subject": {
493
493
-
"digest": "sha256:abc123...",
494
494
-
"mediaType": "application/vnd.oci.image.manifest.v1+json"
495
495
-
},
496
496
-
"layers": [{
497
497
-
"mediaType": "application/vnd.dev.cosign.simplesigning.v1+json",
498
498
-
"digest": "sha256:sig...",
499
499
-
"annotations": {
500
500
-
"dev.sigstore.cosign.signature": "MEUCIQDx...",
501
501
-
"io.atcr.source": "atproto://did:plc:alice123/io.atcr.signature/..."
502
502
-
}
503
503
-
}]
504
504
-
}
161
161
+
# Use in CI/CD
162
162
+
atcr-verify $IMAGE --quiet && kubectl apply -f deployment.yaml
505
163
```
506
164
507
507
-
**Benefits:**
508
508
-
- ✅ Works with standard Cosign verification
509
509
-
- ✅ Kubernetes admission controllers work out of box
510
510
-
- ✅ ATProto signatures still available for discovery
511
511
-
- ✅ Cross-reference via `io.atcr.source` annotation
165
165
+
**Features:**
166
166
+
- Trust policy management (which DIDs to trust)
167
167
+
- Multiple output formats (text, JSON, SARIF)
168
168
+
- Offline verification with cached DID documents
169
169
+
- Library usage for custom integrations
512
170
513
513
-
**Trade-offs:**
514
514
-
- ❌ Duplicate storage (ATProto + OCI)
515
515
-
- ❌ Consistency issues (what if one write fails?)
516
516
-
- ❌ Signatures tied to specific registry
171
171
+
**See:** [docs/ATCR_VERIFY_CLI.md](./ATCR_VERIFY_CLI.md) for complete CLI specification
517
172
518
518
-
#### Option 2: Custom Admission Controller
173
173
+
### 3. **External Services**
519
174
520
520
-
Write Kubernetes admission controller that understands ATProto:
175
175
+
Deploy verification as a service:
521
176
177
177
+
**GitHub Actions:**
522
178
```yaml
523
523
-
# admission-controller deployment
524
524
-
apiVersion: v1
525
525
-
kind: ConfigMap
526
526
-
metadata:
527
527
-
name: atcr-policy
528
528
-
data:
529
529
-
policy.yaml: |
530
530
-
policies:
531
531
-
- name: require-atcr-signatures
532
532
-
images:
533
533
-
- "atcr.io/*/*"
534
534
-
verification:
535
535
-
method: atproto
536
536
-
requireSignature: true
179
179
+
- name: Verify image signature
180
180
+
uses: atcr-io/atcr-verify-action@v1
181
181
+
with:
182
182
+
image: atcr.io/alice/myapp:${{ github.sha }}
183
183
+
policy: .atcr/trust-policy.yaml
537
184
```
538
185
539
539
-
**Benefits:**
540
540
-
- ✅ Native ATProto support
541
541
-
- ✅ No OCI conversion needed
542
542
-
- ✅ Can enforce ATCR-specific policies
186
186
+
**GitLab CI, Jenkins, CircleCI:**
187
187
+
- Use `atcr-verify` CLI in pipeline
188
188
+
- Fail build if verification fails
189
189
+
- Enforce signature requirements before deployment
543
190
544
544
-
**Trade-offs:**
545
545
-
- ❌ Doesn't work with standard tools (Cosign, Notary)
546
546
-
- ❌ Additional infrastructure to maintain
547
547
-
- ❌ Limited ecosystem integration
191
191
+
### 4. **X.509 Certificates (Hold-as-CA)** ⚠️
548
192
549
549
-
#### Recommendation
193
193
+
Optional approach where hold services issue X.509 certificates based on ATProto signatures:
550
194
551
551
-
**Primary approach: OCI Referrers API Bridge**
552
552
-
- Implement `/v2/{owner}/{repo}/referrers/{digest}` in AppView
553
553
-
- Query ATProto on-demand and transform to OCI format
554
554
-
- Works with Cosign, Notary, Kubernetes admission controllers
555
555
-
- No duplicate storage, single source of truth
195
195
+
**Use cases:**
196
196
+
- Enterprise environments requiring PKI compliance
197
197
+
- Tools that only support X.509 (legacy systems)
198
198
+
- Notation integration (P-256 certificates)
556
199
557
557
-
**Why this works:**
558
558
-
- Cosign/Notary just make HTTP GET requests to the registry
559
559
-
- AppView is already the registry - just add one endpoint
560
560
-
- Transformation is simple (ATProto record → OCI descriptor)
561
561
-
- Signatures stay in ATProto where they belong
562
562
-
563
563
-
### Cosign Verification (OCI Referrers API)
564
564
-
565
565
-
```bash
566
566
-
# Standard Cosign works out of the box:
567
567
-
cosign verify atcr.io/alice/myapp:latest \
568
568
-
--key <(atcr-cli key export alice credential-helper-default)
200
200
+
**Trade-offs:**
201
201
+
- ❌ Introduces centralization (hold acts as CA)
202
202
+
- ❌ Trust shifts from DIDs to hold operator
203
203
+
- ❌ Requires hold service infrastructure
569
204
570
570
-
# What happens:
571
571
-
# 1. Cosign queries: GET /v2/alice/myapp/referrers/sha256:abc123
572
572
-
# 2. AppView fetches signatures from alice's PDS
573
573
-
# 3. AppView returns OCI referrers index
574
574
-
# 4. Cosign downloads signature artifact
575
575
-
# 5. Cosign verifies with public key
576
576
-
# 6. Success!
205
205
+
**See:** [docs/HOLD_AS_CA.md](./HOLD_AS_CA.md) for complete architecture and security considerations
577
206
578
578
-
# Or with public key inline:
579
579
-
cosign verify atcr.io/alice/myapp:latest --key '-----BEGIN PUBLIC KEY-----
580
580
-
MFkwEwYHKoZI...
581
581
-
-----END PUBLIC KEY-----'
582
582
-
```
207
207
+
## Integration Strategy Decision Matrix
583
208
584
584
-
**Fetching public keys from ATProto:**
209
209
+
Choose the right integration approach:
585
210
586
586
-
Public keys are stored in ATProto records and can be fetched via standard XRPC:
211
211
+
| Use Case | Recommended Approach | Priority |
212
212
+
|----------|---------------------|----------|
213
213
+
| **Kubernetes admission control** | Ratify plugin or Gatekeeper provider | HIGH |
214
214
+
| **CI/CD verification** | atcr-verify CLI or GitHub Actions | HIGH |
215
215
+
| **Docker/containerd** | Containerd bindir plugin | MEDIUM |
216
216
+
| **Policy enforcement** | OPA Gatekeeper + external provider | HIGH |
217
217
+
| **Manual verification** | Shell scripts or atcr-verify CLI | LOW |
218
218
+
| **Enterprise PKI compliance** | Hold-as-CA (X.509 certificates) | OPTIONAL |
219
219
+
| **Legacy tool support** | Hold-as-CA or external bridge service | OPTIONAL |
587
220
588
588
-
```bash
589
589
-
# Query for public keys
590
590
-
curl "https://atcr.io/xrpc/com.atproto.repo.listRecords?\
591
591
-
repo=did:plc:alice123&\
592
592
-
collection=io.atcr.signing.key"
221
221
+
**See:** [docs/INTEGRATION_STRATEGY.md](./INTEGRATION_STRATEGY.md) for complete integration planning guide including:
222
222
+
- Architecture layers and data flow
223
223
+
- Tool compatibility matrix (16+ tools)
224
224
+
- Implementation roadmap (4 phases)
225
225
+
- When to use each approach
593
226
594
594
-
# Extract public key and save as PEM
595
595
-
# Then use in Cosign:
596
596
-
cosign verify atcr.io/alice/myapp:latest --key alice.pub
597
597
-
```
227
227
+
## Trust Policies
598
228
599
599
-
### Kubernetes Policy Example (OCI Referrers API)
229
229
+
Define which signatures you trust:
600
230
601
231
```yaml
602
602
-
# Sigstore Policy Controller
603
603
-
apiVersion: policy.sigstore.dev/v1beta1
604
604
-
kind: ClusterImagePolicy
605
605
-
metadata:
606
606
-
name: atcr-images-must-be-signed
607
607
-
spec:
608
608
-
images:
609
609
-
- glob: "atcr.io/*/*"
610
610
-
authorities:
611
611
-
- key:
612
612
-
# Public key from ATProto record
613
613
-
data: |
614
614
-
-----BEGIN PUBLIC KEY-----
615
615
-
MFkwEwYHKoZI...
616
616
-
-----END PUBLIC KEY-----
617
617
-
```
232
232
+
# trust-policy.yaml
233
233
+
version: 1.0
618
234
619
619
-
**How it works:**
620
620
-
1. Pod tries to run `atcr.io/alice/myapp:latest`
621
621
-
2. Policy Controller intercepts
622
622
-
3. Queries registry for OCI referrers (finds signature)
623
623
-
4. Verifies signature with public key
624
624
-
5. Allows pod if valid
625
625
-
626
626
-
### Trust Policies
235
235
+
trustedDIDs:
236
236
+
did:plc:alice123:
237
237
+
name: "Alice (DevOps Lead)"
238
238
+
validFrom: "2024-01-01T00:00:00Z"
239
239
+
expiresAt: null
627
240
628
628
-
Define what signatures are required for image execution:
241
241
+
did:plc:bob456:
242
242
+
name: "Bob (Security Team)"
243
243
+
validFrom: "2024-06-01T00:00:00Z"
244
244
+
expiresAt: "2025-12-31T23:59:59Z"
629
245
630
630
-
```yaml
631
631
-
# ~/.atcr/trust-policy.yaml
632
246
policies:
633
247
- name: production-images
634
634
-
scope: "atcr.io/alice/prod-*"
248
248
+
scope: "atcr.io/*/prod-*"
635
249
require:
636
636
-
- signature: true
637
637
-
- keyIds: ["ci-key-1", "alice-release-key"]
638
638
-
action: enforce # block, audit, or allow
250
250
+
signature: true
251
251
+
trustedDIDs:
252
252
+
- did:plc:alice123
253
253
+
- did:plc:bob456
254
254
+
minSignatures: 1
255
255
+
action: enforce # reject if policy fails
639
256
640
257
- name: dev-images
641
641
-
scope: "atcr.io/alice/dev-*"
258
258
+
scope: "atcr.io/*/dev-*"
642
259
require:
643
643
-
- signature: false
644
644
-
action: audit
260
260
+
signature: false
261
261
+
action: audit # log but don't reject
645
262
```
646
263
647
647
-
**Integration points:**
648
648
-
- Kubernetes admission controller
649
649
-
- Docker Content Trust equivalent
650
650
-
- CI/CD pipeline gates
264
264
+
**Use with:**
265
265
+
- `atcr-verify` CLI: `atcr-verify IMAGE --policy trust-policy.yaml`
266
266
+
- Kubernetes webhooks: ConfigMap with policy
267
267
+
- CI/CD pipelines: Fail build if policy not met
651
268
652
269
## Security Considerations
653
270
654
654
-
### Key Storage Security
271
271
+
### What ATProto Signatures Prove
655
272
656
656
-
**OS keychain benefits:**
657
657
-
- ✅ Encrypted storage
658
658
-
- ✅ Access control (requires user password/biometric)
659
659
-
- ✅ Auditing (macOS logs keychain access)
660
660
-
- ✅ Hardware-backed on modern systems (Secure Enclave, TPM)
273
273
+
✅ **Identity:** Manifest signed by specific DID (e.g., `did:plc:alice123`)
274
274
+
✅ **Integrity:** Manifest content hasn't been tampered with
275
275
+
✅ **Timestamp:** When the manifest was signed
276
276
+
✅ **Authenticity:** Signature created with private key for that DID
661
277
662
662
-
**Best practices:**
663
663
-
- Generate keys on device (never transmitted)
664
664
-
- Use hardware-backed storage when available
665
665
-
- Require user approval for key access (biometric/password)
666
666
-
- Rotate keys periodically (e.g., annually)
278
278
+
### What They Don't Prove
667
279
668
668
-
### Trust Model
280
280
+
❌ **Vulnerability-free:** Signature doesn't mean image is safe
281
281
+
❌ **Authorization:** DID ownership doesn't imply permission to deploy
282
282
+
❌ **Key security:** Private key could be compromised
283
283
+
❌ **PDS trustworthiness:** Malicious PDS could create fake records
669
284
670
670
-
**What signatures prove:**
671
671
-
- ✅ User had access to private key at signing time
672
672
-
- ✅ Manifest digest matches what was signed
673
673
-
- ✅ Signature created by specific key ID
674
674
-
- ✅ Timestamp of signature creation
285
285
+
### Trust Dependencies
675
286
676
676
-
**What signatures don't prove:**
677
677
-
- ❌ Image is free of vulnerabilities
678
678
-
- ❌ Image contents are safe to run
679
679
-
- ❌ User's identity is verified (depends on DID trust)
680
680
-
- ❌ Private key wasn't compromised
287
287
+
When verifying signatures, you're trusting:
288
288
+
1. **DID resolution** (PLC directory, did:web) - public key is correct for DID
289
289
+
2. **PDS integrity** - PDS serves correct records and doesn't forge signatures
290
290
+
3. **Cryptographic primitives** - ECDSA K-256 remains secure
291
291
+
4. **Your trust policy** - DIDs you've chosen to trust are legitimate
681
292
682
682
-
**Trust dependencies:**
683
683
-
- User protects their private key
684
684
-
- OS keychain security
685
685
-
- DID resolution accuracy (PLC directory, did:web)
686
686
-
- PDS serves correct public key records
687
687
-
- Signature algorithms remain secure
293
293
+
### Best Practices
688
294
689
689
-
### Multi-Device Support
295
295
+
**1. Use Trust Policies**
296
296
+
Don't blindly trust all signatures - define which DIDs you trust:
297
297
+
```yaml
298
298
+
trustedDIDs:
299
299
+
- did:plc:your-org-team
300
300
+
- did:plc:your-ci-system
301
301
+
```
690
302
691
691
-
**Challenge:** User has multiple devices (laptop, desktop, CI/CD)
303
303
+
**2. Monitor Signature Coverage**
304
304
+
Track which images have signatures:
305
305
+
```bash
306
306
+
atcr-verify --check-coverage namespace/production
307
307
+
```
692
308
693
693
-
**Options:**
694
694
-
695
695
-
1. **Separate keys per device:**
696
696
-
```json
697
697
-
{
698
698
-
"keyId": "alice-macbook-pro",
699
699
-
"deviceId": "macbook-pro"
700
700
-
},
701
701
-
{
702
702
-
"keyId": "alice-desktop",
703
703
-
"deviceId": "desktop"
704
704
-
}
705
705
-
```
706
706
-
- Pros: Best security (key compromise limited to one device)
707
707
-
- Cons: Need to trust signatures from any device
708
708
-
709
709
-
2. **Shared key via secure sync:**
710
710
-
- Export key from primary device
711
711
-
- Import to secondary devices
712
712
-
- Stored in each device's keychain
713
713
-
- Pros: Single key ID to trust
714
714
-
- Cons: More attack surface (key on multiple devices)
715
715
-
716
716
-
3. **Primary + secondary model:**
717
717
-
- Primary key on main device
718
718
-
- Secondary keys on other devices
719
719
-
- Trust policy requires primary key signature
720
720
-
- Pros: Flexible + secure
721
721
-
- Cons: More complex setup
722
722
-
723
723
-
**Recommendation:** Separate keys per device (Option 1) for security, with trust policy accepting any of user's keys.
724
724
-
725
725
-
### Key Compromise Response
726
726
-
727
727
-
If a device is lost or private key is compromised:
728
728
-
729
729
-
1. **Revoke the key** via AppView web UI or XRPC API
730
730
-
- Updates `io.atcr.signing.key` record: `"revoked": true`
731
731
-
- Revocation is atomic and immediate
309
309
+
**3. Enforce in Production**
310
310
+
Use Kubernetes admission control to block unsigned images:
311
311
+
```yaml
312
312
+
# Ratify + Gatekeeper or custom webhook
313
313
+
enforceSignatures: true
314
314
+
failurePolicy: Fail
315
315
+
```
732
316
733
733
-
2. **Generate new key** on new/existing device
734
734
-
- Automatic on next `docker login` from secure device
735
735
-
- Credential helper generates new key pair
317
317
+
**4. Verify in CI/CD**
318
318
+
Never deploy unsigned images:
319
319
+
```yaml
320
320
+
# GitHub Actions
321
321
+
- name: Verify signature
322
322
+
run: atcr-verify $IMAGE || exit 1
323
323
+
```
736
324
737
737
-
3. **Old signatures still exist but fail verification**
738
738
-
- Revoked key = untrusted
739
739
-
- No certificate revocation list (CRL) delays
740
740
-
- Globally visible within seconds
325
325
+
**5. Plan for Compromised Keys**
326
326
+
- Rotate DID keys periodically
327
327
+
- Monitor DID documents for unexpected key changes
328
328
+
- Have incident response plan for key compromise
741
329
742
742
-
### CI/CD Signing
330
330
+
## Implementation Status
743
331
744
744
-
For automated builds, use standard Cosign in your CI pipeline:
332
332
+
### ✅ Available Now
745
333
746
746
-
```yaml
747
747
-
# .github/workflows/build.yml
748
748
-
steps:
749
749
-
- name: Push image
750
750
-
run: docker push atcr.io/alice/myapp:latest
334
334
+
- **ATProto signatures**: All manifests automatically signed by PDS
335
335
+
- **ORAS artifacts**: Signature metadata stored as OCI artifacts
336
336
+
- **OCI Referrers API**: Discovery via standard OCI endpoints
337
337
+
- **Shell scripts**: Manual verification examples
338
338
+
- **Documentation**: Complete integration guides
751
339
752
752
-
- name: Sign with Cosign
753
753
-
run: cosign sign atcr.io/alice/myapp:latest --key ${{ secrets.COSIGN_KEY }}
754
754
-
```
340
340
+
### 🔄 In Development
755
341
756
756
-
**Key management:**
757
757
-
- Generate Cosign key pair: `cosign generate-key-pair`
758
758
-
- Store private key in CI secrets (GitHub Actions, GitLab CI, etc.)
759
759
-
- Publish public key to PDS via XRPC or AppView web UI
760
760
-
- Cosign stores signature via registry's OCI API
761
761
-
- AppView automatically stores in ATProto
342
342
+
- **atcr-verify CLI**: Standalone verification tool
343
343
+
- **Ratify plugin**: Kubernetes integration
344
344
+
- **Gatekeeper provider**: OPA policy enforcement
345
345
+
- **GitHub Actions**: CI/CD integration
762
346
763
763
-
**Or use automatic signing:**
764
764
-
- Configure credential helper in CI environment
765
765
-
- Signatures happen automatically on push
766
766
-
- No explicit signing step needed
347
347
+
### 📋 Planned
767
348
768
768
-
## Implementation Roadmap
349
349
+
- **Containerd plugin**: Runtime-level verification
350
350
+
- **Hold-as-CA**: X.509 certificate generation (optional)
351
351
+
- **Web UI**: Signature viewer in AppView
352
352
+
- **Offline bundles**: Air-gapped verification
769
353
770
770
-
### Phase 1: Core Signing (2-3 weeks)
354
354
+
## Comparison with Other Signing Solutions
771
355
772
772
-
**Week 1: Credential helper key management**
773
773
-
- Generate ECDSA key pair on first run
774
774
-
- Store private key in OS keychain
775
775
-
- Create `io.atcr.signing.key` record in PDS
776
776
-
- Handle key rotation
356
356
+
| Feature | ATCR (ATProto) | Cosign (Sigstore) | Notation (Notary v2) |
357
357
+
|---------|---------------|-------------------|---------------------|
358
358
+
| **Signing** | Automatic (PDS) | Manual or keyless | Manual |
359
359
+
| **Keys** | K-256 (secp256k1) | P-256 or RSA | P-256, P-384, P-521 |
360
360
+
| **Trust** | DID-based | OIDC + Fulcio CA | X.509 PKI |
361
361
+
| **Storage** | ATProto PDS | OCI registry | OCI registry |
362
362
+
| **Centralization** | Decentralized | Centralized (Fulcio) | Configurable |
363
363
+
| **Transparency Log** | ATProto firehose | Rekor | Configurable |
364
364
+
| **Verification** | Custom tools/plugins | Cosign CLI | Notation CLI |
365
365
+
| **Kubernetes** | Plugins (Ratify) | Policy Controller | Policy Controller |
777
366
778
778
-
**Week 2: Signing integration**
779
779
-
- Sign manifest digest after authentication
780
780
-
- Send signature to AppView via XRPC
781
781
-
- AppView stores in user's PDS or hold's PDS
782
782
-
- Error handling and retries
367
367
+
**ATCR advantages:**
368
368
+
- ✅ Decentralized trust (no CA required)
369
369
+
- ✅ Automatic signing (no extra tools)
370
370
+
- ✅ DID-based identity (portable, self-sovereign)
371
371
+
- ✅ Transparent via ATProto firehose
783
372
784
784
-
**Week 3: OCI Referrers API**
785
785
-
- Implement `GET /v2/{owner}/{repo}/referrers/{digest}` in AppView
786
786
-
- Query ATProto for signatures
787
787
-
- Transform to OCI Index format
788
788
-
- Return Cosign-compatible artifacts
789
789
-
- Test with `cosign verify`
373
373
+
**ATCR trade-offs:**
374
374
+
- ⚠️ Requires custom verification tools/plugins
375
375
+
- ⚠️ K-256 not supported by Notation (needs Hold-as-CA)
376
376
+
- ⚠️ Smaller ecosystem than Cosign/Notation
790
377
791
791
-
### Phase 2: Enhanced Features (2-3 weeks)
378
378
+
## Why Not Use Cosign Directly?
792
379
793
793
-
**Key management (credential helper):**
794
794
-
- Key rotation support
795
795
-
- Revocation handling
796
796
-
- Device identification
797
797
-
- Key expiration
380
380
+
**Question:** Why not just integrate with Cosign's keyless signing (OIDC + Fulcio)?
798
381
799
799
-
**Signature storage:**
800
800
-
- Handle manual Cosign signing (via OCI API)
801
801
-
- Store signatures from both automatic and manual flows
802
802
-
- Signature deduplication
803
803
-
- Signature audit logs
382
382
+
**Answer:** ATProto and Cosign use incompatible authentication models:
804
383
805
805
-
**AppView endpoints:**
806
806
-
- XRPC endpoints for key/signature queries
807
807
-
- Web UI for viewing keys and signatures
808
808
-
- Key revocation via web interface
384
384
+
| Requirement | Cosign Keyless | ATProto |
385
385
+
|-------------|---------------|---------|
386
386
+
| **Identity protocol** | OIDC | ATProto OAuth + DPoP |
387
387
+
| **Token format** | JWT from OIDC provider | DPoP-bound access token |
388
388
+
| **CA** | Fulcio (Sigstore CA) | None (DID-based PKI) |
389
389
+
| **Infrastructure** | Fulcio + Rekor + TUF | PDS + DID resolver |
809
390
810
810
-
### Phase 3: Kubernetes Integration (2-3 weeks)
391
391
+
**To make Cosign work, we'd need to:**
392
392
+
1. Deploy Fulcio (certificate authority)
393
393
+
2. Deploy Rekor (transparency log)
394
394
+
3. Deploy TUF (metadata distribution)
395
395
+
4. Build OIDC provider bridge for ATProto OAuth
396
396
+
5. Maintain all this infrastructure
811
397
812
812
-
**Admission controller setup:**
813
813
-
- Documentation for Sigstore Policy Controller
814
814
-
- Example policies for ATCR images
815
815
-
- Public key management (fetch from ATProto)
816
816
-
- Integration testing with real clusters
398
398
+
**Instead:** We leverage ATProto's existing signatures and build lightweight plugins/tools for verification. This is simpler, more decentralized, and aligns with ATCR's design philosophy.
817
399
818
818
-
**Advanced features:**
819
819
-
- Signature caching in AppView (reduce PDS queries)
820
820
-
- Multi-signature support (require N signatures)
821
821
-
- Timestamp verification
822
822
-
- Signature expiration policies
400
400
+
**For tools that need X.509 certificates:** See [Hold-as-CA](./HOLD_AS_CA.md) for an optional centralized approach.
823
401
824
824
-
### Phase 4: UI Integration (1-2 weeks)
402
402
+
## Getting Started
825
403
826
826
-
**AppView web UI:**
827
827
-
- Show signature status on repository pages
828
828
-
- List signing keys for users
829
829
-
- Revoke keys via web interface
830
830
-
- Signature verification badges
404
404
+
### Verify Your First Image
831
405
832
832
-
## Comparison: Automatic vs Manual Signing
406
406
+
```bash
407
407
+
# 1. Check if image has ATProto signature
408
408
+
oras discover atcr.io/alice/myapp:latest \
409
409
+
--artifact-type application/vnd.atproto.signature.v1+json
833
410
834
834
-
| Feature | Automatic (Credential Helper) | Manual (Standard Cosign) |
835
835
-
|---------|-------------------------------|--------------------------|
836
836
-
| **User action** | Zero - happens on push | `cosign sign` after push |
837
837
-
| **Key management** | Automatic generation/storage | User manages keys |
838
838
-
| **Consistency** | Every image signed | Easy to forget |
839
839
-
| **Setup** | Works with credential helper | Install Cosign, generate keys |
840
840
-
| **CI/CD** | Automatic if cred helper configured | Explicit signing step |
841
841
-
| **Flexibility** | Opinionated defaults | Full control over workflow |
842
842
-
| **Use case** | Most users, simple workflows | Advanced users, custom workflows |
411
411
+
# 2. Pull signature metadata
412
412
+
oras pull atcr.io/alice/myapp@sha256:sig789...
843
413
844
844
-
**Recommendation:**
845
845
-
- **Start with automatic**: Best UX, works for most users
846
846
-
- **Use manual** for: CI/CD pipelines, hardware tokens, custom signing workflows
414
414
+
# 3. Verify with shell script
415
415
+
./examples/verification/atcr-verify.sh atcr.io/alice/myapp:latest
847
416
848
848
-
## Complete Workflow Summary
417
417
+
# 4. Use atcr-verify CLI (when available)
418
418
+
atcr-verify atcr.io/alice/myapp:latest --policy trust-policy.yaml
419
419
+
```
849
420
850
850
-
### Option 1: Automatic Signing (Recommended)
421
421
+
### Deploy Kubernetes Verification
851
422
852
423
```bash
853
853
-
# Setup (one time)
854
854
-
docker login atcr.io
855
855
-
# → Credential helper generates ECDSA key pair
856
856
-
# → Private key in OS keychain
857
857
-
# → Public key published to PDS
424
424
+
# 1. Choose an approach
425
425
+
# Option A: Ratify plugin (recommended)
426
426
+
# Option B: Gatekeeper external provider
427
427
+
# Option C: Custom admission webhook
428
428
+
429
429
+
# 2. Follow integration guide
430
430
+
# See docs/SIGNATURE_INTEGRATION.md for step-by-step
858
431
859
859
-
# Push (automatic signing)
860
860
-
docker push atcr.io/alice/myapp:latest
861
861
-
# → Image pushed and signed automatically
862
862
-
# → No extra commands!
432
432
+
# 3. Enable for namespace
433
433
+
kubectl label namespace production atcr-verify=enabled
863
434
864
864
-
# Verify (standard Cosign)
865
865
-
cosign verify atcr.io/alice/myapp:latest --key alice.pub
866
866
-
# → Cosign queries OCI Referrers API
867
867
-
# → AppView returns ATProto signatures as OCI artifacts
868
868
-
# → Verification succeeds ✓
435
435
+
# 4. Test with sample pod
436
436
+
kubectl run test --image=atcr.io/alice/myapp:latest -n production
869
437
```
870
438
871
871
-
### Option 2: Manual Signing (DIY)
439
439
+
### Integrate with CI/CD
872
440
873
441
```bash
874
874
-
# Push image
875
875
-
docker push atcr.io/alice/myapp:latest
442
442
+
# GitHub Actions
443
443
+
- name: Verify signature
444
444
+
run: |
445
445
+
curl -LO https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify
446
446
+
chmod +x atcr-verify
447
447
+
./atcr-verify ${{ env.IMAGE }} --policy .atcr/trust-policy.yaml
876
448
877
877
-
# Sign with Cosign
878
878
-
cosign sign atcr.io/alice/myapp:latest --key cosign.key
879
879
-
# → Cosign stores via OCI API
880
880
-
# → AppView stores in ATProto
881
881
-
882
882
-
# Verify (same as automatic)
883
883
-
cosign verify atcr.io/alice/myapp:latest --key cosign.pub
449
449
+
# GitLab CI
450
450
+
verify_image:
451
451
+
script:
452
452
+
- wget https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify
453
453
+
- chmod +x atcr-verify
454
454
+
- ./atcr-verify $IMAGE --policy .atcr/trust-policy.yaml
884
455
```
885
456
886
886
-
### Kubernetes (Standard Admission Controller)
457
457
+
## Documentation
887
458
888
888
-
```yaml
889
889
-
# Sigstore Policy Controller (standard)
890
890
-
apiVersion: policy.sigstore.dev/v1beta1
891
891
-
kind: ClusterImagePolicy
892
892
-
metadata:
893
893
-
name: atcr-signed-only
894
894
-
spec:
895
895
-
images:
896
896
-
- glob: "atcr.io/*/*"
897
897
-
authorities:
898
898
-
- key:
899
899
-
data: |
900
900
-
-----BEGIN PUBLIC KEY-----
901
901
-
[Alice's public key from ATProto]
902
902
-
-----END PUBLIC KEY-----
903
903
-
```
459
459
+
### Core Documentation
460
460
+
461
461
+
- **[ATProto Signatures](./ATPROTO_SIGNATURES.md)** - Technical deep-dive into signature format and verification
462
462
+
- **[Signature Integration](./SIGNATURE_INTEGRATION.md)** - Tool-specific integration guides (Ratify, Gatekeeper, Containerd)
463
463
+
- **[Integration Strategy](./INTEGRATION_STRATEGY.md)** - High-level overview and decision matrix
464
464
+
- **[atcr-verify CLI](./ATCR_VERIFY_CLI.md)** - CLI tool specification and usage
465
465
+
- **[Hold-as-CA](./HOLD_AS_CA.md)** - Optional X.509 certificate approach
904
466
905
905
-
**How admission control works:**
906
906
-
1. Pod tries to start with `atcr.io/alice/myapp:latest`
907
907
-
2. Policy Controller intercepts
908
908
-
3. Calls `GET /v2/alice/myapp/referrers/sha256:abc123`
909
909
-
4. AppView returns signatures from ATProto
910
910
-
5. Policy Controller verifies with public key
911
911
-
6. Pod allowed to start ✓
467
467
+
### Examples
912
468
913
913
-
### Key Design Points
469
469
+
- **[examples/verification/](../examples/verification/)** - Shell scripts, Kubernetes configs, trust policies
470
470
+
- **[examples/plugins/](../examples/plugins/)** - Plugin skeletons for Ratify, Gatekeeper, Containerd
914
471
915
915
-
**User experience:**
916
916
-
- ✅ Two options: automatic (credential helper) or manual (standard Cosign)
917
917
-
- ✅ Standard verification tools work (Cosign, Notary, Kubernetes)
918
918
-
- ✅ No custom ATCR-specific signing commands
919
919
-
- ✅ User-controlled keys (OS keychain or self-managed)
472
472
+
### External References
920
473
921
921
-
**Architecture:**
922
922
-
- **Signing**: Client-side only (credential helper or Cosign)
923
923
-
- **Storage**: ATProto (user's PDS or hold's PDS via `io.atcr.signature`)
924
924
-
- **Verification**: Standard tools via OCI Referrers API bridge
925
925
-
- **Bridge**: AppView transforms ATProto → OCI format on-demand
474
474
+
- **ATProto:** https://atproto.com/specs/repository (repository commit signing)
475
475
+
- **ORAS:** https://oras.land/ (artifact registry)
476
476
+
- **OCI Referrers API:** https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
477
477
+
- **Ratify:** https://ratify.dev/ (verification framework)
478
478
+
- **OPA Gatekeeper:** https://open-policy-agent.github.io/gatekeeper/
926
479
927
927
-
**Why this works:**
928
928
-
- ✅ No server-side signing needed (impossible with ATProto constraints)
929
929
-
- ✅ Signatures discoverable via ATProto
930
930
-
- ✅ No duplicate storage (single source of truth)
931
931
-
- ✅ Standard OCI compliance for verification
480
480
+
## Support
932
481
933
933
-
## References
482
482
+
For questions or issues:
483
483
+
- GitHub Issues: https://github.com/atcr-io/atcr/issues
484
484
+
- Documentation: https://docs.atcr.io
485
485
+
- Security: security@atcr.io
934
486
935
935
-
### Signing & Verification
936
936
-
- [Sigstore Cosign](https://github.com/sigstore/cosign)
937
937
-
- [Notary v2 Specification](https://notaryproject.dev/)
938
938
-
- [Cosign Signature Specification](https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md)
487
487
+
## Summary
939
488
940
940
-
### OCI & Registry
941
941
-
- [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec)
942
942
-
- [OCI Referrers API](https://github.com/oras-project/artifacts-spec/blob/main/manifest-referrers-api.md)
943
943
-
- [OCI Artifacts](https://github.com/opencontainers/artifacts)
489
489
+
**Key Points:**
944
490
945
945
-
### ATProto
946
946
-
- [ATProto Specification](https://atproto.com/)
947
947
-
- [ATProto Repository Specification](https://atproto.com/specs/repository)
491
491
+
1. **Automatic signing**: Every ATCR image is automatically signed via ATProto's native signature system
492
492
+
2. **No additional tools**: Signing happens transparently when you push images
493
493
+
3. **Decentralized trust**: DID-based signatures, no central CA required
494
494
+
4. **Standard discovery**: ORAS artifacts and OCI Referrers API for signature metadata
495
495
+
5. **Custom verification**: Use plugins, CLI tools, or shell scripts (not Cosign directly)
496
496
+
6. **Multiple integrations**: Kubernetes (Ratify, Gatekeeper), CI/CD (atcr-verify), containerd
497
497
+
7. **Optional X.509**: Hold-as-CA for enterprise PKI compliance (centralized)
948
498
949
949
-
### Key Management
950
950
-
- [Docker Credential Helpers](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers)
951
951
-
- [macOS Keychain Services](https://developer.apple.com/documentation/security/keychain_services)
952
952
-
- [Windows Credential Manager](https://docs.microsoft.com/en-us/windows/security/identity-protection/credential-guard/)
953
953
-
- [Linux Secret Service API](https://specifications.freedesktop.org/secret-service/)
499
499
+
**Next Steps:**
954
500
955
955
-
### Kubernetes Integration
956
956
-
- [Sigstore Policy Controller](https://docs.sigstore.dev/policy-controller/overview/)
957
957
-
- [Ratify (Notary verification for Kubernetes)](https://ratify.dev/)
501
501
+
1. Read [examples/verification/README.md](../examples/verification/README.md) for practical examples
502
502
+
2. Choose integration approach from [INTEGRATION_STRATEGY.md](./INTEGRATION_STRATEGY.md)
503
503
+
3. Implement plugin or deploy CLI tool from [SIGNATURE_INTEGRATION.md](./SIGNATURE_INTEGRATION.md)
504
504
+
4. Define trust policy for your organization
505
505
+
5. Deploy to test environment first, then production
+692
docs/INTEGRATION_STRATEGY.md
···
1
1
+
# ATCR Signature Verification Integration Strategy
2
2
+
3
3
+
## Overview
4
4
+
5
5
+
This document provides a comprehensive overview of how to integrate ATProto signature verification into various tools and workflows. ATCR uses a layered approach that provides maximum compatibility while maintaining ATProto's decentralized philosophy.
6
6
+
7
7
+
## Architecture Layers
8
8
+
9
9
+
```
10
10
+
┌─────────────────────────────────────────────────────────┐
11
11
+
│ Layer 4: Applications & Workflows │
12
12
+
│ - CI/CD pipelines │
13
13
+
│ - Kubernetes admission control │
14
14
+
│ - Runtime verification │
15
15
+
│ - Security scanning │
16
16
+
└──────────────────────┬──────────────────────────────────┘
17
17
+
↓
18
18
+
┌─────────────────────────────────────────────────────────┐
19
19
+
│ Layer 3: Integration Methods │
20
20
+
│ - Plugins (Ratify, Gatekeeper, Containerd) │
21
21
+
│ - CLI tools (atcr-verify) │
22
22
+
│ - External services (webhooks, APIs) │
23
23
+
│ - (Optional) X.509 certificates (hold-as-CA) │
24
24
+
└──────────────────────┬──────────────────────────────────┘
25
25
+
↓
26
26
+
┌─────────────────────────────────────────────────────────┐
27
27
+
│ Layer 2: Signature Discovery │
28
28
+
│ - OCI Referrers API (GET /v2/.../referrers/...) │
29
29
+
│ - ORAS artifact format │
30
30
+
│ - artifactType: application/vnd.atproto.signature... │
31
31
+
└──────────────────────┬──────────────────────────────────┘
32
32
+
↓
33
33
+
┌─────────────────────────────────────────────────────────┐
34
34
+
│ Layer 1: ATProto Signatures (Foundation) │
35
35
+
│ - Manifests signed by PDS (K-256) │
36
36
+
│ - Signatures in ATProto repository commits │
37
37
+
│ - Public keys in DID documents │
38
38
+
│ - DID-based identity │
39
39
+
└─────────────────────────────────────────────────────────┘
40
40
+
```
41
41
+
42
42
+
## Integration Approaches
43
43
+
44
44
+
### Approach 1: Plugin-Based (RECOMMENDED) ⭐
45
45
+
46
46
+
**Best for:** Kubernetes, standard tooling, production deployments
47
47
+
48
48
+
Integrate through plugin systems of existing tools:
49
49
+
50
50
+
#### Ratify Verifier Plugin
51
51
+
- **Use case:** Kubernetes admission control via Gatekeeper
52
52
+
- **Effort:** 2-3 weeks to build
53
53
+
- **Maturity:** CNCF Sandbox project, growing adoption
54
54
+
- **Benefits:**
55
55
+
- ✅ Standard plugin interface
56
56
+
- ✅ Works with existing Ratify deployments
57
57
+
- ✅ Policy-based enforcement
58
58
+
- ✅ Multi-verifier support (can combine with Notation, Cosign)
59
59
+
60
60
+
**Implementation:**
61
61
+
```go
62
62
+
// Ratify plugin interface
63
63
+
type ReferenceVerifier interface {
64
64
+
VerifyReference(
65
65
+
ctx context.Context,
66
66
+
subjectRef common.Reference,
67
67
+
referenceDesc ocispecs.ReferenceDescriptor,
68
68
+
store referrerStore.ReferrerStore,
69
69
+
) (VerifierResult, error)
70
70
+
}
71
71
+
```
72
72
+
73
73
+
**Deployment:**
74
74
+
```yaml
75
75
+
apiVersion: config.ratify.deislabs.io/v1beta1
76
76
+
kind: Verifier
77
77
+
metadata:
78
78
+
name: atcr-verifier
79
79
+
spec:
80
80
+
name: atproto
81
81
+
artifactType: application/vnd.atproto.signature.v1+json
82
82
+
parameters:
83
83
+
trustedDIDs:
84
84
+
- did:plc:alice123
85
85
+
```
86
86
+
87
87
+
See [Ratify Integration Guide](./SIGNATURE_INTEGRATION.md#ratify-plugin)
88
88
+
89
89
+
---
90
90
+
91
91
+
#### OPA Gatekeeper External Provider
92
92
+
- **Use case:** Kubernetes admission control with OPA policies
93
93
+
- **Effort:** 2-3 weeks to build
94
94
+
- **Maturity:** Very stable, widely adopted
95
95
+
- **Benefits:**
96
96
+
- ✅ Rego-based policies (flexible)
97
97
+
- ✅ External data provider API (standard)
98
98
+
- ✅ Can reuse existing Gatekeeper deployments
99
99
+
100
100
+
**Implementation:**
101
101
+
```go
102
102
+
// External data provider
103
103
+
type Provider struct {
104
104
+
verifier *atproto.Verifier
105
105
+
}
106
106
+
107
107
+
func (p *Provider) Provide(ctx context.Context, req ProviderRequest) (*ProviderResponse, error) {
108
108
+
image := req.Keys["image"]
109
109
+
result, err := p.verifier.Verify(ctx, image)
110
110
+
return &ProviderResponse{
111
111
+
Data: map[string]bool{"verified": result.Verified},
112
112
+
}, nil
113
113
+
}
114
114
+
```
115
115
+
116
116
+
**Policy:**
117
117
+
```rego
118
118
+
package verify
119
119
+
120
120
+
violation[{"msg": msg}] {
121
121
+
container := input.review.object.spec.containers[_]
122
122
+
startswith(container.image, "atcr.io/")
123
123
+
124
124
+
response := external_data({
125
125
+
"provider": "atcr-verifier",
126
126
+
"keys": ["image"],
127
127
+
"values": [container.image]
128
128
+
})
129
129
+
130
130
+
response.verified != true
131
131
+
msg := sprintf("Image %v has no valid ATProto signature", [container.image])
132
132
+
}
133
133
+
```
134
134
+
135
135
+
See [Gatekeeper Integration Guide](./SIGNATURE_INTEGRATION.md#opa-gatekeeper-external-provider)
136
136
+
137
137
+
---
138
138
+
139
139
+
#### Containerd 2.0 Image Verifier Plugin
140
140
+
- **Use case:** Runtime verification at image pull time
141
141
+
- **Effort:** 1-2 weeks to build
142
142
+
- **Maturity:** New in Containerd 2.0 (Nov 2024)
143
143
+
- **Benefits:**
144
144
+
- ✅ Runtime enforcement (pull-time verification)
145
145
+
- ✅ Works for Docker, nerdctl, ctr
146
146
+
- ✅ Transparent to users
147
147
+
- ✅ No Kubernetes required
148
148
+
149
149
+
**Limitation:** CRI plugin integration still maturing
150
150
+
151
151
+
**Implementation:**
152
152
+
```bash
153
153
+
#!/bin/bash
154
154
+
# /usr/local/bin/containerd-verifiers/atcr-verifier
155
155
+
# Binary called by containerd on image pull
156
156
+
157
157
+
# Containerd passes image info via stdin
158
158
+
read -r INPUT
159
159
+
160
160
+
IMAGE=$(echo "$INPUT" | jq -r '.reference')
161
161
+
DIGEST=$(echo "$INPUT" | jq -r '.descriptor.digest')
162
162
+
163
163
+
# Verify signature
164
164
+
if atcr-verify "$IMAGE@$DIGEST" --quiet; then
165
165
+
exit 0 # Verified
166
166
+
else
167
167
+
exit 1 # Failed
168
168
+
fi
169
169
+
```
170
170
+
171
171
+
**Configuration:**
172
172
+
```toml
173
173
+
# /etc/containerd/config.toml
174
174
+
[plugins."io.containerd.image-verifier.v1.bindir"]
175
175
+
bin_dir = "/usr/local/bin/containerd-verifiers"
176
176
+
max_verifiers = 5
177
177
+
per_verifier_timeout = "10s"
178
178
+
```
179
179
+
180
180
+
See [Containerd Integration Guide](./SIGNATURE_INTEGRATION.md#containerd-20)
181
181
+
182
182
+
---
183
183
+
184
184
+
### Approach 2: CLI Tool (RECOMMENDED) ⭐
185
185
+
186
186
+
**Best for:** CI/CD, scripts, general-purpose verification
187
187
+
188
188
+
Use `atcr-verify` CLI tool directly in workflows:
189
189
+
190
190
+
#### Command-Line Verification
191
191
+
```bash
192
192
+
# Basic verification
193
193
+
atcr-verify atcr.io/alice/myapp:latest
194
194
+
195
195
+
# With trust policy
196
196
+
atcr-verify atcr.io/alice/myapp:latest --policy trust-policy.yaml
197
197
+
198
198
+
# JSON output for scripting
199
199
+
atcr-verify atcr.io/alice/myapp:latest --output json
200
200
+
201
201
+
# Quiet mode for exit codes
202
202
+
atcr-verify atcr.io/alice/myapp:latest --quiet && echo "Verified"
203
203
+
```
204
204
+
205
205
+
#### CI/CD Integration
206
206
+
207
207
+
**GitHub Actions:**
208
208
+
```yaml
209
209
+
- name: Verify image
210
210
+
run: atcr-verify ${{ env.IMAGE }} --policy .github/trust-policy.yaml
211
211
+
```
212
212
+
213
213
+
**GitLab CI:**
214
214
+
```yaml
215
215
+
verify:
216
216
+
image: atcr.io/atcr/verify:latest
217
217
+
script:
218
218
+
- atcr-verify ${IMAGE} --policy trust-policy.yaml
219
219
+
```
220
220
+
221
221
+
**Universal Container:**
222
222
+
```bash
223
223
+
docker run --rm atcr.io/atcr/verify:latest verify IMAGE
224
224
+
```
225
225
+
226
226
+
**Benefits:**
227
227
+
- ✅ Works everywhere (not just Kubernetes)
228
228
+
- ✅ Simple integration (single binary)
229
229
+
- ✅ No plugin installation required
230
230
+
- ✅ Offline mode support
231
231
+
232
232
+
See [atcr-verify CLI Documentation](./ATCR_VERIFY_CLI.md)
233
233
+
234
234
+
---
235
235
+
236
236
+
### Approach 3: External Services
237
237
+
238
238
+
**Best for:** Custom admission controllers, API-based verification
239
239
+
240
240
+
Build verification as a service that tools can call:
241
241
+
242
242
+
#### Webhook Service
243
243
+
```go
244
244
+
// HTTP endpoint for verification
245
245
+
func (h *Handler) VerifyImage(w http.ResponseWriter, r *http.Request) {
246
246
+
image := r.URL.Query().Get("image")
247
247
+
248
248
+
result, err := h.verifier.Verify(r.Context(), image)
249
249
+
if err != nil {
250
250
+
http.Error(w, err.Error(), http.StatusInternalServerError)
251
251
+
return
252
252
+
}
253
253
+
254
254
+
json.NewEncoder(w).Encode(map[string]interface{}{
255
255
+
"verified": result.Verified,
256
256
+
"did": result.Signature.DID,
257
257
+
"signedAt": result.Signature.SignedAt,
258
258
+
})
259
259
+
}
260
260
+
```
261
261
+
262
262
+
#### Usage from Kyverno
263
263
+
```yaml
264
264
+
verifyImages:
265
265
+
- imageReferences:
266
266
+
- "atcr.io/*/*"
267
267
+
attestors:
268
268
+
- entries:
269
269
+
- api:
270
270
+
url: http://atcr-verify.kube-system/verify?image={{ image }}
271
271
+
```
272
272
+
273
273
+
**Benefits:**
274
274
+
- ✅ Flexible integration
275
275
+
- ✅ Centralized verification logic
276
276
+
- ✅ Caching and rate limiting
277
277
+
- ✅ Can add additional checks (vulnerability scanning, etc.)
278
278
+
279
279
+
---
280
280
+
281
281
+
### Approach 4: Hold-as-CA (OPTIONAL, ENTERPRISE ONLY)
282
282
+
283
283
+
**Best for:** Enterprise X.509 PKI compliance requirements
284
284
+
285
285
+
⚠️ **WARNING:** This approach introduces centralization trade-offs. Only use if you have specific X.509 compliance requirements.
286
286
+
287
287
+
Hold services act as Certificate Authorities that issue X.509 certificates for users, enabling standard Notation verification.
288
288
+
289
289
+
**When to use:**
290
290
+
- Enterprise requires standard X.509 PKI
291
291
+
- Cannot deploy custom plugins
292
292
+
- Accept centralization trade-off for tool compatibility
293
293
+
294
294
+
**When NOT to use:**
295
295
+
- Default deployments (use plugins instead)
296
296
+
- Maximum decentralization required
297
297
+
- Don't need X.509 compliance
298
298
+
299
299
+
See [Hold-as-CA Architecture](./HOLD_AS_CA.md) for complete details and security implications.
300
300
+
301
301
+
---
302
302
+
303
303
+
## Tool Compatibility Matrix
304
304
+
305
305
+
| Tool | Discover | Verify | Integration Method | Priority | Effort |
306
306
+
|------|----------|--------|-------------------|----------|--------|
307
307
+
| **Kubernetes** | | | | | |
308
308
+
| OPA Gatekeeper | ✅ | ✅ | External provider | **HIGH** | 2-3 weeks |
309
309
+
| Ratify | ✅ | ✅ | Verifier plugin | **HIGH** | 2-3 weeks |
310
310
+
| Kyverno | ✅ | ⚠️ | External service | MEDIUM | 2 weeks |
311
311
+
| Portieris | ❌ | ❌ | N/A (deprecated) | NONE | - |
312
312
+
| **Runtime** | | | | | |
313
313
+
| Containerd 2.0 | ✅ | ✅ | Bindir plugin | **MED-HIGH** | 1-2 weeks |
314
314
+
| CRI-O | ⚠️ | ⚠️ | Upstream contribution | MEDIUM | 3-4 weeks |
315
315
+
| Podman | ⚠️ | ⚠️ | Upstream contribution | MEDIUM | 3-4 weeks |
316
316
+
| **CI/CD** | | | | | |
317
317
+
| GitHub Actions | ✅ | ✅ | Custom action | **HIGH** | 1 week |
318
318
+
| GitLab CI | ✅ | ✅ | Container image | **HIGH** | 1 week |
319
319
+
| Jenkins/CircleCI | ✅ | ✅ | Container image | HIGH | 1 week |
320
320
+
| **Scanners** | | | | | |
321
321
+
| Trivy | ✅ | ❌ | N/A (not verifier) | NONE | - |
322
322
+
| Snyk | ❌ | ❌ | N/A (not verifier) | NONE | - |
323
323
+
| Anchore | ❌ | ❌ | N/A (not verifier) | NONE | - |
324
324
+
| **Registries** | | | | | |
325
325
+
| Harbor | ✅ | ⚠️ | UI integration | LOW | - |
326
326
+
| **OCI Tools** | | | | | |
327
327
+
| ORAS CLI | ✅ | ❌ | Already works | Document | - |
328
328
+
| Notation | ⚠️ | ⚠️ | Hold-as-CA | OPTIONAL | 3-4 weeks |
329
329
+
| Cosign | ❌ | ❌ | Not compatible | NONE | - |
330
330
+
| Crane | ✅ | ❌ | Already works | Document | - |
331
331
+
| Skopeo | ⚠️ | ⚠️ | Upstream contribution | LOW | 3-4 weeks |
332
332
+
333
333
+
**Legend:**
334
334
+
- ✅ Works / Feasible
335
335
+
- ⚠️ Partial / Requires changes
336
336
+
- ❌ Not applicable / Not feasible
337
337
+
338
338
+
---
339
339
+
340
340
+
## Implementation Roadmap
341
341
+
342
342
+
### Phase 1: Foundation (4-5 weeks) ⭐
343
343
+
344
344
+
**Goal:** Core verification capability
345
345
+
346
346
+
1. **atcr-verify CLI tool** (Week 1-2)
347
347
+
- ATProto signature verification
348
348
+
- Trust policy support
349
349
+
- Multiple output formats
350
350
+
- Offline mode
351
351
+
352
352
+
2. **OCI Referrers API** (Week 2-3)
353
353
+
- AppView endpoint implementation
354
354
+
- ORAS artifact serving
355
355
+
- Integration with existing SBOM pattern
356
356
+
357
357
+
3. **CI/CD Container Image** (Week 3)
358
358
+
- Universal verification image
359
359
+
- Documentation for GitHub Actions, GitLab CI
360
360
+
- Example workflows
361
361
+
362
362
+
4. **Documentation** (Week 4-5)
363
363
+
- Integration guides
364
364
+
- Trust policy examples
365
365
+
- Troubleshooting guides
366
366
+
367
367
+
**Deliverables:**
368
368
+
- `atcr-verify` binary (Linux, macOS, Windows)
369
369
+
- `atcr.io/atcr/verify:latest` container image
370
370
+
- OCI Referrers API implementation
371
371
+
- Complete documentation
372
372
+
373
373
+
---
374
374
+
375
375
+
### Phase 2: Kubernetes Integration (3-4 weeks)
376
376
+
377
377
+
**Goal:** Production-ready Kubernetes admission control
378
378
+
379
379
+
5. **OPA Gatekeeper Provider** (Week 1-2)
380
380
+
- External data provider service
381
381
+
- Helm chart for deployment
382
382
+
- Example policies
383
383
+
384
384
+
6. **Ratify Plugin** (Week 2-3)
385
385
+
- Verifier plugin implementation
386
386
+
- Testing with Ratify
387
387
+
- Documentation
388
388
+
389
389
+
7. **Kubernetes Examples** (Week 4)
390
390
+
- Deployment manifests
391
391
+
- Policy examples
392
392
+
- Integration testing
393
393
+
394
394
+
**Deliverables:**
395
395
+
- `atcr-gatekeeper-provider` service
396
396
+
- Ratify plugin binary
397
397
+
- Kubernetes deployment examples
398
398
+
- Production deployment guide
399
399
+
400
400
+
---
401
401
+
402
402
+
### Phase 3: Runtime Verification (2-3 weeks)
403
403
+
404
404
+
**Goal:** Pull-time verification
405
405
+
406
406
+
8. **Containerd Plugin** (Week 1-2)
407
407
+
- Bindir verifier implementation
408
408
+
- Configuration documentation
409
409
+
- Testing with Docker, nerdctl
410
410
+
411
411
+
9. **CRI-O/Podman Integration** (Week 3, optional)
412
412
+
- Upstream contribution (if accepted)
413
413
+
- Policy.json extension
414
414
+
- Documentation
415
415
+
416
416
+
**Deliverables:**
417
417
+
- Containerd verifier binary
418
418
+
- Configuration guides
419
419
+
- Runtime verification examples
420
420
+
421
421
+
---
422
422
+
423
423
+
### Phase 4: Optional Features (2-3 weeks)
424
424
+
425
425
+
**Goal:** Enterprise features (if demanded)
426
426
+
427
427
+
10. **Hold-as-CA** (Week 1-2, optional)
428
428
+
- Certificate generation
429
429
+
- Notation signature creation
430
430
+
- Trust store distribution
431
431
+
- **Only if enterprise customers request**
432
432
+
433
433
+
11. **Advanced Features** (Week 3, as needed)
434
434
+
- Signature transparency log
435
435
+
- Multi-signature support
436
436
+
- Hardware token integration
437
437
+
438
438
+
**Deliverables:**
439
439
+
- Hold co-signing implementation (if needed)
440
440
+
- Advanced feature documentation
441
441
+
442
442
+
---
443
443
+
444
444
+
## Decision Matrix
445
445
+
446
446
+
### Which Integration Approach Should I Use?
447
447
+
448
448
+
```
449
449
+
┌─────────────────────────────────────────────────┐
450
450
+
│ Are you using Kubernetes? │
451
451
+
└───────────────┬─────────────────────────────────┘
452
452
+
│
453
453
+
┌────────┴────────┐
454
454
+
│ │
455
455
+
YES NO
456
456
+
│ │
457
457
+
↓ ↓
458
458
+
┌──────────────┐ ┌──────────────┐
459
459
+
│ Using │ │ CI/CD │
460
460
+
│ Gatekeeper? │ │ Pipeline? │
461
461
+
└──────┬───────┘ └──────┬───────┘
462
462
+
│ │
463
463
+
┌────┴────┐ ┌────┴────┐
464
464
+
YES NO YES NO
465
465
+
│ │ │ │
466
466
+
↓ ↓ ↓ ↓
467
467
+
External Ratify GitHub Universal
468
468
+
Provider Plugin Action CLI Tool
469
469
+
```
470
470
+
471
471
+
#### Use OPA Gatekeeper Provider if:
472
472
+
- ✅ Already using Gatekeeper
473
473
+
- ✅ Want Rego-based policies
474
474
+
- ✅ Need flexible policy logic
475
475
+
476
476
+
#### Use Ratify Plugin if:
477
477
+
- ✅ Using Ratify (or planning to)
478
478
+
- ✅ Want standard plugin interface
479
479
+
- ✅ Need multi-verifier support (Notation + Cosign + ATProto)
480
480
+
481
481
+
#### Use atcr-verify CLI if:
482
482
+
- ✅ CI/CD pipelines
483
483
+
- ✅ Local development
484
484
+
- ✅ Non-Kubernetes environments
485
485
+
- ✅ Want simple integration
486
486
+
487
487
+
#### Use Containerd Plugin if:
488
488
+
- ✅ Need runtime enforcement
489
489
+
- ✅ Want pull-time verification
490
490
+
- ✅ Using Containerd 2.0+
491
491
+
492
492
+
#### Use Hold-as-CA if:
493
493
+
- ⚠️ Enterprise X.509 PKI compliance required
494
494
+
- ⚠️ Cannot deploy plugins
495
495
+
- ⚠️ Accept centralization trade-off
496
496
+
497
497
+
---
498
498
+
499
499
+
## Best Practices
500
500
+
501
501
+
### 1. Start Simple
502
502
+
503
503
+
Begin with CLI tool integration in CI/CD:
504
504
+
```bash
505
505
+
# Add to .github/workflows/deploy.yml
506
506
+
- run: atcr-verify $IMAGE --policy .github/trust-policy.yaml
507
507
+
```
508
508
+
509
509
+
### 2. Define Trust Policies
510
510
+
511
511
+
Create trust policies early:
512
512
+
```yaml
513
513
+
# trust-policy.yaml
514
514
+
policies:
515
515
+
- name: production
516
516
+
scope: "atcr.io/*/prod-*"
517
517
+
require:
518
518
+
signature: true
519
519
+
trustedDIDs: [did:plc:devops-team]
520
520
+
action: enforce
521
521
+
```
522
522
+
523
523
+
### 3. Progressive Rollout
524
524
+
525
525
+
1. **Week 1:** Add verification to CI/CD (audit mode)
526
526
+
2. **Week 2:** Enforce in CI/CD
527
527
+
3. **Week 3:** Add Kubernetes admission control (audit mode)
528
528
+
4. **Week 4:** Enforce in Kubernetes
529
529
+
530
530
+
### 4. Monitor and Alert
531
531
+
532
532
+
Track verification metrics:
533
533
+
- Verification success/failure rates
534
534
+
- Policy violations
535
535
+
- Signature coverage (% of images signed)
536
536
+
537
537
+
### 5. Plan for Key Rotation
538
538
+
539
539
+
- Document DID key rotation procedures
540
540
+
- Test key rotation in non-production
541
541
+
- Monitor for unexpected key changes
542
542
+
543
543
+
---
544
544
+
545
545
+
## Common Patterns
546
546
+
547
547
+
### Pattern 1: Multi-Layer Defense
548
548
+
549
549
+
```
550
550
+
1. CI/CD verification (atcr-verify)
551
551
+
↓ (blocks unsigned images from being pushed)
552
552
+
2. Kubernetes admission (Gatekeeper/Ratify)
553
553
+
↓ (blocks unsigned images from running)
554
554
+
3. Runtime verification (Containerd plugin)
555
555
+
↓ (blocks unsigned images from being pulled)
556
556
+
```
557
557
+
558
558
+
### Pattern 2: Trust Policy Inheritance
559
559
+
560
560
+
```yaml
561
561
+
# Global policy
562
562
+
trustedDIDs:
563
563
+
- did:plc:security-team # Always trusted
564
564
+
565
565
+
# Environment-specific policies
566
566
+
staging:
567
567
+
trustedDIDs:
568
568
+
- did:plc:developers # Additional trust for staging
569
569
+
570
570
+
production:
571
571
+
trustedDIDs: [] # Only global trust (security-team)
572
572
+
```
573
573
+
574
574
+
### Pattern 3: Offline Verification
575
575
+
576
576
+
```bash
577
577
+
# Build environment (online)
578
578
+
atcr-verify export $IMAGE -o bundle.json
579
579
+
580
580
+
# Air-gapped environment (offline)
581
581
+
atcr-verify $IMAGE --offline --bundle bundle.json
582
582
+
```
583
583
+
584
584
+
---
585
585
+
586
586
+
## Migration Guide
587
587
+
588
588
+
### From Docker Content Trust (DCT)
589
589
+
590
590
+
DCT is deprecated. Migrate to ATCR signatures:
591
591
+
592
592
+
**Old (DCT):**
593
593
+
```bash
594
594
+
export DOCKER_CONTENT_TRUST=1
595
595
+
docker push myimage:latest
596
596
+
```
597
597
+
598
598
+
**New (ATCR):**
599
599
+
```bash
600
600
+
# Signatures created automatically on push
601
601
+
docker push atcr.io/myorg/myimage:latest
602
602
+
603
603
+
# Verify in CI/CD
604
604
+
atcr-verify atcr.io/myorg/myimage:latest
605
605
+
```
606
606
+
607
607
+
### From Cosign
608
608
+
609
609
+
Cosign and ATCR signatures can coexist:
610
610
+
611
611
+
**Dual signing:**
612
612
+
```bash
613
613
+
# Push to ATCR (ATProto signature automatic)
614
614
+
docker push atcr.io/myorg/myimage:latest
615
615
+
616
616
+
# Also sign with Cosign (if needed)
617
617
+
cosign sign atcr.io/myorg/myimage:latest
618
618
+
```
619
619
+
620
620
+
**Verification:**
621
621
+
```bash
622
622
+
# Verify ATProto signature
623
623
+
atcr-verify atcr.io/myorg/myimage:latest
624
624
+
625
625
+
# Or verify Cosign signature
626
626
+
cosign verify atcr.io/myorg/myimage:latest --key cosign.pub
627
627
+
```
628
628
+
629
629
+
---
630
630
+
631
631
+
## Troubleshooting
632
632
+
633
633
+
### Signatures Not Found
634
634
+
635
635
+
**Symptom:** `atcr-verify` reports "no signature found"
636
636
+
637
637
+
**Diagnosis:**
638
638
+
```bash
639
639
+
# Check if Referrers API works
640
640
+
curl "https://atcr.io/v2/OWNER/REPO/referrers/DIGEST"
641
641
+
642
642
+
# Check if signature artifact exists
643
643
+
oras discover atcr.io/OWNER/REPO:TAG
644
644
+
```
645
645
+
646
646
+
**Solutions:**
647
647
+
1. Verify Referrers API is implemented
648
648
+
2. Re-push image to generate signature
649
649
+
3. Check AppView logs for signature creation errors
650
650
+
651
651
+
### DID Resolution Fails
652
652
+
653
653
+
**Symptom:** Cannot resolve DID to public key
654
654
+
655
655
+
**Diagnosis:**
656
656
+
```bash
657
657
+
# Test DID resolution
658
658
+
curl https://plc.directory/did:plc:XXXXXX
659
659
+
660
660
+
# Check DID document has verificationMethod
661
661
+
curl https://plc.directory/did:plc:XXXXXX | jq .verificationMethod
662
662
+
```
663
663
+
664
664
+
**Solutions:**
665
665
+
1. Check internet connectivity
666
666
+
2. Verify DID is valid
667
667
+
3. Ensure DID document contains public key
668
668
+
669
669
+
### Policy Violations
670
670
+
671
671
+
**Symptom:** Verification fails with "trust policy violation"
672
672
+
673
673
+
**Diagnosis:**
674
674
+
```bash
675
675
+
# Verify with verbose output
676
676
+
atcr-verify IMAGE --policy policy.yaml --verbose
677
677
+
```
678
678
+
679
679
+
**Solutions:**
680
680
+
1. Add DID to trustedDIDs list
681
681
+
2. Check signature age vs. maxAge
682
682
+
3. Verify policy scope matches image
683
683
+
684
684
+
---
685
685
+
686
686
+
## See Also
687
687
+
688
688
+
- [ATProto Signatures](./ATPROTO_SIGNATURES.md) - Technical foundation
689
689
+
- [atcr-verify CLI](./ATCR_VERIFY_CLI.md) - CLI tool documentation
690
690
+
- [Signature Integration](./SIGNATURE_INTEGRATION.md) - Tool-specific guides
691
691
+
- [Hold-as-CA](./HOLD_AS_CA.md) - X.509 certificate approach (optional)
692
692
+
- [Examples](../examples/verification/) - Working code examples
+1210
docs/SIGNATURE_INTEGRATION.md
···
1
1
+
# Integrating ATProto Signatures with OCI Tools
2
2
+
3
3
+
This guide shows how to work with ATProto signatures using standard OCI/ORAS tools and integrate signature verification into your workflows.
4
4
+
5
5
+
## Quick Reference: Tool Compatibility
6
6
+
7
7
+
| Tool | Discover Signatures | Fetch Signatures | Verify Signatures |
8
8
+
|------|-------------------|------------------|-------------------|
9
9
+
| `oras discover` | ✅ Yes | - | - |
10
10
+
| `oras pull` | - | ✅ Yes | ❌ No (custom tool needed) |
11
11
+
| `oras manifest fetch` | - | ✅ Yes | - |
12
12
+
| `cosign tree` | ✅ Yes (as artifacts) | - | - |
13
13
+
| `cosign verify` | - | - | ❌ No (different format) |
14
14
+
| `crane manifest` | - | ✅ Yes | - |
15
15
+
| `skopeo inspect` | ✅ Yes (in referrers) | - | - |
16
16
+
| `docker` | ❌ No (not visible) | - | - |
17
17
+
| **`atcr-verify`** | ✅ Yes | ✅ Yes | ✅ Yes |
18
18
+
19
19
+
**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.
20
20
+
21
21
+
## Understanding What Tools See
22
22
+
23
23
+
### ORAS CLI: Full Support for Discovery
24
24
+
25
25
+
ORAS understands referrers and can discover ATProto signature artifacts:
26
26
+
27
27
+
```bash
28
28
+
# Discover all artifacts attached to an image
29
29
+
$ oras discover atcr.io/alice/myapp:latest
30
30
+
31
31
+
Discovered 2 artifacts referencing alice/myapp@sha256:abc123456789...:
32
32
+
Digest: sha256:abc123456789...
33
33
+
34
34
+
Artifact Type: application/spdx+json
35
35
+
Digest: sha256:sbom123...
36
36
+
Size: 45678
37
37
+
38
38
+
Artifact Type: application/vnd.atproto.signature.v1+json
39
39
+
Digest: sha256:sig789...
40
40
+
Size: 512
41
41
+
```
42
42
+
43
43
+
**What ORAS shows:**
44
44
+
- ✅ Artifact type (identifies it as an ATProto signature)
45
45
+
- ✅ Digest (can fetch the artifact)
46
46
+
- ✅ Size
47
47
+
48
48
+
**To filter for signatures only:**
49
49
+
50
50
+
```bash
51
51
+
$ oras discover atcr.io/alice/myapp:latest \
52
52
+
--artifact-type application/vnd.atproto.signature.v1+json
53
53
+
54
54
+
Discovered 1 artifact referencing alice/myapp@sha256:abc123...:
55
55
+
Artifact Type: application/vnd.atproto.signature.v1+json
56
56
+
Digest: sha256:sig789...
57
57
+
```
58
58
+
59
59
+
### Fetching Signature Metadata with ORAS
60
60
+
61
61
+
Pull the signature artifact to examine it:
62
62
+
63
63
+
```bash
64
64
+
# Pull signature artifact to current directory
65
65
+
$ oras pull atcr.io/alice/myapp@sha256:sig789...
66
66
+
67
67
+
Downloaded atproto-signature.json
68
68
+
Pulled atcr.io/alice/myapp@sha256:sig789...
69
69
+
Digest: sha256:sig789...
70
70
+
71
71
+
# Examine the signature metadata
72
72
+
$ cat atproto-signature.json | jq .
73
73
+
{
74
74
+
"$type": "io.atcr.atproto.signature",
75
75
+
"version": "1.0",
76
76
+
"subject": {
77
77
+
"digest": "sha256:abc123...",
78
78
+
"mediaType": "application/vnd.oci.image.manifest.v1+json"
79
79
+
},
80
80
+
"atproto": {
81
81
+
"did": "did:plc:alice123",
82
82
+
"handle": "alice.bsky.social",
83
83
+
"pdsEndpoint": "https://bsky.social",
84
84
+
"recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123",
85
85
+
"commitCid": "bafyreih8...",
86
86
+
"signedAt": "2025-10-31T12:34:56.789Z"
87
87
+
},
88
88
+
"signature": {
89
89
+
"algorithm": "ECDSA-K256-SHA256",
90
90
+
"keyId": "did:plc:alice123#atproto",
91
91
+
"publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z"
92
92
+
}
93
93
+
}
94
94
+
```
95
95
+
96
96
+
### Cosign: Discovers but Cannot Verify
97
97
+
98
98
+
Cosign can see ATProto signatures as artifacts but can't verify them:
99
99
+
100
100
+
```bash
101
101
+
# Cosign tree shows attached artifacts
102
102
+
$ cosign tree atcr.io/alice/myapp:latest
103
103
+
104
104
+
📦 Supply Chain Security Related artifacts for an image: atcr.io/alice/myapp:latest
105
105
+
└── 💾 Attestations for an image tag: atcr.io/alice/myapp:sha256-abc123.att
106
106
+
├── 🍒 sha256:sbom123... (application/spdx+json)
107
107
+
└── 🍒 sha256:sig789... (application/vnd.atproto.signature.v1+json)
108
108
+
109
109
+
# Cosign verify doesn't work (expected)
110
110
+
$ cosign verify atcr.io/alice/myapp:latest
111
111
+
112
112
+
Error: no matching signatures:
113
113
+
main.go:62: error during command execution: no matching signatures:
114
114
+
```
115
115
+
116
116
+
**Why cosign verify fails:**
117
117
+
- Cosign expects signatures in its own format (`dev.cosignproject.cosign/signature` annotation)
118
118
+
- ATProto signatures use a different format and trust model
119
119
+
- This is **intentional** - we're not trying to be Cosign-compatible
120
120
+
121
121
+
### Crane: Fetch Manifests
122
122
+
123
123
+
Crane can fetch the signature manifest:
124
124
+
125
125
+
```bash
126
126
+
# Get signature artifact manifest
127
127
+
$ crane manifest atcr.io/alice/myapp@sha256:sig789... | jq .
128
128
+
{
129
129
+
"schemaVersion": 2,
130
130
+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
131
131
+
"artifactType": "application/vnd.atproto.signature.v1+json",
132
132
+
"config": {
133
133
+
"mediaType": "application/vnd.oci.empty.v1+json",
134
134
+
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
135
135
+
"size": 2
136
136
+
},
137
137
+
"subject": {
138
138
+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
139
139
+
"digest": "sha256:abc123...",
140
140
+
"size": 1234
141
141
+
},
142
142
+
"layers": [{
143
143
+
"mediaType": "application/vnd.atproto.signature.v1+json",
144
144
+
"digest": "sha256:meta456...",
145
145
+
"size": 512
146
146
+
}],
147
147
+
"annotations": {
148
148
+
"io.atcr.atproto.did": "did:plc:alice123",
149
149
+
"io.atcr.atproto.pds": "https://bsky.social",
150
150
+
"io.atcr.atproto.recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123"
151
151
+
}
152
152
+
}
153
153
+
```
154
154
+
155
155
+
### Skopeo: Inspect Images
156
156
+
157
157
+
Skopeo shows referrers in image inspection:
158
158
+
159
159
+
```bash
160
160
+
$ skopeo inspect --raw docker://atcr.io/alice/myapp:latest | jq .
161
161
+
162
162
+
# Standard manifest (no signature info visible in manifest itself)
163
163
+
164
164
+
# To see referrers (if registry supports Referrers API):
165
165
+
$ curl -H "Accept: application/vnd.oci.image.index.v1+json" \
166
166
+
"https://atcr.io/v2/alice/myapp/referrers/sha256:abc123"
167
167
+
```
168
168
+
169
169
+
## Manual Verification with Shell Scripts
170
170
+
171
171
+
Until `atcr-verify` is built, you can verify signatures manually:
172
172
+
173
173
+
### Simple Verification Script
174
174
+
175
175
+
```bash
176
176
+
#!/bin/bash
177
177
+
# verify-atproto-signature.sh
178
178
+
# Usage: ./verify-atproto-signature.sh atcr.io/alice/myapp:latest
179
179
+
180
180
+
set -e
181
181
+
182
182
+
IMAGE="$1"
183
183
+
184
184
+
echo "[1/6] Resolving image digest..."
185
185
+
DIGEST=$(crane digest "$IMAGE")
186
186
+
echo " → $DIGEST"
187
187
+
188
188
+
echo "[2/6] Discovering ATProto signature..."
189
189
+
REGISTRY=$(echo "$IMAGE" | cut -d/ -f1)
190
190
+
REPO=$(echo "$IMAGE" | cut -d/ -f2-)
191
191
+
REPO_PATH=$(echo "$REPO" | cut -d: -f1)
192
192
+
193
193
+
SIG_ARTIFACTS=$(curl -s -H "Accept: application/vnd.oci.image.index.v1+json" \
194
194
+
"https://${REGISTRY}/v2/${REPO_PATH}/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json")
195
195
+
196
196
+
SIG_DIGEST=$(echo "$SIG_ARTIFACTS" | jq -r '.manifests[0].digest')
197
197
+
if [ "$SIG_DIGEST" = "null" ]; then
198
198
+
echo " ✗ No ATProto signature found"
199
199
+
exit 1
200
200
+
fi
201
201
+
echo " → Found signature: $SIG_DIGEST"
202
202
+
203
203
+
echo "[3/6] Fetching signature metadata..."
204
204
+
oras pull "${REGISTRY}/${REPO_PATH}@${SIG_DIGEST}" -o /tmp/sig --quiet
205
205
+
206
206
+
DID=$(jq -r '.atproto.did' /tmp/sig/atproto-signature.json)
207
207
+
PDS=$(jq -r '.atproto.pdsEndpoint' /tmp/sig/atproto-signature.json)
208
208
+
RECORD_URI=$(jq -r '.atproto.recordUri' /tmp/sig/atproto-signature.json)
209
209
+
echo " → DID: $DID"
210
210
+
echo " → PDS: $PDS"
211
211
+
echo " → Record: $RECORD_URI"
212
212
+
213
213
+
echo "[4/6] Resolving DID to public key..."
214
214
+
DID_DOC=$(curl -s "https://plc.directory/$DID")
215
215
+
PUB_KEY_MB=$(echo "$DID_DOC" | jq -r '.verificationMethod[0].publicKeyMultibase')
216
216
+
echo " → Public key: $PUB_KEY_MB"
217
217
+
218
218
+
echo "[5/6] Querying PDS for signed commit..."
219
219
+
# Extract collection and rkey from record URI
220
220
+
COLLECTION=$(echo "$RECORD_URI" | sed 's|at://[^/]*/\([^/]*\)/.*|\1|')
221
221
+
RKEY=$(echo "$RECORD_URI" | sed 's|at://.*/||')
222
222
+
223
223
+
RECORD=$(curl -s "${PDS}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=${COLLECTION}&rkey=${RKEY}")
224
224
+
RECORD_CID=$(echo "$RECORD" | jq -r '.cid')
225
225
+
echo " → Record CID: $RECORD_CID"
226
226
+
227
227
+
echo "[6/6] Verifying signature..."
228
228
+
echo " ⚠ Note: Full cryptographic verification requires ATProto crypto library"
229
229
+
echo " ⚠ This script verifies record existence and DID resolution only"
230
230
+
echo ""
231
231
+
echo " ✓ Record exists in PDS"
232
232
+
echo " ✓ DID resolved successfully"
233
233
+
echo " ✓ Public key retrieved"
234
234
+
echo ""
235
235
+
echo "To fully verify the cryptographic signature, use: atcr-verify $IMAGE"
236
236
+
```
237
237
+
238
238
+
### Full Verification (Requires Go + indigo)
239
239
+
240
240
+
```go
241
241
+
// verify.go - Full cryptographic verification
242
242
+
package main
243
243
+
244
244
+
import (
245
245
+
"encoding/json"
246
246
+
"fmt"
247
247
+
"net/http"
248
248
+
249
249
+
"github.com/bluesky-social/indigo/atproto/crypto"
250
250
+
"github.com/multiformats/go-multibase"
251
251
+
)
252
252
+
253
253
+
func verifyATProtoSignature(did, pds, recordURI string) error {
254
254
+
// 1. Resolve DID to public key
255
255
+
didDoc, err := fetchDIDDocument(did)
256
256
+
if err != nil {
257
257
+
return fmt.Errorf("failed to resolve DID: %w", err)
258
258
+
}
259
259
+
260
260
+
pubKeyMB := didDoc.VerificationMethod[0].PublicKeyMultibase
261
261
+
262
262
+
// 2. Decode multibase public key
263
263
+
_, pubKeyBytes, err := multibase.Decode(pubKeyMB)
264
264
+
if err != nil {
265
265
+
return fmt.Errorf("failed to decode public key: %w", err)
266
266
+
}
267
267
+
268
268
+
// Remove multicodec prefix (first 2 bytes for K-256)
269
269
+
pubKeyBytes = pubKeyBytes[2:]
270
270
+
271
271
+
// 3. Parse as K-256 public key
272
272
+
pubKey, err := crypto.ParsePublicKeyK256(pubKeyBytes)
273
273
+
if err != nil {
274
274
+
return fmt.Errorf("failed to parse public key: %w", err)
275
275
+
}
276
276
+
277
277
+
// 4. Fetch repository commit from PDS
278
278
+
commit, err := fetchRepoCommit(pds, did)
279
279
+
if err != nil {
280
280
+
return fmt.Errorf("failed to fetch commit: %w", err)
281
281
+
}
282
282
+
283
283
+
// 5. Verify signature
284
284
+
bytesToVerify := commit.Unsigned().BytesForSigning()
285
285
+
err = pubKey.Verify(bytesToVerify, commit.Sig)
286
286
+
if err != nil {
287
287
+
return fmt.Errorf("signature verification failed: %w", err)
288
288
+
}
289
289
+
290
290
+
fmt.Println("✓ Signature verified successfully!")
291
291
+
return nil
292
292
+
}
293
293
+
294
294
+
func fetchDIDDocument(did string) (*DIDDocument, error) {
295
295
+
resp, err := http.Get(fmt.Sprintf("https://plc.directory/%s", did))
296
296
+
if err != nil {
297
297
+
return nil, err
298
298
+
}
299
299
+
defer resp.Body.Close()
300
300
+
301
301
+
var didDoc DIDDocument
302
302
+
err = json.NewDecoder(resp.Body).Decode(&didDoc)
303
303
+
return &didDoc, err
304
304
+
}
305
305
+
306
306
+
// ... additional helper functions
307
307
+
```
308
308
+
309
309
+
## Kubernetes Integration
310
310
+
311
311
+
### Option 1: Admission Webhook (Recommended)
312
312
+
313
313
+
Create a validating webhook that verifies ATProto signatures:
314
314
+
315
315
+
```yaml
316
316
+
# atcr-verify-webhook.yaml
317
317
+
apiVersion: v1
318
318
+
kind: Service
319
319
+
metadata:
320
320
+
name: atcr-verify
321
321
+
namespace: kube-system
322
322
+
spec:
323
323
+
selector:
324
324
+
app: atcr-verify
325
325
+
ports:
326
326
+
- port: 443
327
327
+
targetPort: 8443
328
328
+
---
329
329
+
apiVersion: apps/v1
330
330
+
kind: Deployment
331
331
+
metadata:
332
332
+
name: atcr-verify
333
333
+
namespace: kube-system
334
334
+
spec:
335
335
+
replicas: 2
336
336
+
selector:
337
337
+
matchLabels:
338
338
+
app: atcr-verify
339
339
+
template:
340
340
+
metadata:
341
341
+
labels:
342
342
+
app: atcr-verify
343
343
+
spec:
344
344
+
containers:
345
345
+
- name: webhook
346
346
+
image: atcr.io/atcr/verify-webhook:latest
347
347
+
ports:
348
348
+
- containerPort: 8443
349
349
+
env:
350
350
+
- name: REQUIRE_SIGNATURE
351
351
+
value: "true"
352
352
+
- name: TRUSTED_DIDS
353
353
+
value: "did:plc:alice123,did:plc:bob456"
354
354
+
---
355
355
+
apiVersion: admissionregistration.k8s.io/v1
356
356
+
kind: ValidatingWebhookConfiguration
357
357
+
metadata:
358
358
+
name: atcr-verify
359
359
+
webhooks:
360
360
+
- name: verify.atcr.io
361
361
+
clientConfig:
362
362
+
service:
363
363
+
name: atcr-verify
364
364
+
namespace: kube-system
365
365
+
path: /validate
366
366
+
caBundle: <base64-encoded-ca-cert>
367
367
+
rules:
368
368
+
- operations: ["CREATE", "UPDATE"]
369
369
+
apiGroups: [""]
370
370
+
apiVersions: ["v1"]
371
371
+
resources: ["pods"]
372
372
+
admissionReviewVersions: ["v1", "v1beta1"]
373
373
+
sideEffects: None
374
374
+
failurePolicy: Fail # Reject pods if verification fails
375
375
+
namespaceSelector:
376
376
+
matchExpressions:
377
377
+
- key: atcr-verify
378
378
+
operator: In
379
379
+
values: ["enabled"]
380
380
+
```
381
381
+
382
382
+
**Webhook Server Logic** (pseudocode):
383
383
+
384
384
+
```go
385
385
+
func (h *WebhookHandler) ValidatePod(req *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
386
386
+
pod := &corev1.Pod{}
387
387
+
json.Unmarshal(req.Object.Raw, pod)
388
388
+
389
389
+
for _, container := range pod.Spec.Containers {
390
390
+
if !strings.HasPrefix(container.Image, "atcr.io/") {
391
391
+
continue // Only verify ATCR images
392
392
+
}
393
393
+
394
394
+
// Verify ATProto signature
395
395
+
err := verifyImageSignature(container.Image)
396
396
+
if err != nil {
397
397
+
return &admissionv1.AdmissionResponse{
398
398
+
Allowed: false,
399
399
+
Result: &metav1.Status{
400
400
+
Message: fmt.Sprintf("Image %s failed ATProto verification: %v",
401
401
+
container.Image, err),
402
402
+
},
403
403
+
}
404
404
+
}
405
405
+
}
406
406
+
407
407
+
return &admissionv1.AdmissionResponse{Allowed: true}
408
408
+
}
409
409
+
```
410
410
+
411
411
+
**Enable verification for specific namespaces:**
412
412
+
413
413
+
```bash
414
414
+
# Label namespace to enable verification
415
415
+
kubectl label namespace production atcr-verify=enabled
416
416
+
417
417
+
# Pods in this namespace must have valid ATProto signatures
418
418
+
kubectl apply -f pod.yaml -n production
419
419
+
```
420
420
+
421
421
+
### Option 2: Kyverno Policy
422
422
+
423
423
+
Use Kyverno for policy-based validation:
424
424
+
425
425
+
```yaml
426
426
+
# kyverno-atcr-policy.yaml
427
427
+
apiVersion: kyverno.io/v1
428
428
+
kind: ClusterPolicy
429
429
+
metadata:
430
430
+
name: verify-atcr-signatures
431
431
+
spec:
432
432
+
validationFailureAction: enforce
433
433
+
background: false
434
434
+
rules:
435
435
+
- name: atcr-images-must-be-signed
436
436
+
match:
437
437
+
any:
438
438
+
- resources:
439
439
+
kinds:
440
440
+
- Pod
441
441
+
validate:
442
442
+
message: "ATCR images must have valid ATProto signatures"
443
443
+
foreach:
444
444
+
- list: "request.object.spec.containers"
445
445
+
deny:
446
446
+
conditions:
447
447
+
all:
448
448
+
- key: "{{ element.image }}"
449
449
+
operator: In
450
450
+
value: "atcr.io/*"
451
451
+
- key: "{{ atcrVerifySignature(element.image) }}"
452
452
+
operator: NotEquals
453
453
+
value: true
454
454
+
```
455
455
+
456
456
+
**Note:** Requires custom Kyverno extension for `atcrVerifySignature()` function or external service integration.
457
457
+
458
458
+
### Option 3: Ratify Verifier Plugin (Recommended) ⭐
459
459
+
460
460
+
Ratify is a verification engine that integrates with OPA Gatekeeper. Build a custom verifier plugin for ATProto signatures:
461
461
+
462
462
+
**Ratify Plugin Architecture:**
463
463
+
```go
464
464
+
// pkg/verifier/atproto/verifier.go
465
465
+
package atproto
466
466
+
467
467
+
import (
468
468
+
"context"
469
469
+
"encoding/json"
470
470
+
471
471
+
"github.com/ratify-project/ratify/pkg/common"
472
472
+
"github.com/ratify-project/ratify/pkg/ocispecs"
473
473
+
"github.com/ratify-project/ratify/pkg/referrerstore"
474
474
+
"github.com/ratify-project/ratify/pkg/verifier"
475
475
+
)
476
476
+
477
477
+
type ATProtoVerifier struct {
478
478
+
name string
479
479
+
config ATProtoConfig
480
480
+
resolver *Resolver
481
481
+
}
482
482
+
483
483
+
type ATProtoConfig struct {
484
484
+
TrustedDIDs []string `json:"trustedDIDs"`
485
485
+
}
486
486
+
487
487
+
func (v *ATProtoVerifier) Name() string {
488
488
+
return v.name
489
489
+
}
490
490
+
491
491
+
func (v *ATProtoVerifier) Type() string {
492
492
+
return "atproto"
493
493
+
}
494
494
+
495
495
+
func (v *ATProtoVerifier) CanVerify(artifactType string) bool {
496
496
+
return artifactType == "application/vnd.atproto.signature.v1+json"
497
497
+
}
498
498
+
499
499
+
func (v *ATProtoVerifier) VerifyReference(
500
500
+
ctx context.Context,
501
501
+
subjectRef common.Reference,
502
502
+
referenceDesc ocispecs.ReferenceDescriptor,
503
503
+
store referrerstore.ReferrerStore,
504
504
+
) (verifier.VerifierResult, error) {
505
505
+
// 1. Fetch signature blob from store
506
506
+
sigBlob, err := store.GetBlobContent(ctx, subjectRef, referenceDesc.Digest)
507
507
+
if err != nil {
508
508
+
return verifier.VerifierResult{IsSuccess: false}, err
509
509
+
}
510
510
+
511
511
+
// 2. Parse ATProto signature metadata
512
512
+
var sigData ATProtoSignature
513
513
+
if err := json.Unmarshal(sigBlob, &sigData); err != nil {
514
514
+
return verifier.VerifierResult{IsSuccess: false}, err
515
515
+
}
516
516
+
517
517
+
// 3. Resolve DID to public key
518
518
+
pubKey, err := v.resolver.ResolveDIDToPublicKey(ctx, sigData.ATProto.DID)
519
519
+
if err != nil {
520
520
+
return verifier.VerifierResult{IsSuccess: false}, err
521
521
+
}
522
522
+
523
523
+
// 4. Fetch repository commit from PDS
524
524
+
commit, err := v.resolver.FetchCommit(ctx, sigData.ATProto.PDSEndpoint,
525
525
+
sigData.ATProto.DID, sigData.ATProto.CommitCID)
526
526
+
if err != nil {
527
527
+
return verifier.VerifierResult{IsSuccess: false}, err
528
528
+
}
529
529
+
530
530
+
// 5. Verify K-256 signature
531
531
+
valid := verifyK256Signature(pubKey, commit.Unsigned(), commit.Sig)
532
532
+
if !valid {
533
533
+
return verifier.VerifierResult{IsSuccess: false},
534
534
+
fmt.Errorf("signature verification failed")
535
535
+
}
536
536
+
537
537
+
// 6. Check trust policy
538
538
+
if !v.isTrusted(sigData.ATProto.DID) {
539
539
+
return verifier.VerifierResult{IsSuccess: false},
540
540
+
fmt.Errorf("DID %s not in trusted list", sigData.ATProto.DID)
541
541
+
}
542
542
+
543
543
+
return verifier.VerifierResult{
544
544
+
IsSuccess: true,
545
545
+
Name: v.name,
546
546
+
Type: v.Type(),
547
547
+
Message: fmt.Sprintf("Verified for DID %s", sigData.ATProto.DID),
548
548
+
Extensions: map[string]interface{}{
549
549
+
"did": sigData.ATProto.DID,
550
550
+
"handle": sigData.ATProto.Handle,
551
551
+
"signedAt": sigData.ATProto.SignedAt,
552
552
+
"commitCid": sigData.ATProto.CommitCID,
553
553
+
},
554
554
+
}, nil
555
555
+
}
556
556
+
557
557
+
func (v *ATProtoVerifier) isTrusted(did string) bool {
558
558
+
for _, trustedDID := range v.config.TrustedDIDs {
559
559
+
if did == trustedDID {
560
560
+
return true
561
561
+
}
562
562
+
}
563
563
+
return false
564
564
+
}
565
565
+
```
566
566
+
567
567
+
**Deploy Ratify with ATProto Plugin:**
568
568
+
569
569
+
1. **Build plugin:**
570
570
+
```bash
571
571
+
CGO_ENABLED=0 go build -o atproto-verifier ./cmd/ratify-atproto-plugin
572
572
+
```
573
573
+
574
574
+
2. **Create custom Ratify image:**
575
575
+
```dockerfile
576
576
+
FROM ghcr.io/ratify-project/ratify:latest
577
577
+
COPY atproto-verifier /.ratify/plugins/atproto-verifier
578
578
+
```
579
579
+
580
580
+
3. **Deploy Ratify:**
581
581
+
```yaml
582
582
+
apiVersion: apps/v1
583
583
+
kind: Deployment
584
584
+
metadata:
585
585
+
name: ratify
586
586
+
namespace: gatekeeper-system
587
587
+
spec:
588
588
+
replicas: 1
589
589
+
selector:
590
590
+
matchLabels:
591
591
+
app: ratify
592
592
+
template:
593
593
+
metadata:
594
594
+
labels:
595
595
+
app: ratify
596
596
+
spec:
597
597
+
containers:
598
598
+
- name: ratify
599
599
+
image: atcr.io/atcr/ratify-with-atproto:latest
600
600
+
args:
601
601
+
- serve
602
602
+
- --config=/config/ratify-config.yaml
603
603
+
volumeMounts:
604
604
+
- name: config
605
605
+
mountPath: /config
606
606
+
volumes:
607
607
+
- name: config
608
608
+
configMap:
609
609
+
name: ratify-config
610
610
+
```
611
611
+
612
612
+
4. **Configure Verifier:**
613
613
+
```yaml
614
614
+
apiVersion: config.ratify.deislabs.io/v1beta1
615
615
+
kind: Verifier
616
616
+
metadata:
617
617
+
name: atproto-verifier
618
618
+
spec:
619
619
+
name: atproto
620
620
+
artifactType: application/vnd.atproto.signature.v1+json
621
621
+
address: /.ratify/plugins/atproto-verifier
622
622
+
parameters:
623
623
+
trustedDIDs:
624
624
+
- did:plc:alice123
625
625
+
- did:plc:bob456
626
626
+
```
627
627
+
628
628
+
5. **Use with Gatekeeper:**
629
629
+
```yaml
630
630
+
apiVersion: constraints.gatekeeper.sh/v1beta1
631
631
+
kind: RatifyVerification
632
632
+
metadata:
633
633
+
name: atcr-signatures-required
634
634
+
spec:
635
635
+
enforcementAction: deny
636
636
+
match:
637
637
+
kinds:
638
638
+
- apiGroups: [""]
639
639
+
kinds: ["Pod"]
640
640
+
```
641
641
+
642
642
+
**Benefits:**
643
643
+
- ✅ Standard plugin interface
644
644
+
- ✅ Works with existing Ratify deployments
645
645
+
- ✅ Can combine with other verifiers (Notation, Cosign)
646
646
+
- ✅ Policy-based enforcement via Gatekeeper
647
647
+
648
648
+
**See Also:** [Integration Strategy - Ratify Plugin](./INTEGRATION_STRATEGY.md#ratify-verifier-plugin)
649
649
+
650
650
+
---
651
651
+
652
652
+
### Option 4: OPA Gatekeeper External Data Provider ⭐
653
653
+
654
654
+
Use Gatekeeper's External Data Provider feature to verify ATProto signatures:
655
655
+
656
656
+
**Provider Service:**
657
657
+
```go
658
658
+
// cmd/gatekeeper-provider/main.go
659
659
+
package main
660
660
+
661
661
+
import (
662
662
+
"context"
663
663
+
"encoding/json"
664
664
+
"net/http"
665
665
+
666
666
+
"github.com/atcr-io/atcr/pkg/verify"
667
667
+
)
668
668
+
669
669
+
type ProviderRequest struct {
670
670
+
Keys []string `json:"keys"`
671
671
+
Values []string `json:"values"`
672
672
+
}
673
673
+
674
674
+
type ProviderResponse struct {
675
675
+
SystemError string `json:"system_error,omitempty"`
676
676
+
Responses []map[string]interface{} `json:"responses"`
677
677
+
}
678
678
+
679
679
+
func handleProvide(w http.ResponseWriter, r *http.Request) {
680
680
+
var req ProviderRequest
681
681
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
682
682
+
http.Error(w, err.Error(), http.StatusBadRequest)
683
683
+
return
684
684
+
}
685
685
+
686
686
+
// Verify each image
687
687
+
responses := make([]map[string]interface{}, 0, len(req.Values))
688
688
+
for _, image := range req.Values {
689
689
+
result, err := verifier.Verify(context.Background(), image)
690
690
+
691
691
+
response := map[string]interface{}{
692
692
+
"image": image,
693
693
+
"verified": false,
694
694
+
}
695
695
+
696
696
+
if err == nil && result.Verified {
697
697
+
response["verified"] = true
698
698
+
response["did"] = result.Signature.DID
699
699
+
response["signedAt"] = result.Signature.SignedAt
700
700
+
}
701
701
+
702
702
+
responses = append(responses, response)
703
703
+
}
704
704
+
705
705
+
w.Header().Set("Content-Type", "application/json")
706
706
+
json.NewEncoder(w).Encode(ProviderResponse{
707
707
+
Responses: responses,
708
708
+
})
709
709
+
}
710
710
+
711
711
+
func main() {
712
712
+
http.HandleFunc("/provide", handleProvide)
713
713
+
http.ListenAndServe(":8080", nil)
714
714
+
}
715
715
+
```
716
716
+
717
717
+
**Deploy Provider:**
718
718
+
```yaml
719
719
+
apiVersion: apps/v1
720
720
+
kind: Deployment
721
721
+
metadata:
722
722
+
name: atcr-provider
723
723
+
namespace: gatekeeper-system
724
724
+
spec:
725
725
+
replicas: 2
726
726
+
selector:
727
727
+
matchLabels:
728
728
+
app: atcr-provider
729
729
+
template:
730
730
+
metadata:
731
731
+
labels:
732
732
+
app: atcr-provider
733
733
+
spec:
734
734
+
containers:
735
735
+
- name: provider
736
736
+
image: atcr.io/atcr/gatekeeper-provider:latest
737
737
+
ports:
738
738
+
- containerPort: 8080
739
739
+
env:
740
740
+
- name: ATCR_POLICY_FILE
741
741
+
value: /config/trust-policy.yaml
742
742
+
volumeMounts:
743
743
+
- name: config
744
744
+
mountPath: /config
745
745
+
volumes:
746
746
+
- name: config
747
747
+
configMap:
748
748
+
name: atcr-trust-policy
749
749
+
---
750
750
+
apiVersion: v1
751
751
+
kind: Service
752
752
+
metadata:
753
753
+
name: atcr-provider
754
754
+
namespace: gatekeeper-system
755
755
+
spec:
756
756
+
selector:
757
757
+
app: atcr-provider
758
758
+
ports:
759
759
+
- port: 80
760
760
+
targetPort: 8080
761
761
+
```
762
762
+
763
763
+
**Configure Gatekeeper:**
764
764
+
```yaml
765
765
+
apiVersion: config.gatekeeper.sh/v1alpha1
766
766
+
kind: Config
767
767
+
metadata:
768
768
+
name: config
769
769
+
namespace: gatekeeper-system
770
770
+
spec:
771
771
+
sync:
772
772
+
syncOnly:
773
773
+
- group: ""
774
774
+
version: "v1"
775
775
+
kind: "Pod"
776
776
+
validation:
777
777
+
traces:
778
778
+
- user: "gatekeeper"
779
779
+
dump: "All"
780
780
+
---
781
781
+
apiVersion: externaldata.gatekeeper.sh/v1alpha1
782
782
+
kind: Provider
783
783
+
metadata:
784
784
+
name: atcr-verifier
785
785
+
spec:
786
786
+
url: http://atcr-provider.gatekeeper-system/provide
787
787
+
timeout: 10
788
788
+
```
789
789
+
790
790
+
**Policy (Rego):**
791
791
+
```rego
792
792
+
package verify
793
793
+
794
794
+
import future.keywords.contains
795
795
+
import future.keywords.if
796
796
+
import future.keywords.in
797
797
+
798
798
+
# External data call
799
799
+
provider := "atcr-verifier"
800
800
+
801
801
+
violation[{"msg": msg}] {
802
802
+
container := input.review.object.spec.containers[_]
803
803
+
startswith(container.image, "atcr.io/")
804
804
+
805
805
+
# Call external provider
806
806
+
response := external_data({
807
807
+
"provider": provider,
808
808
+
"keys": ["image"],
809
809
+
"values": [container.image]
810
810
+
})
811
811
+
812
812
+
# Check verification result
813
813
+
not response[_].verified == true
814
814
+
815
815
+
msg := sprintf("Image %v has no valid ATProto signature", [container.image])
816
816
+
}
817
817
+
```
818
818
+
819
819
+
**Benefits:**
820
820
+
- ✅ Uses standard Gatekeeper external data API
821
821
+
- ✅ Flexible Rego policies
822
822
+
- ✅ Can add caching, rate limiting
823
823
+
- ✅ Easy to deploy and update
824
824
+
825
825
+
**See Also:** [Integration Strategy - Gatekeeper Provider](./INTEGRATION_STRATEGY.md#opa-gatekeeper-external-provider)
826
826
+
827
827
+
---
828
828
+
829
829
+
### Option 5: OPA Gatekeeper
830
830
+
831
831
+
Use OPA for policy enforcement:
832
832
+
833
833
+
```yaml
834
834
+
# gatekeeper-constraint-template.yaml
835
835
+
apiVersion: templates.gatekeeper.sh/v1
836
836
+
kind: ConstraintTemplate
837
837
+
metadata:
838
838
+
name: atcrverify
839
839
+
spec:
840
840
+
crd:
841
841
+
spec:
842
842
+
names:
843
843
+
kind: ATCRVerify
844
844
+
targets:
845
845
+
- target: admission.k8s.gatekeeper.sh
846
846
+
rego: |
847
847
+
package atcrverify
848
848
+
849
849
+
violation[{"msg": msg}] {
850
850
+
container := input.review.object.spec.containers[_]
851
851
+
startswith(container.image, "atcr.io/")
852
852
+
not verified(container.image)
853
853
+
msg := sprintf("Image %v has no valid ATProto signature", [container.image])
854
854
+
}
855
855
+
856
856
+
verified(image) {
857
857
+
# Call external verification service
858
858
+
response := http.send({
859
859
+
"method": "GET",
860
860
+
"url": sprintf("http://atcr-verify.kube-system.svc/verify?image=%v", [image]),
861
861
+
})
862
862
+
response.status_code == 200
863
863
+
}
864
864
+
---
865
865
+
apiVersion: constraints.gatekeeper.sh/v1beta1
866
866
+
kind: ATCRVerify
867
867
+
metadata:
868
868
+
name: atcr-signatures-required
869
869
+
spec:
870
870
+
match:
871
871
+
kinds:
872
872
+
- apiGroups: [""]
873
873
+
kinds: ["Pod"]
874
874
+
```
875
875
+
876
876
+
## CI/CD Integration
877
877
+
878
878
+
### GitHub Actions
879
879
+
880
880
+
```yaml
881
881
+
# .github/workflows/verify-and-deploy.yml
882
882
+
name: Verify and Deploy
883
883
+
884
884
+
on:
885
885
+
push:
886
886
+
branches: [main]
887
887
+
888
888
+
jobs:
889
889
+
verify-image:
890
890
+
runs-on: ubuntu-latest
891
891
+
steps:
892
892
+
- name: Install ORAS
893
893
+
run: |
894
894
+
curl -LO https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz
895
895
+
tar -xzf oras_1.0.0_linux_amd64.tar.gz
896
896
+
sudo mv oras /usr/local/bin/
897
897
+
898
898
+
- name: Install crane
899
899
+
run: |
900
900
+
curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.15.2/go-containerregistry_Linux_x86_64.tar.gz" > crane.tar.gz
901
901
+
tar -xzf crane.tar.gz
902
902
+
sudo mv crane /usr/local/bin/
903
903
+
904
904
+
- name: Verify image signature
905
905
+
run: |
906
906
+
IMAGE="atcr.io/alice/myapp:${{ github.sha }}"
907
907
+
908
908
+
# Get image digest
909
909
+
DIGEST=$(crane digest "$IMAGE")
910
910
+
911
911
+
# Check for ATProto signature
912
912
+
REFERRERS=$(curl -s "https://atcr.io/v2/alice/myapp/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json")
913
913
+
914
914
+
SIG_COUNT=$(echo "$REFERRERS" | jq '.manifests | length')
915
915
+
if [ "$SIG_COUNT" -eq 0 ]; then
916
916
+
echo "❌ No ATProto signature found"
917
917
+
exit 1
918
918
+
fi
919
919
+
920
920
+
echo "✓ Found $SIG_COUNT signature(s)"
921
921
+
922
922
+
# TODO: Full verification when atcr-verify is available
923
923
+
# atcr-verify "$IMAGE" --policy policy.yaml
924
924
+
925
925
+
- name: Deploy to Kubernetes
926
926
+
if: success()
927
927
+
run: |
928
928
+
kubectl set image deployment/myapp myapp=atcr.io/alice/myapp:${{ github.sha }}
929
929
+
```
930
930
+
931
931
+
### GitLab CI
932
932
+
933
933
+
```yaml
934
934
+
# .gitlab-ci.yml
935
935
+
verify_image:
936
936
+
stage: verify
937
937
+
image: alpine:latest
938
938
+
before_script:
939
939
+
- apk add --no-cache curl jq
940
940
+
script:
941
941
+
- |
942
942
+
IMAGE="atcr.io/alice/myapp:${CI_COMMIT_SHA}"
943
943
+
944
944
+
# Install crane
945
945
+
wget https://github.com/google/go-containerregistry/releases/download/v0.15.2/go-containerregistry_Linux_x86_64.tar.gz
946
946
+
tar -xzf go-containerregistry_Linux_x86_64.tar.gz crane
947
947
+
948
948
+
# Get digest
949
949
+
DIGEST=$(./crane digest "$IMAGE")
950
950
+
951
951
+
# Check signature
952
952
+
REFERRERS=$(curl -s "https://atcr.io/v2/alice/myapp/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json")
953
953
+
954
954
+
if [ $(echo "$REFERRERS" | jq '.manifests | length') -eq 0 ]; then
955
955
+
echo "❌ No signature found"
956
956
+
exit 1
957
957
+
fi
958
958
+
959
959
+
echo "✓ Signature verified"
960
960
+
961
961
+
deploy:
962
962
+
stage: deploy
963
963
+
dependencies:
964
964
+
- verify_image
965
965
+
script:
966
966
+
- kubectl set image deployment/myapp myapp=atcr.io/alice/myapp:${CI_COMMIT_SHA}
967
967
+
```
968
968
+
969
969
+
## Integration with Containerd
970
970
+
971
971
+
Containerd can be extended with verification plugins:
972
972
+
973
973
+
```go
974
974
+
// containerd-atcr-verifier plugin
975
975
+
package main
976
976
+
977
977
+
import (
978
978
+
"context"
979
979
+
"fmt"
980
980
+
981
981
+
"github.com/containerd/containerd"
982
982
+
"github.com/containerd/containerd/remotes"
983
983
+
)
984
984
+
985
985
+
type ATCRVerifier struct {
986
986
+
// Configuration
987
987
+
}
988
988
+
989
989
+
func (v *ATCRVerifier) Verify(ctx context.Context, ref string) error {
990
990
+
// 1. Query referrers API for signatures
991
991
+
sigs, err := v.discoverSignatures(ctx, ref)
992
992
+
if err != nil {
993
993
+
return err
994
994
+
}
995
995
+
996
996
+
if len(sigs) == 0 {
997
997
+
return fmt.Errorf("no ATProto signature found for %s", ref)
998
998
+
}
999
999
+
1000
1000
+
// 2. Fetch and verify signature
1001
1001
+
for _, sig := range sigs {
1002
1002
+
err := v.verifySignature(ctx, sig)
1003
1003
+
if err == nil {
1004
1004
+
return nil // At least one valid signature
1005
1005
+
}
1006
1006
+
}
1007
1007
+
1008
1008
+
return fmt.Errorf("all signatures failed verification")
1009
1009
+
}
1010
1010
+
1011
1011
+
// Use as containerd resolver wrapper
1012
1012
+
func NewVerifyingResolver(base remotes.Resolver, verifier *ATCRVerifier) remotes.Resolver {
1013
1013
+
return &verifyingResolver{
1014
1014
+
Resolver: base,
1015
1015
+
verifier: verifier,
1016
1016
+
}
1017
1017
+
}
1018
1018
+
```
1019
1019
+
1020
1020
+
**Containerd config** (`/etc/containerd/config.toml`):
1021
1021
+
1022
1022
+
```toml
1023
1023
+
[plugins."io.containerd.grpc.v1.cri".registry]
1024
1024
+
[plugins."io.containerd.grpc.v1.cri".registry.configs]
1025
1025
+
[plugins."io.containerd.grpc.v1.cri".registry.configs."atcr.io"]
1026
1026
+
[plugins."io.containerd.grpc.v1.cri".registry.configs."atcr.io".auth]
1027
1027
+
username = "alice"
1028
1028
+
password = "..."
1029
1029
+
# Custom verifier hook (requires plugin)
1030
1030
+
verify_signatures = true
1031
1031
+
signature_type = "atproto"
1032
1032
+
```
1033
1033
+
1034
1034
+
## Trust Policies
1035
1035
+
1036
1036
+
Define what signatures you trust:
1037
1037
+
1038
1038
+
```yaml
1039
1039
+
# trust-policy.yaml
1040
1040
+
version: 1.0
1041
1041
+
1042
1042
+
policies:
1043
1043
+
# Production images must be signed
1044
1044
+
- name: production-images
1045
1045
+
scope: "atcr.io/*/prod-*"
1046
1046
+
require:
1047
1047
+
- signature: true
1048
1048
+
trustedDIDs:
1049
1049
+
- did:plc:alice123
1050
1050
+
- did:plc:bob456
1051
1051
+
minSignatures: 1
1052
1052
+
action: enforce # reject if policy fails
1053
1053
+
1054
1054
+
# Development images don't require signatures
1055
1055
+
- name: dev-images
1056
1056
+
scope: "atcr.io/*/dev-*"
1057
1057
+
require:
1058
1058
+
- signature: false
1059
1059
+
action: audit # log but don't reject
1060
1060
+
1061
1061
+
# Staging requires at least 1 signature from any trusted DID
1062
1062
+
- name: staging-images
1063
1063
+
scope: "atcr.io/*/staging-*"
1064
1064
+
require:
1065
1065
+
- signature: true
1066
1066
+
trustedDIDs:
1067
1067
+
- did:plc:alice123
1068
1068
+
- did:plc:bob456
1069
1069
+
- did:plc:charlie789
1070
1070
+
action: enforce
1071
1071
+
1072
1072
+
# DID trust configuration
1073
1073
+
trustedDIDs:
1074
1074
+
did:plc:alice123:
1075
1075
+
name: "Alice (DevOps Lead)"
1076
1076
+
validFrom: "2024-01-01T00:00:00Z"
1077
1077
+
expiresAt: null
1078
1078
+
1079
1079
+
did:plc:bob456:
1080
1080
+
name: "Bob (Security Team)"
1081
1081
+
validFrom: "2024-06-01T00:00:00Z"
1082
1082
+
expiresAt: "2025-12-31T23:59:59Z"
1083
1083
+
```
1084
1084
+
1085
1085
+
## Troubleshooting
1086
1086
+
1087
1087
+
### No Signature Found
1088
1088
+
1089
1089
+
```bash
1090
1090
+
$ oras discover atcr.io/alice/myapp:latest --artifact-type application/vnd.atproto.signature.v1+json
1091
1091
+
1092
1092
+
Discovered 0 artifacts
1093
1093
+
```
1094
1094
+
1095
1095
+
**Possible causes:**
1096
1096
+
1. Image was pushed before signature creation was implemented
1097
1097
+
2. Signature artifact creation failed
1098
1098
+
3. Registry doesn't support Referrers API
1099
1099
+
1100
1100
+
**Solutions:**
1101
1101
+
- Re-push the image to generate signature
1102
1102
+
- Check AppView logs for signature creation errors
1103
1103
+
- Verify Referrers API endpoint: `GET /v2/{repo}/referrers/{digest}`
1104
1104
+
1105
1105
+
### Signature Verification Fails
1106
1106
+
1107
1107
+
**Check DID resolution:**
1108
1108
+
```bash
1109
1109
+
curl -s "https://plc.directory/did:plc:alice123" | jq .
1110
1110
+
# Should return DID document with verificationMethod
1111
1111
+
```
1112
1112
+
1113
1113
+
**Check PDS connectivity:**
1114
1114
+
```bash
1115
1115
+
curl -s "https://bsky.social/xrpc/com.atproto.repo.describeRepo?repo=did:plc:alice123" | jq .
1116
1116
+
# Should return repository metadata
1117
1117
+
```
1118
1118
+
1119
1119
+
**Check record exists:**
1120
1120
+
```bash
1121
1121
+
curl -s "https://bsky.social/xrpc/com.atproto.repo.getRecord?\
1122
1122
+
repo=did:plc:alice123&\
1123
1123
+
collection=io.atcr.manifest&\
1124
1124
+
rkey=abc123" | jq .
1125
1125
+
# Should return manifest record
1126
1126
+
```
1127
1127
+
1128
1128
+
### Registry Returns 404 for Referrers
1129
1129
+
1130
1130
+
Some registries don't support OCI Referrers API yet. Fallback to tag-based discovery:
1131
1131
+
1132
1132
+
```bash
1133
1133
+
# Look for signature tags (if implemented)
1134
1134
+
crane ls atcr.io/alice/myapp | grep sig
1135
1135
+
```
1136
1136
+
1137
1137
+
## Best Practices
1138
1138
+
1139
1139
+
### 1. Always Verify in Production
1140
1140
+
1141
1141
+
Enable signature verification for production namespaces:
1142
1142
+
1143
1143
+
```bash
1144
1144
+
kubectl label namespace production atcr-verify=enabled
1145
1145
+
```
1146
1146
+
1147
1147
+
### 2. Use Trust Policies
1148
1148
+
1149
1149
+
Don't blindly trust all signatures - define which DIDs you trust:
1150
1150
+
1151
1151
+
```yaml
1152
1152
+
trustedDIDs:
1153
1153
+
- did:plc:your-org-team
1154
1154
+
- did:plc:your-ci-system
1155
1155
+
```
1156
1156
+
1157
1157
+
### 3. Monitor Signature Coverage
1158
1158
+
1159
1159
+
Track which images have signatures:
1160
1160
+
1161
1161
+
```bash
1162
1162
+
# Check all images in a namespace
1163
1163
+
kubectl get pods -n production -o json | \
1164
1164
+
jq -r '.items[].spec.containers[].image' | \
1165
1165
+
while read image; do
1166
1166
+
echo -n "$image: "
1167
1167
+
oras discover "$image" --artifact-type application/vnd.atproto.signature.v1+json | \
1168
1168
+
grep -q "Discovered 0" && echo "❌ No signature" || echo "✓ Signed"
1169
1169
+
done
1170
1170
+
```
1171
1171
+
1172
1172
+
### 4. Automate Verification in CI/CD
1173
1173
+
1174
1174
+
Never deploy unsigned images to production:
1175
1175
+
1176
1176
+
```yaml
1177
1177
+
# GitHub Actions
1178
1178
+
- name: Verify signature
1179
1179
+
run: |
1180
1180
+
if ! atcr-verify $IMAGE; then
1181
1181
+
echo "❌ Image is not signed"
1182
1182
+
exit 1
1183
1183
+
fi
1184
1184
+
```
1185
1185
+
1186
1186
+
### 5. Plan for Offline Scenarios
1187
1187
+
1188
1188
+
For air-gapped environments, cache signature metadata and DID documents:
1189
1189
+
1190
1190
+
```bash
1191
1191
+
# Export signatures and DID docs for offline use
1192
1192
+
./export-verification-bundle.sh atcr.io/alice/myapp:latest > bundle.json
1193
1193
+
1194
1194
+
# In air-gapped environment
1195
1195
+
atcr-verify --offline --bundle bundle.json atcr.io/alice/myapp:latest
1196
1196
+
```
1197
1197
+
1198
1198
+
## Next Steps
1199
1199
+
1200
1200
+
1. **Try manual verification** using the shell scripts above
1201
1201
+
2. **Set up admission webhook** for your Kubernetes cluster
1202
1202
+
3. **Define trust policies** for your organization
1203
1203
+
4. **Integrate into CI/CD** pipelines
1204
1204
+
5. **Monitor signature coverage** across your images
1205
1205
+
1206
1206
+
## See Also
1207
1207
+
1208
1208
+
- [ATProto Signatures](./ATPROTO_SIGNATURES.md) - Technical deep-dive
1209
1209
+
- [SBOM Scanning](./SBOM_SCANNING.md) - Similar ORAS artifact pattern
1210
1210
+
- [Example Scripts](../examples/verification/) - Working verification examples
+500
examples/plugins/README.md
···
1
1
+
# ATProto Signature Verification Plugins and Examples
2
2
+
3
3
+
This directory contains reference implementations and examples for integrating ATProto signature verification into various tools and workflows.
4
4
+
5
5
+
## Overview
6
6
+
7
7
+
ATCR uses ATProto's native signature system to cryptographically sign container images. To integrate signature verification into existing tools (Kubernetes, CI/CD, container runtimes), you can:
8
8
+
9
9
+
1. **Build plugins** for verification frameworks (Ratify, Gatekeeper, Containerd)
10
10
+
2. **Use external services** called by policy engines
11
11
+
3. **Integrate CLI tools** in your CI/CD pipelines
12
12
+
13
13
+
## Directory Structure
14
14
+
15
15
+
```
16
16
+
examples/plugins/
17
17
+
├── README.md # This file
18
18
+
├── ratify-verifier/ # Ratify plugin for Kubernetes
19
19
+
│ ├── README.md
20
20
+
│ ├── verifier.go
21
21
+
│ ├── config.go
22
22
+
│ ├── resolver.go
23
23
+
│ ├── crypto.go
24
24
+
│ ├── Dockerfile
25
25
+
│ ├── deployment.yaml
26
26
+
│ └── verifier-crd.yaml
27
27
+
├── gatekeeper-provider/ # OPA Gatekeeper external provider
28
28
+
│ ├── README.md
29
29
+
│ ├── main.go
30
30
+
│ ├── verifier.go
31
31
+
│ ├── resolver.go
32
32
+
│ ├── crypto.go
33
33
+
│ ├── Dockerfile
34
34
+
│ ├── deployment.yaml
35
35
+
│ └── provider-crd.yaml
36
36
+
├── containerd-verifier/ # Containerd bindir plugin
37
37
+
│ ├── README.md
38
38
+
│ ├── main.go
39
39
+
│ └── Dockerfile
40
40
+
└── ci-cd/ # CI/CD integration examples
41
41
+
├── github-actions.yml
42
42
+
├── gitlab-ci.yml
43
43
+
└── jenkins-pipeline.groovy
44
44
+
```
45
45
+
46
46
+
## Quick Start
47
47
+
48
48
+
### For Kubernetes (Recommended)
49
49
+
50
50
+
**Option A: Ratify Plugin**
51
51
+
```bash
52
52
+
cd ratify-verifier
53
53
+
# Build plugin and deploy to Kubernetes
54
54
+
./build.sh
55
55
+
kubectl apply -f deployment.yaml
56
56
+
kubectl apply -f verifier-crd.yaml
57
57
+
```
58
58
+
59
59
+
**Option B: Gatekeeper Provider**
60
60
+
```bash
61
61
+
cd gatekeeper-provider
62
62
+
# Build and deploy external provider
63
63
+
docker build -t atcr.io/atcr/gatekeeper-provider:latest .
64
64
+
kubectl apply -f deployment.yaml
65
65
+
kubectl apply -f provider-crd.yaml
66
66
+
```
67
67
+
68
68
+
### For CI/CD
69
69
+
70
70
+
**GitHub Actions**
71
71
+
```yaml
72
72
+
# Copy examples/plugins/ci-cd/github-actions.yml to .github/workflows/
73
73
+
cp ci-cd/github-actions.yml ../.github/workflows/verify-and-deploy.yml
74
74
+
```
75
75
+
76
76
+
**GitLab CI**
77
77
+
```yaml
78
78
+
# Copy examples/plugins/ci-cd/gitlab-ci.yml to your repo
79
79
+
cp ci-cd/gitlab-ci.yml ../.gitlab-ci.yml
80
80
+
```
81
81
+
82
82
+
### For Containerd
83
83
+
84
84
+
```bash
85
85
+
cd containerd-verifier
86
86
+
# Build plugin
87
87
+
./build.sh
88
88
+
# Install to containerd plugins directory
89
89
+
sudo cp atcr-verifier /opt/containerd/bin/
90
90
+
```
91
91
+
92
92
+
## Plugins Overview
93
93
+
94
94
+
### Ratify Verifier Plugin ⭐
95
95
+
96
96
+
**Use case:** Kubernetes admission control with OPA Gatekeeper
97
97
+
98
98
+
**How it works:**
99
99
+
1. Gatekeeper receives pod creation request
100
100
+
2. Calls Ratify verification engine
101
101
+
3. Ratify loads ATProto verifier plugin
102
102
+
4. Plugin verifies signature and checks trust policy
103
103
+
5. Returns allow/deny decision to Gatekeeper
104
104
+
105
105
+
**Pros:**
106
106
+
- Standard Ratify plugin interface
107
107
+
- Works with existing Gatekeeper deployments
108
108
+
- Can combine with other verifiers (Notation, Cosign)
109
109
+
- Policy-based enforcement
110
110
+
111
111
+
**Cons:**
112
112
+
- Requires building custom Ratify image
113
113
+
- Plugin must be compiled into image
114
114
+
- More complex deployment
115
115
+
116
116
+
**See:** [ratify-verifier/README.md](./ratify-verifier/README.md)
117
117
+
118
118
+
### Gatekeeper External Provider ⭐
119
119
+
120
120
+
**Use case:** Kubernetes admission control with OPA Gatekeeper
121
121
+
122
122
+
**How it works:**
123
123
+
1. Gatekeeper receives pod creation request
124
124
+
2. Rego policy calls external data provider API
125
125
+
3. Provider verifies ATProto signature
126
126
+
4. Returns verification result to Gatekeeper
127
127
+
5. Rego policy makes allow/deny decision
128
128
+
129
129
+
**Pros:**
130
130
+
- Simpler deployment (separate service)
131
131
+
- Easy to update (no Gatekeeper changes)
132
132
+
- Flexible Rego policies
133
133
+
- Can add caching, rate limiting
134
134
+
135
135
+
**Cons:**
136
136
+
- Additional service to maintain
137
137
+
- Network dependency (provider must be reachable)
138
138
+
- Slightly higher latency
139
139
+
140
140
+
**See:** [gatekeeper-provider/README.md](./gatekeeper-provider/README.md)
141
141
+
142
142
+
### Containerd Bindir Plugin
143
143
+
144
144
+
**Use case:** Runtime-level verification for all images
145
145
+
146
146
+
**How it works:**
147
147
+
1. Containerd pulls image
148
148
+
2. Calls verifier plugin (bindir)
149
149
+
3. Plugin verifies ATProto signature
150
150
+
4. Returns result to containerd
151
151
+
5. Containerd allows/blocks image
152
152
+
153
153
+
**Pros:**
154
154
+
- Works at runtime level (not just Kubernetes)
155
155
+
- CRI-O, Podman support (CRI-compatible)
156
156
+
- No Kubernetes required
157
157
+
- Applies to all images
158
158
+
159
159
+
**Cons:**
160
160
+
- Containerd 2.0+ required
161
161
+
- More complex to debug
162
162
+
- Less flexible policies
163
163
+
164
164
+
**See:** [containerd-verifier/README.md](./containerd-verifier/README.md)
165
165
+
166
166
+
## CI/CD Integration Examples
167
167
+
168
168
+
### GitHub Actions
169
169
+
170
170
+
Complete workflow with:
171
171
+
- Image signature verification
172
172
+
- DID trust checking
173
173
+
- Automated deployment on success
174
174
+
175
175
+
**See:** [ci-cd/github-actions.yml](./ci-cd/github-actions.yml)
176
176
+
177
177
+
### GitLab CI
178
178
+
179
179
+
Pipeline with:
180
180
+
- Multi-stage verification
181
181
+
- Trust policy enforcement
182
182
+
- Manual deployment approval
183
183
+
184
184
+
**See:** [ci-cd/gitlab-ci.yml](./ci-cd/gitlab-ci.yml)
185
185
+
186
186
+
### Jenkins
187
187
+
188
188
+
Declarative pipeline with:
189
189
+
- Signature verification stage
190
190
+
- Deployment gates
191
191
+
- Rollback on failure
192
192
+
193
193
+
**See:** [ci-cd/jenkins-pipeline.groovy](./ci-cd/jenkins-pipeline.groovy) (coming soon)
194
194
+
195
195
+
## Common Components
196
196
+
197
197
+
All plugins share common functionality:
198
198
+
199
199
+
### DID Resolution
200
200
+
201
201
+
Resolve DID to public key:
202
202
+
```go
203
203
+
func ResolveDIDToPublicKey(ctx context.Context, did string) (*PublicKey, error)
204
204
+
```
205
205
+
206
206
+
**Steps:**
207
207
+
1. Fetch DID document from PLC directory or did:web
208
208
+
2. Extract verification method
209
209
+
3. Decode multibase public key
210
210
+
4. Parse as K-256 public key
211
211
+
212
212
+
### PDS Communication
213
213
+
214
214
+
Fetch repository commit:
215
215
+
```go
216
216
+
func FetchCommit(ctx context.Context, pdsEndpoint, did, commitCID string) (*Commit, error)
217
217
+
```
218
218
+
219
219
+
**Steps:**
220
220
+
1. Call `com.atproto.sync.getRepo` XRPC endpoint
221
221
+
2. Parse CAR file response
222
222
+
3. Extract commit with matching CID
223
223
+
4. Return commit data and signature
224
224
+
225
225
+
### Signature Verification
226
226
+
227
227
+
Verify ECDSA K-256 signature:
228
228
+
```go
229
229
+
func VerifySignature(pubKey *PublicKey, commit *Commit) error
230
230
+
```
231
231
+
232
232
+
**Steps:**
233
233
+
1. Extract unsigned commit bytes
234
234
+
2. Hash with SHA-256
235
235
+
3. Verify ECDSA signature over hash
236
236
+
4. Check signature is valid for public key
237
237
+
238
238
+
### Trust Policy
239
239
+
240
240
+
Check if DID is trusted:
241
241
+
```go
242
242
+
func IsTrusted(did string, now time.Time) bool
243
243
+
```
244
244
+
245
245
+
**Steps:**
246
246
+
1. Load trust policy from config
247
247
+
2. Check if DID in trusted list
248
248
+
3. Verify validFrom/expiresAt timestamps
249
249
+
4. Return true if trusted
250
250
+
251
251
+
## Trust Policy Format
252
252
+
253
253
+
All plugins use the same trust policy format:
254
254
+
255
255
+
```yaml
256
256
+
version: 1.0
257
257
+
258
258
+
trustedDIDs:
259
259
+
did:plc:alice123:
260
260
+
name: "Alice (DevOps Lead)"
261
261
+
validFrom: "2024-01-01T00:00:00Z"
262
262
+
expiresAt: null
263
263
+
264
264
+
did:plc:bob456:
265
265
+
name: "Bob (Security Team)"
266
266
+
validFrom: "2024-06-01T00:00:00Z"
267
267
+
expiresAt: "2025-12-31T23:59:59Z"
268
268
+
269
269
+
policies:
270
270
+
- name: production-images
271
271
+
scope: "atcr.io/*/prod-*"
272
272
+
require:
273
273
+
signature: true
274
274
+
trustedDIDs:
275
275
+
- did:plc:alice123
276
276
+
- did:plc:bob456
277
277
+
minSignatures: 1
278
278
+
action: enforce
279
279
+
280
280
+
- name: dev-images
281
281
+
scope: "atcr.io/*/dev-*"
282
282
+
require:
283
283
+
signature: false
284
284
+
action: audit
285
285
+
```
286
286
+
287
287
+
## Implementation Notes
288
288
+
289
289
+
### Dependencies
290
290
+
291
291
+
All plugins require:
292
292
+
- Go 1.21+ for building
293
293
+
- ATProto DID resolution (PLC directory, did:web)
294
294
+
- ATProto PDS XRPC API access
295
295
+
- ECDSA K-256 signature verification
296
296
+
297
297
+
### Caching
298
298
+
299
299
+
Recommended caching strategy:
300
300
+
- **DID documents**: 5 minute TTL
301
301
+
- **Public keys**: 5 minute TTL
302
302
+
- **PDS endpoints**: 5 minute TTL
303
303
+
- **Signature results**: 5 minute TTL
304
304
+
305
305
+
### Error Handling
306
306
+
307
307
+
Plugins should handle:
308
308
+
- DID resolution failures (network, invalid DID)
309
309
+
- PDS connectivity issues (timeout, 404, 500)
310
310
+
- Invalid signature format
311
311
+
- Untrusted DIDs
312
312
+
- Network timeouts
313
313
+
314
314
+
### Logging
315
315
+
316
316
+
Structured logging with:
317
317
+
- `image` - Image being verified
318
318
+
- `did` - Signer DID
319
319
+
- `duration` - Operation duration
320
320
+
- `error` - Error message (if failed)
321
321
+
322
322
+
### Metrics
323
323
+
324
324
+
Expose Prometheus metrics:
325
325
+
- `atcr_verifications_total{result="verified|failed|error"}`
326
326
+
- `atcr_verification_duration_seconds`
327
327
+
- `atcr_did_resolutions_total{result="success|failure"}`
328
328
+
- `atcr_cache_hits_total`
329
329
+
- `atcr_cache_misses_total`
330
330
+
331
331
+
## Testing
332
332
+
333
333
+
### Unit Tests
334
334
+
335
335
+
Test individual components:
336
336
+
```bash
337
337
+
# Test DID resolution
338
338
+
go test ./pkg/resolver -v
339
339
+
340
340
+
# Test signature verification
341
341
+
go test ./pkg/crypto -v
342
342
+
343
343
+
# Test trust policy
344
344
+
go test ./pkg/trust -v
345
345
+
```
346
346
+
347
347
+
### Integration Tests
348
348
+
349
349
+
Test with real services:
350
350
+
```bash
351
351
+
# Test against ATCR registry
352
352
+
go test ./integration -tags=integration -v
353
353
+
354
354
+
# Test with test PDS
355
355
+
go test ./integration -tags=integration -pds=https://test.pds.example.com
356
356
+
```
357
357
+
358
358
+
### End-to-End Tests
359
359
+
360
360
+
Test full deployment:
361
361
+
```bash
362
362
+
# Deploy to test cluster
363
363
+
kubectl apply -f test/fixtures/
364
364
+
365
365
+
# Create pod with signed image (should succeed)
366
366
+
kubectl run test-signed --image=atcr.io/test/signed:latest
367
367
+
368
368
+
# Create pod with unsigned image (should fail)
369
369
+
kubectl run test-unsigned --image=atcr.io/test/unsigned:latest
370
370
+
```
371
371
+
372
372
+
## Performance Considerations
373
373
+
374
374
+
### Latency
375
375
+
376
376
+
Typical verification latency:
377
377
+
- DID resolution: 50-200ms (cached: <1ms)
378
378
+
- PDS query: 100-500ms (cached: <1ms)
379
379
+
- Signature verification: 1-5ms
380
380
+
- **Total**: 150-700ms (uncached), <10ms (cached)
381
381
+
382
382
+
### Throughput
383
383
+
384
384
+
Expected throughput (single instance):
385
385
+
- Without caching: ~5-10 verifications/second
386
386
+
- With caching: ~100-500 verifications/second
387
387
+
388
388
+
### Scaling
389
389
+
390
390
+
For high traffic:
391
391
+
- Deploy multiple replicas (stateless)
392
392
+
- Use Redis for distributed caching
393
393
+
- Implement rate limiting
394
394
+
- Monitor P95/P99 latency
395
395
+
396
396
+
## Security Considerations
397
397
+
398
398
+
### Network Policies
399
399
+
400
400
+
Restrict access to:
401
401
+
- DID resolution (PLC directory only)
402
402
+
- PDS XRPC endpoints
403
403
+
- Internal services only
404
404
+
405
405
+
### Denial of Service
406
406
+
407
407
+
Protect against:
408
408
+
- High verification request rate
409
409
+
- Slow DID resolution
410
410
+
- Malicious images with many signatures
411
411
+
- Large signature artifacts
412
412
+
413
413
+
### Trust Model
414
414
+
415
415
+
Understand trust dependencies:
416
416
+
- DID resolution is accurate (PLC directory)
417
417
+
- PDS serves correct records
418
418
+
- Private keys are secure
419
419
+
- Trust policy is maintained
420
420
+
421
421
+
## Troubleshooting
422
422
+
423
423
+
### Plugin Not Loading
424
424
+
425
425
+
```bash
426
426
+
# Check plugin exists
427
427
+
ls -la /path/to/plugin
428
428
+
429
429
+
# Check plugin is executable
430
430
+
chmod +x /path/to/plugin
431
431
+
432
432
+
# Check plugin logs
433
433
+
tail -f /var/log/atcr-verifier.log
434
434
+
```
435
435
+
436
436
+
### Verification Failing
437
437
+
438
438
+
```bash
439
439
+
# Test DID resolution
440
440
+
curl https://plc.directory/did:plc:alice123
441
441
+
442
442
+
# Test PDS connectivity
443
443
+
curl https://bsky.social/xrpc/com.atproto.server.describeServer
444
444
+
445
445
+
# Test signature exists
446
446
+
oras discover atcr.io/alice/myapp:latest \
447
447
+
--artifact-type application/vnd.atproto.signature.v1+json
448
448
+
```
449
449
+
450
450
+
### Policy Not Enforcing
451
451
+
452
452
+
```bash
453
453
+
# Check policy is loaded
454
454
+
kubectl get configmap atcr-trust-policy -n gatekeeper-system
455
455
+
456
456
+
# Check constraint is active
457
457
+
kubectl get constraint atcr-signatures-required -o yaml
458
458
+
459
459
+
# Check logs
460
460
+
kubectl logs -n gatekeeper-system deployment/ratify
461
461
+
```
462
462
+
463
463
+
## See Also
464
464
+
465
465
+
### Documentation
466
466
+
467
467
+
- [ATProto Signatures](../../docs/ATPROTO_SIGNATURES.md) - Technical deep-dive
468
468
+
- [Signature Integration](../../docs/SIGNATURE_INTEGRATION.md) - Tool-specific guides
469
469
+
- [Integration Strategy](../../docs/INTEGRATION_STRATEGY.md) - High-level overview
470
470
+
- [atcr-verify CLI](../../docs/ATCR_VERIFY_CLI.md) - CLI tool specification
471
471
+
472
472
+
### Examples
473
473
+
474
474
+
- [Verification Scripts](../verification/) - Shell scripts for manual verification
475
475
+
- [Kubernetes Webhook](../verification/kubernetes-webhook.yaml) - Custom webhook example
476
476
+
477
477
+
### External Resources
478
478
+
479
479
+
- [Ratify](https://ratify.dev/) - Verification framework
480
480
+
- [OPA Gatekeeper](https://open-policy-agent.github.io/gatekeeper/) - Policy engine
481
481
+
- [Containerd](https://containerd.io/) - Container runtime
482
482
+
483
483
+
## Support
484
484
+
485
485
+
For questions or issues:
486
486
+
- GitHub Issues: https://github.com/atcr-io/atcr/issues
487
487
+
- Documentation: https://docs.atcr.io
488
488
+
- Security: security@atcr.io
489
489
+
490
490
+
## Contributing
491
491
+
492
492
+
Contributions welcome! Please:
493
493
+
1. Follow existing code structure
494
494
+
2. Add tests for new features
495
495
+
3. Update documentation
496
496
+
4. Submit pull request
497
497
+
498
498
+
## License
499
499
+
500
500
+
See [LICENSE](../../LICENSE) file in repository root.
+166
examples/plugins/ci-cd/github-actions.yml
···
1
1
+
# GitHub Actions workflow for verifying ATProto signatures
2
2
+
3
3
+
name: Verify and Deploy
4
4
+
5
5
+
on:
6
6
+
push:
7
7
+
branches: [main]
8
8
+
pull_request:
9
9
+
branches: [main]
10
10
+
11
11
+
env:
12
12
+
REGISTRY: atcr.io
13
13
+
IMAGE_NAME: ${{ github.repository }}
14
14
+
15
15
+
jobs:
16
16
+
verify-signature:
17
17
+
name: Verify Image Signature
18
18
+
runs-on: ubuntu-latest
19
19
+
steps:
20
20
+
- name: Checkout code
21
21
+
uses: actions/checkout@v4
22
22
+
23
23
+
- name: Set up image tag
24
24
+
id: vars
25
25
+
run: |
26
26
+
echo "IMAGE_TAG=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}" >> $GITHUB_OUTPUT
27
27
+
28
28
+
- name: Install verification tools
29
29
+
run: |
30
30
+
# Install ORAS
31
31
+
curl -LO https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz
32
32
+
tar -xzf oras_1.0.0_linux_amd64.tar.gz
33
33
+
sudo mv oras /usr/local/bin/
34
34
+
35
35
+
# Install crane
36
36
+
curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.15.2/go-containerregistry_Linux_x86_64.tar.gz" > crane.tar.gz
37
37
+
tar -xzf crane.tar.gz
38
38
+
sudo mv crane /usr/local/bin/
39
39
+
40
40
+
# Install atcr-verify (when available)
41
41
+
# curl -LO https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify
42
42
+
# chmod +x atcr-verify
43
43
+
# sudo mv atcr-verify /usr/local/bin/
44
44
+
45
45
+
- name: Check for signature
46
46
+
id: check_signature
47
47
+
run: |
48
48
+
IMAGE="${{ steps.vars.outputs.IMAGE_TAG }}"
49
49
+
echo "Checking signature for $IMAGE"
50
50
+
51
51
+
# Get image digest
52
52
+
DIGEST=$(crane digest "$IMAGE")
53
53
+
echo "Image digest: $DIGEST"
54
54
+
55
55
+
# Check for ATProto signature using ORAS
56
56
+
REPO=$(echo "$IMAGE" | cut -d: -f1)
57
57
+
REFERRERS=$(curl -s "https://${{ env.REGISTRY }}/v2/${REPO#${{ env.REGISTRY }}/}/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json")
58
58
+
59
59
+
SIG_COUNT=$(echo "$REFERRERS" | jq '.manifests | length')
60
60
+
61
61
+
if [ "$SIG_COUNT" -eq 0 ]; then
62
62
+
echo "❌ No ATProto signature found"
63
63
+
echo "has_signature=false" >> $GITHUB_OUTPUT
64
64
+
exit 1
65
65
+
fi
66
66
+
67
67
+
echo "✓ Found $SIG_COUNT signature(s)"
68
68
+
echo "has_signature=true" >> $GITHUB_OUTPUT
69
69
+
70
70
+
- name: Verify signature (full verification)
71
71
+
if: steps.check_signature.outputs.has_signature == 'true'
72
72
+
run: |
73
73
+
IMAGE="${{ steps.vars.outputs.IMAGE_TAG }}"
74
74
+
75
75
+
# Option 1: Use atcr-verify CLI (when available)
76
76
+
# atcr-verify "$IMAGE" --policy .atcr/trust-policy.yaml
77
77
+
78
78
+
# Option 2: Use shell script
79
79
+
chmod +x examples/verification/atcr-verify.sh
80
80
+
./examples/verification/atcr-verify.sh "$IMAGE"
81
81
+
82
82
+
echo "✓ Signature verified successfully"
83
83
+
84
84
+
- name: Verify signer DID
85
85
+
if: steps.check_signature.outputs.has_signature == 'true'
86
86
+
run: |
87
87
+
IMAGE="${{ steps.vars.outputs.IMAGE_TAG }}"
88
88
+
89
89
+
# Get signature metadata
90
90
+
DIGEST=$(crane digest "$IMAGE")
91
91
+
REPO=$(echo "$IMAGE" | cut -d: -f1)
92
92
+
93
93
+
REFERRERS=$(curl -s "https://${{ env.REGISTRY }}/v2/${REPO#${{ env.REGISTRY }}/}/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json")
94
94
+
SIG_DIGEST=$(echo "$REFERRERS" | jq -r '.manifests[0].digest')
95
95
+
96
96
+
# Pull signature artifact
97
97
+
oras pull "${REPO}@${SIG_DIGEST}" -o /tmp/sig
98
98
+
99
99
+
# Extract DID
100
100
+
DID=$(jq -r '.atproto.did' /tmp/sig/atproto-signature.json)
101
101
+
echo "Signed by DID: $DID"
102
102
+
103
103
+
# Check against trusted DIDs
104
104
+
TRUSTED_DIDS="${{ secrets.TRUSTED_DIDS }}" # e.g., "did:plc:alice123,did:plc:bob456"
105
105
+
106
106
+
if [[ ",$TRUSTED_DIDS," == *",$DID,"* ]]; then
107
107
+
echo "✓ DID is trusted"
108
108
+
else
109
109
+
echo "❌ DID $DID is not in trusted list"
110
110
+
exit 1
111
111
+
fi
112
112
+
113
113
+
deploy:
114
114
+
name: Deploy to Kubernetes
115
115
+
needs: verify-signature
116
116
+
runs-on: ubuntu-latest
117
117
+
if: github.ref == 'refs/heads/main'
118
118
+
steps:
119
119
+
- name: Checkout code
120
120
+
uses: actions/checkout@v4
121
121
+
122
122
+
- name: Set up image tag
123
123
+
id: vars
124
124
+
run: |
125
125
+
echo "IMAGE_TAG=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}" >> $GITHUB_OUTPUT
126
126
+
127
127
+
- name: Set up kubectl
128
128
+
uses: azure/setup-kubectl@v3
129
129
+
130
130
+
- name: Configure kubectl
131
131
+
run: |
132
132
+
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > /tmp/kubeconfig
133
133
+
export KUBECONFIG=/tmp/kubeconfig
134
134
+
135
135
+
- name: Deploy to production
136
136
+
run: |
137
137
+
kubectl set image deployment/myapp \
138
138
+
myapp=${{ steps.vars.outputs.IMAGE_TAG }} \
139
139
+
-n production
140
140
+
141
141
+
kubectl rollout status deployment/myapp -n production
142
142
+
143
143
+
- name: Verify deployment
144
144
+
run: |
145
145
+
kubectl get pods -n production -l app=myapp
146
146
+
147
147
+
# Wait for rollout to complete
148
148
+
kubectl wait --for=condition=available --timeout=300s \
149
149
+
deployment/myapp -n production
150
150
+
151
151
+
# Alternative: Use atcr-verify action (when available)
152
152
+
verify-with-action:
153
153
+
name: Verify with ATCR Action
154
154
+
runs-on: ubuntu-latest
155
155
+
steps:
156
156
+
- name: Checkout code
157
157
+
uses: actions/checkout@v4
158
158
+
159
159
+
- name: Verify image signature
160
160
+
# uses: atcr-io/atcr-verify-action@v1
161
161
+
# with:
162
162
+
# image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
163
163
+
# policy: .atcr/trust-policy.yaml
164
164
+
# fail-on-error: true
165
165
+
run: |
166
166
+
echo "TODO: Use official atcr-verify GitHub Action"
+156
examples/plugins/ci-cd/gitlab-ci.yml
···
1
1
+
# GitLab CI pipeline for verifying ATProto signatures
2
2
+
3
3
+
variables:
4
4
+
REGISTRY: atcr.io
5
5
+
IMAGE_NAME: $CI_PROJECT_PATH
6
6
+
IMAGE_TAG: $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
7
7
+
8
8
+
stages:
9
9
+
- build
10
10
+
- verify
11
11
+
- deploy
12
12
+
13
13
+
build_image:
14
14
+
stage: build
15
15
+
image: docker:latest
16
16
+
services:
17
17
+
- docker:dind
18
18
+
script:
19
19
+
- docker build -t $IMAGE_TAG .
20
20
+
- docker push $IMAGE_TAG
21
21
+
22
22
+
verify_signature:
23
23
+
stage: verify
24
24
+
image: alpine:latest
25
25
+
before_script:
26
26
+
- apk add --no-cache curl jq
27
27
+
script:
28
28
+
- |
29
29
+
echo "Verifying signature for $IMAGE_TAG"
30
30
+
31
31
+
# Install crane
32
32
+
wget https://github.com/google/go-containerregistry/releases/download/v0.15.2/go-containerregistry_Linux_x86_64.tar.gz
33
33
+
tar -xzf go-containerregistry_Linux_x86_64.tar.gz crane
34
34
+
mv crane /usr/local/bin/
35
35
+
36
36
+
# Get image digest
37
37
+
DIGEST=$(crane digest "$IMAGE_TAG")
38
38
+
echo "Image digest: $DIGEST"
39
39
+
40
40
+
# Extract repository path
41
41
+
REPO=$(echo "$IMAGE_TAG" | cut -d: -f1)
42
42
+
REPO_PATH=${REPO#$REGISTRY/}
43
43
+
44
44
+
# Check for ATProto signature
45
45
+
REFERRERS=$(curl -s "https://$REGISTRY/v2/$REPO_PATH/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json")
46
46
+
47
47
+
SIG_COUNT=$(echo "$REFERRERS" | jq '.manifests | length')
48
48
+
49
49
+
if [ "$SIG_COUNT" -eq 0 ]; then
50
50
+
echo "❌ No ATProto signature found"
51
51
+
exit 1
52
52
+
fi
53
53
+
54
54
+
echo "✓ Found $SIG_COUNT signature(s)"
55
55
+
56
56
+
verify_full:
57
57
+
stage: verify
58
58
+
image: alpine:latest
59
59
+
before_script:
60
60
+
- apk add --no-cache curl jq bash
61
61
+
script:
62
62
+
- |
63
63
+
# Option 1: Use atcr-verify CLI (when available)
64
64
+
# wget https://github.com/atcr-io/atcr/releases/latest/download/atcr-verify
65
65
+
# chmod +x atcr-verify
66
66
+
# ./atcr-verify "$IMAGE_TAG" --policy .atcr/trust-policy.yaml
67
67
+
68
68
+
# Option 2: Use shell script
69
69
+
chmod +x examples/verification/atcr-verify.sh
70
70
+
./examples/verification/atcr-verify.sh "$IMAGE_TAG"
71
71
+
72
72
+
echo "✓ Signature verified successfully"
73
73
+
74
74
+
verify_trust:
75
75
+
stage: verify
76
76
+
image: alpine:latest
77
77
+
before_script:
78
78
+
- apk add --no-cache curl jq
79
79
+
script:
80
80
+
- |
81
81
+
# Install crane and ORAS
82
82
+
wget https://github.com/google/go-containerregistry/releases/download/v0.15.2/go-containerregistry_Linux_x86_64.tar.gz
83
83
+
tar -xzf go-containerregistry_Linux_x86_64.tar.gz crane
84
84
+
mv crane /usr/local/bin/
85
85
+
86
86
+
wget https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz
87
87
+
tar -xzf oras_1.0.0_linux_amd64.tar.gz
88
88
+
mv oras /usr/local/bin/
89
89
+
90
90
+
# Get signature metadata
91
91
+
DIGEST=$(crane digest "$IMAGE_TAG")
92
92
+
REPO=$(echo "$IMAGE_TAG" | cut -d: -f1)
93
93
+
REPO_PATH=${REPO#$REGISTRY/}
94
94
+
95
95
+
REFERRERS=$(curl -s "https://$REGISTRY/v2/$REPO_PATH/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json")
96
96
+
SIG_DIGEST=$(echo "$REFERRERS" | jq -r '.manifests[0].digest')
97
97
+
98
98
+
# Pull signature artifact
99
99
+
oras pull "${REPO}@${SIG_DIGEST}" -o /tmp/sig
100
100
+
101
101
+
# Extract DID
102
102
+
DID=$(jq -r '.atproto.did' /tmp/sig/atproto-signature.json)
103
103
+
echo "Signed by DID: $DID"
104
104
+
105
105
+
# Check against trusted DIDs (from CI/CD variables)
106
106
+
if [[ ",$TRUSTED_DIDS," == *",$DID,"* ]]; then
107
107
+
echo "✓ DID is trusted"
108
108
+
else
109
109
+
echo "❌ DID $DID is not in trusted list"
110
110
+
exit 1
111
111
+
fi
112
112
+
113
113
+
deploy_production:
114
114
+
stage: deploy
115
115
+
image: bitnami/kubectl:latest
116
116
+
dependencies:
117
117
+
- verify_signature
118
118
+
- verify_full
119
119
+
- verify_trust
120
120
+
only:
121
121
+
- main
122
122
+
script:
123
123
+
- |
124
124
+
# Configure kubectl
125
125
+
echo "$KUBE_CONFIG" | base64 -d > /tmp/kubeconfig
126
126
+
export KUBECONFIG=/tmp/kubeconfig
127
127
+
128
128
+
# Deploy to production
129
129
+
kubectl set image deployment/myapp \
130
130
+
myapp=$IMAGE_TAG \
131
131
+
-n production
132
132
+
133
133
+
kubectl rollout status deployment/myapp -n production
134
134
+
135
135
+
# Verify deployment
136
136
+
kubectl get pods -n production -l app=myapp
137
137
+
138
138
+
# Alternative: Manual approval before deploy
139
139
+
deploy_production_manual:
140
140
+
stage: deploy
141
141
+
image: bitnami/kubectl:latest
142
142
+
dependencies:
143
143
+
- verify_signature
144
144
+
when: manual
145
145
+
only:
146
146
+
- main
147
147
+
script:
148
148
+
- |
149
149
+
echo "Deploying $IMAGE_TAG to production"
150
150
+
151
151
+
echo "$KUBE_CONFIG" | base64 -d > /tmp/kubeconfig
152
152
+
export KUBECONFIG=/tmp/kubeconfig
153
153
+
154
154
+
kubectl set image deployment/myapp \
155
155
+
myapp=$IMAGE_TAG \
156
156
+
-n production
+501
examples/plugins/gatekeeper-provider/README.md
···
1
1
+
# OPA Gatekeeper External Data Provider for ATProto Signatures
2
2
+
3
3
+
This is a reference implementation of an OPA Gatekeeper External Data Provider that verifies ATProto signatures on ATCR container images.
4
4
+
5
5
+
## Overview
6
6
+
7
7
+
Gatekeeper's External Data Provider feature allows Rego policies to call external HTTP services for data validation. This provider implements signature verification as an HTTP service that Gatekeeper can query.
8
8
+
9
9
+
## Architecture
10
10
+
11
11
+
```
12
12
+
Kubernetes Pod Creation
13
13
+
↓
14
14
+
OPA Gatekeeper (admission webhook)
15
15
+
↓
16
16
+
Rego Policy (constraint template)
17
17
+
↓
18
18
+
External Data Provider API call
19
19
+
↓
20
20
+
ATProto Verification Service ← This service
21
21
+
↓
22
22
+
1. Resolve image digest
23
23
+
2. Discover signature artifacts
24
24
+
3. Parse ATProto signature metadata
25
25
+
4. Resolve DID to public key
26
26
+
5. Fetch commit from PDS
27
27
+
6. Verify K-256 signature
28
28
+
7. Check trust policy
29
29
+
↓
30
30
+
Return: verified=true/false + metadata
31
31
+
```
32
32
+
33
33
+
## Files
34
34
+
35
35
+
- `main.go` - HTTP server and provider endpoints
36
36
+
- `verifier.go` - ATProto signature verification logic
37
37
+
- `resolver.go` - DID and PDS resolution
38
38
+
- `crypto.go` - K-256 signature verification
39
39
+
- `trust-policy.yaml` - Trust policy configuration
40
40
+
- `Dockerfile` - Build provider service image
41
41
+
- `deployment.yaml` - Kubernetes deployment manifest
42
42
+
- `provider-crd.yaml` - Gatekeeper Provider custom resource
43
43
+
- `constraint-template.yaml` - Rego constraint template
44
44
+
- `constraint.yaml` - Policy constraint example
45
45
+
46
46
+
## Prerequisites
47
47
+
48
48
+
- Go 1.21+
49
49
+
- Kubernetes cluster with OPA Gatekeeper installed
50
50
+
- Access to ATCR registry
51
51
+
52
52
+
## Building
53
53
+
54
54
+
```bash
55
55
+
# Build binary
56
56
+
CGO_ENABLED=0 go build -o atcr-provider \
57
57
+
-ldflags="-w -s" \
58
58
+
./main.go
59
59
+
60
60
+
# Build Docker image
61
61
+
docker build -t atcr.io/atcr/gatekeeper-provider:latest .
62
62
+
63
63
+
# Push to registry
64
64
+
docker push atcr.io/atcr/gatekeeper-provider:latest
65
65
+
```
66
66
+
67
67
+
## Deployment
68
68
+
69
69
+
### 1. Create Trust Policy ConfigMap
70
70
+
71
71
+
```bash
72
72
+
kubectl create namespace gatekeeper-system
73
73
+
kubectl create configmap atcr-trust-policy \
74
74
+
--from-file=trust-policy.yaml \
75
75
+
-n gatekeeper-system
76
76
+
```
77
77
+
78
78
+
### 2. Deploy Provider Service
79
79
+
80
80
+
```bash
81
81
+
kubectl apply -f deployment.yaml
82
82
+
```
83
83
+
84
84
+
### 3. Configure Gatekeeper Provider
85
85
+
86
86
+
```bash
87
87
+
kubectl apply -f provider-crd.yaml
88
88
+
```
89
89
+
90
90
+
### 4. Create Constraint Template
91
91
+
92
92
+
```bash
93
93
+
kubectl apply -f constraint-template.yaml
94
94
+
```
95
95
+
96
96
+
### 5. Create Constraint
97
97
+
98
98
+
```bash
99
99
+
kubectl apply -f constraint.yaml
100
100
+
```
101
101
+
102
102
+
### 6. Test
103
103
+
104
104
+
```bash
105
105
+
# Try to create pod with signed image (should succeed)
106
106
+
kubectl run test-signed --image=atcr.io/alice/myapp:latest
107
107
+
108
108
+
# Try to create pod with unsigned image (should fail)
109
109
+
kubectl run test-unsigned --image=atcr.io/malicious/fake:latest
110
110
+
111
111
+
# Check constraint status
112
112
+
kubectl get constraint atcr-signatures-required -o yaml
113
113
+
```
114
114
+
115
115
+
## API Specification
116
116
+
117
117
+
### Provider Endpoint
118
118
+
119
119
+
**POST /provide**
120
120
+
121
121
+
Request:
122
122
+
```json
123
123
+
{
124
124
+
"keys": ["image"],
125
125
+
"values": [
126
126
+
"atcr.io/alice/myapp:latest",
127
127
+
"atcr.io/bob/webapp:v1.0"
128
128
+
]
129
129
+
}
130
130
+
```
131
131
+
132
132
+
Response:
133
133
+
```json
134
134
+
{
135
135
+
"responses": [
136
136
+
{
137
137
+
"image": "atcr.io/alice/myapp:latest",
138
138
+
"verified": true,
139
139
+
"did": "did:plc:alice123",
140
140
+
"handle": "alice.bsky.social",
141
141
+
"signedAt": "2025-10-31T12:34:56Z",
142
142
+
"commitCid": "bafyreih8..."
143
143
+
},
144
144
+
{
145
145
+
"image": "atcr.io/bob/webapp:v1.0",
146
146
+
"verified": false,
147
147
+
"error": "no signature found"
148
148
+
}
149
149
+
]
150
150
+
}
151
151
+
```
152
152
+
153
153
+
### Health Check
154
154
+
155
155
+
**GET /health**
156
156
+
157
157
+
Response:
158
158
+
```json
159
159
+
{
160
160
+
"status": "ok",
161
161
+
"version": "1.0.0"
162
162
+
}
163
163
+
```
164
164
+
165
165
+
## Configuration
166
166
+
167
167
+
### Trust Policy Format
168
168
+
169
169
+
```yaml
170
170
+
# trust-policy.yaml
171
171
+
version: 1.0
172
172
+
173
173
+
trustedDIDs:
174
174
+
did:plc:alice123:
175
175
+
name: "Alice (DevOps)"
176
176
+
validFrom: "2024-01-01T00:00:00Z"
177
177
+
expiresAt: null
178
178
+
179
179
+
did:plc:bob456:
180
180
+
name: "Bob (Security)"
181
181
+
validFrom: "2024-06-01T00:00:00Z"
182
182
+
expiresAt: "2025-12-31T23:59:59Z"
183
183
+
184
184
+
policies:
185
185
+
- name: production
186
186
+
scope: "atcr.io/*/prod-*"
187
187
+
require:
188
188
+
signature: true
189
189
+
trustedDIDs:
190
190
+
- did:plc:alice123
191
191
+
- did:plc:bob456
192
192
+
action: enforce
193
193
+
```
194
194
+
195
195
+
### Provider Configuration
196
196
+
197
197
+
Environment variables:
198
198
+
- `TRUST_POLICY_PATH` - Path to trust policy file (default: `/config/trust-policy.yaml`)
199
199
+
- `HTTP_PORT` - HTTP server port (default: `8080`)
200
200
+
- `LOG_LEVEL` - Log level: debug, info, warn, error (default: `info`)
201
201
+
- `CACHE_ENABLED` - Enable caching (default: `true`)
202
202
+
- `CACHE_TTL` - Cache TTL in seconds (default: `300`)
203
203
+
- `DID_RESOLVER_TIMEOUT` - DID resolution timeout (default: `10s`)
204
204
+
- `PDS_TIMEOUT` - PDS XRPC timeout (default: `10s`)
205
205
+
206
206
+
## Rego Policy Examples
207
207
+
208
208
+
### Simple Verification
209
209
+
210
210
+
```rego
211
211
+
package atcrsignatures
212
212
+
213
213
+
import future.keywords.contains
214
214
+
import future.keywords.if
215
215
+
import future.keywords.in
216
216
+
217
217
+
provider := "atcr-verifier"
218
218
+
219
219
+
violation[{"msg": msg}] {
220
220
+
container := input.review.object.spec.containers[_]
221
221
+
startswith(container.image, "atcr.io/")
222
222
+
223
223
+
# Call external provider
224
224
+
response := external_data({
225
225
+
"provider": provider,
226
226
+
"keys": ["image"],
227
227
+
"values": [container.image]
228
228
+
})
229
229
+
230
230
+
# Check verification result
231
231
+
not response[_].verified == true
232
232
+
233
233
+
msg := sprintf("Image %v has no valid ATProto signature", [container.image])
234
234
+
}
235
235
+
```
236
236
+
237
237
+
### Advanced Verification with DID Trust
238
238
+
239
239
+
```rego
240
240
+
package atcrsignatures
241
241
+
242
242
+
import future.keywords.contains
243
243
+
import future.keywords.if
244
244
+
import future.keywords.in
245
245
+
246
246
+
provider := "atcr-verifier"
247
247
+
248
248
+
trusted_dids := [
249
249
+
"did:plc:alice123",
250
250
+
"did:plc:bob456"
251
251
+
]
252
252
+
253
253
+
violation[{"msg": msg}] {
254
254
+
container := input.review.object.spec.containers[_]
255
255
+
startswith(container.image, "atcr.io/")
256
256
+
257
257
+
# Call external provider
258
258
+
response := external_data({
259
259
+
"provider": provider,
260
260
+
"keys": ["image"],
261
261
+
"values": [container.image]
262
262
+
})
263
263
+
264
264
+
# Get response for this image
265
265
+
result := response[_]
266
266
+
result.image == container.image
267
267
+
268
268
+
# Check if verified
269
269
+
not result.verified == true
270
270
+
msg := sprintf("Image %v failed signature verification: %v", [container.image, result.error])
271
271
+
}
272
272
+
273
273
+
violation[{"msg": msg}] {
274
274
+
container := input.review.object.spec.containers[_]
275
275
+
startswith(container.image, "atcr.io/")
276
276
+
277
277
+
# Call external provider
278
278
+
response := external_data({
279
279
+
"provider": provider,
280
280
+
"keys": ["image"],
281
281
+
"values": [container.image]
282
282
+
})
283
283
+
284
284
+
# Get response for this image
285
285
+
result := response[_]
286
286
+
result.image == container.image
287
287
+
result.verified == true
288
288
+
289
289
+
# Check DID is trusted
290
290
+
not result.did in trusted_dids
291
291
+
msg := sprintf("Image %v signed by untrusted DID: %v", [container.image, result.did])
292
292
+
}
293
293
+
```
294
294
+
295
295
+
### Namespace-Specific Policies
296
296
+
297
297
+
```rego
298
298
+
package atcrsignatures
299
299
+
300
300
+
import future.keywords.contains
301
301
+
import future.keywords.if
302
302
+
import future.keywords.in
303
303
+
304
304
+
provider := "atcr-verifier"
305
305
+
306
306
+
# Production namespaces require signatures
307
307
+
production_namespaces := ["production", "prod", "staging"]
308
308
+
309
309
+
violation[{"msg": msg}] {
310
310
+
# Only apply to production namespaces
311
311
+
input.review.object.metadata.namespace in production_namespaces
312
312
+
313
313
+
container := input.review.object.spec.containers[_]
314
314
+
startswith(container.image, "atcr.io/")
315
315
+
316
316
+
# Call external provider
317
317
+
response := external_data({
318
318
+
"provider": provider,
319
319
+
"keys": ["image"],
320
320
+
"values": [container.image]
321
321
+
})
322
322
+
323
323
+
# Check verification result
324
324
+
not response[_].verified == true
325
325
+
326
326
+
msg := sprintf("Production namespace requires signed images. Image %v is not signed", [container.image])
327
327
+
}
328
328
+
```
329
329
+
330
330
+
## Performance Considerations
331
331
+
332
332
+
### Caching
333
333
+
334
334
+
The provider caches:
335
335
+
- Signature verification results (TTL: 5 minutes)
336
336
+
- DID documents (TTL: 5 minutes)
337
337
+
- PDS endpoints (TTL: 5 minutes)
338
338
+
- Public keys (TTL: 5 minutes)
339
339
+
340
340
+
Enable/disable via `CACHE_ENABLED` environment variable.
341
341
+
342
342
+
### Timeouts
343
343
+
344
344
+
- `DID_RESOLVER_TIMEOUT` - DID resolution timeout (default: 10s)
345
345
+
- `PDS_TIMEOUT` - PDS XRPC calls timeout (default: 10s)
346
346
+
- HTTP client timeout: 30s total
347
347
+
348
348
+
### Horizontal Scaling
349
349
+
350
350
+
The provider is stateless and can be scaled horizontally:
351
351
+
352
352
+
```yaml
353
353
+
apiVersion: apps/v1
354
354
+
kind: Deployment
355
355
+
spec:
356
356
+
replicas: 3 # Scale up for high traffic
357
357
+
```
358
358
+
359
359
+
### Rate Limiting
360
360
+
361
361
+
Consider implementing rate limiting for:
362
362
+
- Gatekeeper → Provider requests
363
363
+
- Provider → DID resolver
364
364
+
- Provider → PDS
365
365
+
366
366
+
## Monitoring
367
367
+
368
368
+
### Metrics
369
369
+
370
370
+
The provider exposes Prometheus metrics at `/metrics`:
371
371
+
372
372
+
```
373
373
+
# Request metrics
374
374
+
atcr_provider_requests_total{status="success|failure"}
375
375
+
atcr_provider_request_duration_seconds
376
376
+
377
377
+
# Verification metrics
378
378
+
atcr_provider_verifications_total{result="verified|failed|error"}
379
379
+
atcr_provider_verification_duration_seconds
380
380
+
381
381
+
# Cache metrics
382
382
+
atcr_provider_cache_hits_total
383
383
+
atcr_provider_cache_misses_total
384
384
+
```
385
385
+
386
386
+
### Logging
387
387
+
388
388
+
Structured JSON logging with fields:
389
389
+
- `image` - Image being verified
390
390
+
- `did` - Signer DID (if found)
391
391
+
- `duration` - Verification duration
392
392
+
- `error` - Error message (if failed)
393
393
+
394
394
+
### Health Checks
395
395
+
396
396
+
```bash
397
397
+
# Liveness probe
398
398
+
curl http://localhost:8080/health
399
399
+
400
400
+
# Readiness probe
401
401
+
curl http://localhost:8080/ready
402
402
+
```
403
403
+
404
404
+
## Troubleshooting
405
405
+
406
406
+
### Provider Not Reachable
407
407
+
408
408
+
```bash
409
409
+
# Check provider pod status
410
410
+
kubectl get pods -n gatekeeper-system -l app=atcr-provider
411
411
+
412
412
+
# Check service
413
413
+
kubectl get svc -n gatekeeper-system atcr-provider
414
414
+
415
415
+
# Test connectivity from Gatekeeper pod
416
416
+
kubectl exec -n gatekeeper-system deployment/gatekeeper-controller-manager -- \
417
417
+
curl http://atcr-provider.gatekeeper-system/health
418
418
+
```
419
419
+
420
420
+
### Verification Failing
421
421
+
422
422
+
```bash
423
423
+
# Check provider logs
424
424
+
kubectl logs -n gatekeeper-system deployment/atcr-provider
425
425
+
426
426
+
# Test verification manually
427
427
+
kubectl run test-curl --rm -it --image=curlimages/curl -- \
428
428
+
curl -X POST http://atcr-provider.gatekeeper-system/provide \
429
429
+
-H "Content-Type: application/json" \
430
430
+
-d '{"keys":["image"],"values":["atcr.io/alice/myapp:latest"]}'
431
431
+
```
432
432
+
433
433
+
### Policy Not Enforcing
434
434
+
435
435
+
```bash
436
436
+
# Check Gatekeeper logs
437
437
+
kubectl logs -n gatekeeper-system deployment/gatekeeper-controller-manager
438
438
+
439
439
+
# Check constraint status
440
440
+
kubectl get constraint atcr-signatures-required -o yaml
441
441
+
442
442
+
# Test policy manually with conftest
443
443
+
conftest test -p constraint-template.yaml pod.yaml
444
444
+
```
445
445
+
446
446
+
## Security Considerations
447
447
+
448
448
+
### Network Policies
449
449
+
450
450
+
Restrict network access:
451
451
+
452
452
+
```yaml
453
453
+
apiVersion: networking.k8s.io/v1
454
454
+
kind: NetworkPolicy
455
455
+
metadata:
456
456
+
name: atcr-provider
457
457
+
namespace: gatekeeper-system
458
458
+
spec:
459
459
+
podSelector:
460
460
+
matchLabels:
461
461
+
app: atcr-provider
462
462
+
ingress:
463
463
+
- from:
464
464
+
- podSelector:
465
465
+
matchLabels:
466
466
+
control-plane: controller-manager # Gatekeeper
467
467
+
ports:
468
468
+
- port: 8080
469
469
+
egress:
470
470
+
- to: # PLC directory
471
471
+
- namespaceSelector: {}
472
472
+
ports:
473
473
+
- port: 443
474
474
+
```
475
475
+
476
476
+
### Authentication
477
477
+
478
478
+
The provider should only be accessible from Gatekeeper. Options:
479
479
+
- Network policies (recommended for Kubernetes)
480
480
+
- Mutual TLS
481
481
+
- API tokens
482
482
+
483
483
+
### Trust Policy Management
484
484
+
485
485
+
- Store trust policy in version control
486
486
+
- Use GitOps (Flux, ArgoCD) for updates
487
487
+
- Review DID changes carefully
488
488
+
- Audit policy modifications
489
489
+
490
490
+
## See Also
491
491
+
492
492
+
- [Gatekeeper Documentation](https://open-policy-agent.github.io/gatekeeper/)
493
493
+
- [External Data Provider](https://open-policy-agent.github.io/gatekeeper/website/docs/externaldata/)
494
494
+
- [ATCR Signature Integration](../../../docs/SIGNATURE_INTEGRATION.md)
495
495
+
- [ATCR Integration Strategy](../../../docs/INTEGRATION_STRATEGY.md)
496
496
+
497
497
+
## Support
498
498
+
499
499
+
For issues or questions:
500
500
+
- GitHub Issues: https://github.com/atcr-io/atcr/issues
501
501
+
- Gatekeeper GitHub: https://github.com/open-policy-agent/gatekeeper
+225
examples/plugins/gatekeeper-provider/main.go
···
1
1
+
// Package main implements an OPA Gatekeeper External Data Provider for ATProto signature verification.
2
2
+
package main
3
3
+
4
4
+
import (
5
5
+
"context"
6
6
+
"encoding/json"
7
7
+
"fmt"
8
8
+
"log"
9
9
+
"net/http"
10
10
+
"os"
11
11
+
"time"
12
12
+
)
13
13
+
14
14
+
const (
15
15
+
// DefaultPort is the default HTTP port
16
16
+
DefaultPort = "8080"
17
17
+
18
18
+
// DefaultTrustPolicyPath is the default trust policy file path
19
19
+
DefaultTrustPolicyPath = "/config/trust-policy.yaml"
20
20
+
)
21
21
+
22
22
+
// Server is the HTTP server for the external data provider.
23
23
+
type Server struct {
24
24
+
verifier *Verifier
25
25
+
port string
26
26
+
httpServer *http.Server
27
27
+
}
28
28
+
29
29
+
// ProviderRequest is the request format from Gatekeeper.
30
30
+
type ProviderRequest struct {
31
31
+
Keys []string `json:"keys"`
32
32
+
Values []string `json:"values"`
33
33
+
}
34
34
+
35
35
+
// ProviderResponse is the response format to Gatekeeper.
36
36
+
type ProviderResponse struct {
37
37
+
SystemError string `json:"system_error,omitempty"`
38
38
+
Responses []map[string]interface{} `json:"responses"`
39
39
+
}
40
40
+
41
41
+
// VerificationResult holds the result of verifying a single image.
42
42
+
type VerificationResult struct {
43
43
+
Image string `json:"image"`
44
44
+
Verified bool `json:"verified"`
45
45
+
DID string `json:"did,omitempty"`
46
46
+
Handle string `json:"handle,omitempty"`
47
47
+
SignedAt time.Time `json:"signedAt,omitempty"`
48
48
+
CommitCID string `json:"commitCid,omitempty"`
49
49
+
Error string `json:"error,omitempty"`
50
50
+
}
51
51
+
52
52
+
// NewServer creates a new provider server.
53
53
+
func NewServer(verifier *Verifier, port string) *Server {
54
54
+
return &Server{
55
55
+
verifier: verifier,
56
56
+
port: port,
57
57
+
}
58
58
+
}
59
59
+
60
60
+
// Start starts the HTTP server.
61
61
+
func (s *Server) Start() error {
62
62
+
mux := http.NewServeMux()
63
63
+
64
64
+
// Provider endpoint (called by Gatekeeper)
65
65
+
mux.HandleFunc("/provide", s.handleProvide)
66
66
+
67
67
+
// Health check endpoints
68
68
+
mux.HandleFunc("/health", s.handleHealth)
69
69
+
mux.HandleFunc("/ready", s.handleReady)
70
70
+
71
71
+
// Metrics endpoint (Prometheus)
72
72
+
// TODO: Implement metrics
73
73
+
// mux.HandleFunc("/metrics", s.handleMetrics)
74
74
+
75
75
+
s.httpServer = &http.Server{
76
76
+
Addr: ":" + s.port,
77
77
+
Handler: mux,
78
78
+
ReadTimeout: 10 * time.Second,
79
79
+
WriteTimeout: 30 * time.Second,
80
80
+
IdleTimeout: 60 * time.Second,
81
81
+
}
82
82
+
83
83
+
log.Printf("Starting ATProto signature verification provider on port %s", s.port)
84
84
+
return s.httpServer.ListenAndServe()
85
85
+
}
86
86
+
87
87
+
// Stop gracefully stops the HTTP server.
88
88
+
func (s *Server) Stop(ctx context.Context) error {
89
89
+
if s.httpServer != nil {
90
90
+
return s.httpServer.Shutdown(ctx)
91
91
+
}
92
92
+
return nil
93
93
+
}
94
94
+
95
95
+
// handleProvide handles the provider endpoint called by Gatekeeper.
96
96
+
func (s *Server) handleProvide(w http.ResponseWriter, r *http.Request) {
97
97
+
if r.Method != http.MethodPost {
98
98
+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
99
99
+
return
100
100
+
}
101
101
+
102
102
+
// Parse request
103
103
+
var req ProviderRequest
104
104
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
105
105
+
log.Printf("ERROR: failed to parse request: %v", err)
106
106
+
http.Error(w, fmt.Sprintf("invalid request: %v", err), http.StatusBadRequest)
107
107
+
return
108
108
+
}
109
109
+
110
110
+
log.Printf("INFO: received verification request for %d images", len(req.Values))
111
111
+
112
112
+
// Verify each image
113
113
+
responses := make([]map[string]interface{}, 0, len(req.Values))
114
114
+
for _, image := range req.Values {
115
115
+
result := s.verifyImage(r.Context(), image)
116
116
+
responses = append(responses, structToMap(result))
117
117
+
}
118
118
+
119
119
+
// Send response
120
120
+
resp := ProviderResponse{
121
121
+
Responses: responses,
122
122
+
}
123
123
+
124
124
+
w.Header().Set("Content-Type", "application/json")
125
125
+
if err := json.NewEncoder(w).Encode(resp); err != nil {
126
126
+
log.Printf("ERROR: failed to encode response: %v", err)
127
127
+
}
128
128
+
}
129
129
+
130
130
+
// verifyImage verifies a single image.
131
131
+
func (s *Server) verifyImage(ctx context.Context, image string) VerificationResult {
132
132
+
start := time.Now()
133
133
+
log.Printf("INFO: verifying image: %s", image)
134
134
+
135
135
+
// Call verifier
136
136
+
verified, metadata, err := s.verifier.Verify(ctx, image)
137
137
+
duration := time.Since(start)
138
138
+
139
139
+
if err != nil {
140
140
+
log.Printf("ERROR: verification failed for %s: %v (duration: %v)", image, err, duration)
141
141
+
return VerificationResult{
142
142
+
Image: image,
143
143
+
Verified: false,
144
144
+
Error: err.Error(),
145
145
+
}
146
146
+
}
147
147
+
148
148
+
if !verified {
149
149
+
log.Printf("WARN: image %s failed verification (duration: %v)", image, duration)
150
150
+
return VerificationResult{
151
151
+
Image: image,
152
152
+
Verified: false,
153
153
+
Error: "signature verification failed",
154
154
+
}
155
155
+
}
156
156
+
157
157
+
log.Printf("INFO: image %s verified successfully (DID: %s, duration: %v)",
158
158
+
image, metadata.DID, duration)
159
159
+
160
160
+
return VerificationResult{
161
161
+
Image: image,
162
162
+
Verified: true,
163
163
+
DID: metadata.DID,
164
164
+
Handle: metadata.Handle,
165
165
+
SignedAt: metadata.SignedAt,
166
166
+
CommitCID: metadata.CommitCID,
167
167
+
}
168
168
+
}
169
169
+
170
170
+
// handleHealth handles health check requests.
171
171
+
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
172
172
+
w.Header().Set("Content-Type", "application/json")
173
173
+
json.NewEncoder(w).Encode(map[string]string{
174
174
+
"status": "ok",
175
175
+
"version": "1.0.0",
176
176
+
})
177
177
+
}
178
178
+
179
179
+
// handleReady handles readiness check requests.
180
180
+
func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
181
181
+
// TODO: Check dependencies (DID resolver, PDS connectivity)
182
182
+
w.Header().Set("Content-Type", "application/json")
183
183
+
json.NewEncoder(w).Encode(map[string]string{
184
184
+
"status": "ready",
185
185
+
})
186
186
+
}
187
187
+
188
188
+
// structToMap converts a struct to a map for JSON encoding.
189
189
+
func structToMap(v interface{}) map[string]interface{} {
190
190
+
data, _ := json.Marshal(v)
191
191
+
var m map[string]interface{}
192
192
+
json.Unmarshal(data, &m)
193
193
+
return m
194
194
+
}
195
195
+
196
196
+
func main() {
197
197
+
// Load configuration
198
198
+
port := os.Getenv("HTTP_PORT")
199
199
+
if port == "" {
200
200
+
port = DefaultPort
201
201
+
}
202
202
+
203
203
+
trustPolicyPath := os.Getenv("TRUST_POLICY_PATH")
204
204
+
if trustPolicyPath == "" {
205
205
+
trustPolicyPath = DefaultTrustPolicyPath
206
206
+
}
207
207
+
208
208
+
// Create verifier
209
209
+
verifier, err := NewVerifier(trustPolicyPath)
210
210
+
if err != nil {
211
211
+
log.Fatalf("FATAL: failed to create verifier: %v", err)
212
212
+
}
213
213
+
214
214
+
// Create server
215
215
+
server := NewServer(verifier, port)
216
216
+
217
217
+
// Start server
218
218
+
if err := server.Start(); err != nil && err != http.ErrServerClosed {
219
219
+
log.Fatalf("FATAL: server error: %v", err)
220
220
+
}
221
221
+
}
222
222
+
223
223
+
// TODO: Implement verifier.go with ATProto signature verification logic
224
224
+
// TODO: Implement resolver.go with DID resolution
225
225
+
// TODO: Implement crypto.go with K-256 signature verification
+304
examples/plugins/ratify-verifier/README.md
···
1
1
+
# Ratify ATProto Verifier Plugin
2
2
+
3
3
+
This is a reference implementation of a Ratify verifier plugin for ATProto signatures.
4
4
+
5
5
+
## Overview
6
6
+
7
7
+
Ratify is a verification framework that integrates with OPA Gatekeeper to enforce signature policies in Kubernetes. This plugin adds support for verifying ATProto signatures on ATCR container images.
8
8
+
9
9
+
## Architecture
10
10
+
11
11
+
```
12
12
+
Kubernetes Pod Creation
13
13
+
↓
14
14
+
OPA Gatekeeper (admission webhook)
15
15
+
↓
16
16
+
Ratify (verification engine)
17
17
+
↓
18
18
+
ATProto Verifier Plugin ← This plugin
19
19
+
↓
20
20
+
1. Fetch signature artifact from registry
21
21
+
2. Parse ATProto signature metadata
22
22
+
3. Resolve DID to public key
23
23
+
4. Fetch repository commit from PDS
24
24
+
5. Verify ECDSA K-256 signature
25
25
+
6. Check trust policy
26
26
+
↓
27
27
+
Return: Allow/Deny
28
28
+
```
29
29
+
30
30
+
## Files
31
31
+
32
32
+
- `verifier.go` - Main verifier implementation
33
33
+
- `config.go` - Configuration and trust policy
34
34
+
- `resolver.go` - DID and PDS resolution
35
35
+
- `crypto.go` - K-256 signature verification
36
36
+
- `Dockerfile` - Build custom Ratify image with plugin
37
37
+
- `deployment.yaml` - Kubernetes deployment manifest
38
38
+
- `verifier-crd.yaml` - Ratify Verifier custom resource
39
39
+
40
40
+
## Prerequisites
41
41
+
42
42
+
- Go 1.21+
43
43
+
- Ratify source code (for building plugin)
44
44
+
- Kubernetes cluster with OPA Gatekeeper installed
45
45
+
- Access to ATCR registry
46
46
+
47
47
+
## Building
48
48
+
49
49
+
```bash
50
50
+
# Clone Ratify
51
51
+
git clone https://github.com/ratify-project/ratify.git
52
52
+
cd ratify
53
53
+
54
54
+
# Copy plugin files
55
55
+
cp -r /path/to/examples/plugins/ratify-verifier plugins/verifier/atproto/
56
56
+
57
57
+
# Build plugin
58
58
+
CGO_ENABLED=0 go build -o atproto-verifier \
59
59
+
-ldflags="-w -s" \
60
60
+
./plugins/verifier/atproto
61
61
+
62
62
+
# Build custom Ratify image with plugin
63
63
+
docker build -f Dockerfile.with-atproto -t atcr.io/atcr/ratify-with-atproto:latest .
64
64
+
```
65
65
+
66
66
+
## Deployment
67
67
+
68
68
+
### 1. Deploy Ratify with Plugin
69
69
+
70
70
+
```bash
71
71
+
# Push custom image
72
72
+
docker push atcr.io/atcr/ratify-with-atproto:latest
73
73
+
74
74
+
# Deploy Ratify
75
75
+
kubectl apply -f deployment.yaml
76
76
+
```
77
77
+
78
78
+
### 2. Configure Verifier
79
79
+
80
80
+
```bash
81
81
+
# Create Verifier custom resource
82
82
+
kubectl apply -f verifier-crd.yaml
83
83
+
```
84
84
+
85
85
+
### 3. Configure Trust Policy
86
86
+
87
87
+
```bash
88
88
+
# Create ConfigMap with trust policy
89
89
+
kubectl create configmap atcr-trust-policy \
90
90
+
--from-file=trust-policy.yaml \
91
91
+
-n gatekeeper-system
92
92
+
```
93
93
+
94
94
+
### 4. Create Gatekeeper Constraint
95
95
+
96
96
+
```bash
97
97
+
kubectl apply -f constraint.yaml
98
98
+
```
99
99
+
100
100
+
### 5. Test
101
101
+
102
102
+
```bash
103
103
+
# Try to create pod with signed image (should succeed)
104
104
+
kubectl run test-signed --image=atcr.io/alice/myapp:latest
105
105
+
106
106
+
# Try to create pod with unsigned image (should fail)
107
107
+
kubectl run test-unsigned --image=atcr.io/malicious/fake:latest
108
108
+
```
109
109
+
110
110
+
## Configuration
111
111
+
112
112
+
### Trust Policy Format
113
113
+
114
114
+
```yaml
115
115
+
# trust-policy.yaml
116
116
+
version: 1.0
117
117
+
118
118
+
trustedDIDs:
119
119
+
did:plc:alice123:
120
120
+
name: "Alice (DevOps)"
121
121
+
validFrom: "2024-01-01T00:00:00Z"
122
122
+
expiresAt: null
123
123
+
124
124
+
did:plc:bob456:
125
125
+
name: "Bob (Security)"
126
126
+
validFrom: "2024-06-01T00:00:00Z"
127
127
+
expiresAt: "2025-12-31T23:59:59Z"
128
128
+
129
129
+
policies:
130
130
+
- name: production
131
131
+
scope: "atcr.io/*/prod-*"
132
132
+
require:
133
133
+
signature: true
134
134
+
trustedDIDs:
135
135
+
- did:plc:alice123
136
136
+
- did:plc:bob456
137
137
+
action: enforce
138
138
+
```
139
139
+
140
140
+
### Verifier Configuration
141
141
+
142
142
+
```yaml
143
143
+
apiVersion: config.ratify.deislabs.io/v1beta1
144
144
+
kind: Verifier
145
145
+
metadata:
146
146
+
name: atproto-verifier
147
147
+
spec:
148
148
+
name: atproto
149
149
+
artifactType: application/vnd.atproto.signature.v1+json
150
150
+
address: /.ratify/plugins/atproto-verifier
151
151
+
parameters:
152
152
+
trustPolicyPath: /config/trust-policy.yaml
153
153
+
didResolverTimeout: 10s
154
154
+
pdsTimeout: 10s
155
155
+
cacheEnabled: true
156
156
+
cacheTTL: 300s
157
157
+
```
158
158
+
159
159
+
## Implementation Details
160
160
+
161
161
+
### Verifier Interface
162
162
+
163
163
+
The plugin implements Ratify's `ReferenceVerifier` interface:
164
164
+
165
165
+
```go
166
166
+
type ReferenceVerifier interface {
167
167
+
Name() string
168
168
+
Type() string
169
169
+
CanVerify(artifactType string) bool
170
170
+
VerifyReference(
171
171
+
ctx context.Context,
172
172
+
subjectRef common.Reference,
173
173
+
referenceDesc ocispecs.ReferenceDescriptor,
174
174
+
store referrerstore.ReferrerStore,
175
175
+
) (VerifierResult, error)
176
176
+
}
177
177
+
```
178
178
+
179
179
+
### Verification Flow
180
180
+
181
181
+
1. **Artifact Fetch**: Download signature artifact from registry via Ratify's store
182
182
+
2. **Parse Metadata**: Extract ATProto signature metadata (DID, PDS, commit CID)
183
183
+
3. **DID Resolution**: Resolve DID to public key via PLC directory or did:web
184
184
+
4. **Commit Fetch**: Get repository commit from PDS via XRPC
185
185
+
5. **Signature Verify**: Verify ECDSA K-256 signature over commit bytes
186
186
+
6. **Trust Check**: Validate DID against trust policy
187
187
+
7. **Result**: Return success/failure with metadata
188
188
+
189
189
+
### Error Handling
190
190
+
191
191
+
The plugin returns detailed error information:
192
192
+
193
193
+
```go
194
194
+
type VerifierResult struct {
195
195
+
IsSuccess bool
196
196
+
Name string
197
197
+
Type string
198
198
+
Message string
199
199
+
Extensions map[string]interface{}
200
200
+
}
201
201
+
```
202
202
+
203
203
+
**Extensions include:**
204
204
+
- `did` - Signer's DID
205
205
+
- `handle` - Signer's handle (if available)
206
206
+
- `signedAt` - Signature timestamp
207
207
+
- `commitCid` - ATProto commit CID
208
208
+
- `pdsEndpoint` - PDS URL
209
209
+
- `error` - Error details (if verification failed)
210
210
+
211
211
+
## Troubleshooting
212
212
+
213
213
+
### Plugin Not Found
214
214
+
215
215
+
```bash
216
216
+
# Check plugin is in image
217
217
+
kubectl exec -n gatekeeper-system deployment/ratify -c ratify -- ls -la /.ratify/plugins/
218
218
+
219
219
+
# Check logs
220
220
+
kubectl logs -n gatekeeper-system deployment/ratify -c ratify
221
221
+
```
222
222
+
223
223
+
### Verification Failing
224
224
+
225
225
+
```bash
226
226
+
# Check Ratify logs for details
227
227
+
kubectl logs -n gatekeeper-system deployment/ratify -c ratify | grep atproto
228
228
+
229
229
+
# Check Verifier status
230
230
+
kubectl get verifier atproto-verifier -o yaml
231
231
+
232
232
+
# Test DID resolution manually
233
233
+
curl https://plc.directory/did:plc:alice123
234
234
+
```
235
235
+
236
236
+
### Trust Policy Issues
237
237
+
238
238
+
```bash
239
239
+
# Check ConfigMap exists
240
240
+
kubectl get configmap atcr-trust-policy -n gatekeeper-system
241
241
+
242
242
+
# View policy contents
243
243
+
kubectl get configmap atcr-trust-policy -n gatekeeper-system -o yaml
244
244
+
```
245
245
+
246
246
+
## Performance Considerations
247
247
+
248
248
+
### Caching
249
249
+
250
250
+
The plugin caches:
251
251
+
- DID documents (TTL: 5 minutes)
252
252
+
- PDS endpoints (TTL: 5 minutes)
253
253
+
- Public keys (TTL: 5 minutes)
254
254
+
255
255
+
Configure via `cacheEnabled` and `cacheTTL` parameters.
256
256
+
257
257
+
### Timeouts
258
258
+
259
259
+
Configure timeouts for external calls:
260
260
+
- `didResolverTimeout` - DID resolution (default: 10s)
261
261
+
- `pdsTimeout` - PDS XRPC calls (default: 10s)
262
262
+
263
263
+
### Rate Limiting
264
264
+
265
265
+
Consider implementing rate limiting for:
266
266
+
- DID resolution (PLC directory)
267
267
+
- PDS XRPC calls
268
268
+
- Signature verification
269
269
+
270
270
+
## Security Considerations
271
271
+
272
272
+
### Trust Policy Management
273
273
+
274
274
+
- Store trust policy in version control
275
275
+
- Review DID additions/removals carefully
276
276
+
- Set expiration dates for temporary access
277
277
+
- Audit trust policy changes
278
278
+
279
279
+
### Private Key Protection
280
280
+
281
281
+
- Plugin only uses public keys
282
282
+
- No private keys needed for verification
283
283
+
- DID resolution is read-only
284
284
+
- PDS queries are read-only
285
285
+
286
286
+
### Denial of Service
287
287
+
288
288
+
- Implement timeouts for all external calls
289
289
+
- Cache DID documents to reduce load
290
290
+
- Rate limit verification requests
291
291
+
- Monitor verification latency
292
292
+
293
293
+
## See Also
294
294
+
295
295
+
- [Ratify Documentation](https://ratify.dev/)
296
296
+
- [Ratify Plugin Development](https://ratify.dev/docs/plugins/verifier/overview)
297
297
+
- [ATCR Signature Integration](../../../docs/SIGNATURE_INTEGRATION.md)
298
298
+
- [ATCR Integration Strategy](../../../docs/INTEGRATION_STRATEGY.md)
299
299
+
300
300
+
## Support
301
301
+
302
302
+
For issues or questions:
303
303
+
- GitHub Issues: https://github.com/atcr-io/atcr/issues
304
304
+
- Ratify GitHub: https://github.com/ratify-project/ratify
+214
examples/plugins/ratify-verifier/verifier.go
···
1
1
+
// Package atproto implements a Ratify verifier plugin for ATProto signatures.
2
2
+
package atproto
3
3
+
4
4
+
import (
5
5
+
"context"
6
6
+
"encoding/json"
7
7
+
"fmt"
8
8
+
"time"
9
9
+
10
10
+
"github.com/ratify-project/ratify/pkg/common"
11
11
+
"github.com/ratify-project/ratify/pkg/ocispecs"
12
12
+
"github.com/ratify-project/ratify/pkg/referrerstore"
13
13
+
"github.com/ratify-project/ratify/pkg/verifier"
14
14
+
)
15
15
+
16
16
+
const (
17
17
+
// VerifierName is the name of this verifier
18
18
+
VerifierName = "atproto"
19
19
+
20
20
+
// VerifierType is the type of this verifier
21
21
+
VerifierType = "atproto"
22
22
+
23
23
+
// ATProtoSignatureArtifactType is the OCI artifact type for ATProto signatures
24
24
+
ATProtoSignatureArtifactType = "application/vnd.atproto.signature.v1+json"
25
25
+
)
26
26
+
27
27
+
// ATProtoVerifier implements the Ratify ReferenceVerifier interface for ATProto signatures.
28
28
+
type ATProtoVerifier struct {
29
29
+
name string
30
30
+
config ATProtoConfig
31
31
+
resolver *Resolver
32
32
+
verifier *SignatureVerifier
33
33
+
trustStore *TrustStore
34
34
+
}
35
35
+
36
36
+
// ATProtoConfig holds configuration for the ATProto verifier.
37
37
+
type ATProtoConfig struct {
38
38
+
// TrustPolicyPath is the path to the trust policy YAML file
39
39
+
TrustPolicyPath string `json:"trustPolicyPath"`
40
40
+
41
41
+
// DIDResolverTimeout is the timeout for DID resolution
42
42
+
DIDResolverTimeout time.Duration `json:"didResolverTimeout"`
43
43
+
44
44
+
// PDSTimeout is the timeout for PDS XRPC calls
45
45
+
PDSTimeout time.Duration `json:"pdsTimeout"`
46
46
+
47
47
+
// CacheEnabled enables caching of DID documents and public keys
48
48
+
CacheEnabled bool `json:"cacheEnabled"`
49
49
+
50
50
+
// CacheTTL is the cache TTL for DID documents and public keys
51
51
+
CacheTTL time.Duration `json:"cacheTTL"`
52
52
+
}
53
53
+
54
54
+
// ATProtoSignature represents the ATProto signature metadata stored in the OCI artifact.
55
55
+
type ATProtoSignature struct {
56
56
+
Type string `json:"$type"`
57
57
+
Version string `json:"version"`
58
58
+
Subject struct {
59
59
+
Digest string `json:"digest"`
60
60
+
MediaType string `json:"mediaType"`
61
61
+
} `json:"subject"`
62
62
+
ATProto struct {
63
63
+
DID string `json:"did"`
64
64
+
Handle string `json:"handle"`
65
65
+
PDSEndpoint string `json:"pdsEndpoint"`
66
66
+
RecordURI string `json:"recordUri"`
67
67
+
CommitCID string `json:"commitCid"`
68
68
+
SignedAt time.Time `json:"signedAt"`
69
69
+
} `json:"atproto"`
70
70
+
Signature struct {
71
71
+
Algorithm string `json:"algorithm"`
72
72
+
KeyID string `json:"keyId"`
73
73
+
PublicKeyMultibase string `json:"publicKeyMultibase"`
74
74
+
} `json:"signature"`
75
75
+
}
76
76
+
77
77
+
// NewATProtoVerifier creates a new ATProto verifier instance.
78
78
+
func NewATProtoVerifier(name string, config ATProtoConfig) (*ATProtoVerifier, error) {
79
79
+
// Load trust policy
80
80
+
trustStore, err := LoadTrustStore(config.TrustPolicyPath)
81
81
+
if err != nil {
82
82
+
return nil, fmt.Errorf("failed to load trust policy: %w", err)
83
83
+
}
84
84
+
85
85
+
// Create resolver with caching
86
86
+
resolver := NewResolver(config.DIDResolverTimeout, config.CacheEnabled, config.CacheTTL)
87
87
+
88
88
+
// Create signature verifier
89
89
+
verifier := NewSignatureVerifier(config.PDSTimeout)
90
90
+
91
91
+
return &ATProtoVerifier{
92
92
+
name: name,
93
93
+
config: config,
94
94
+
resolver: resolver,
95
95
+
verifier: verifier,
96
96
+
trustStore: trustStore,
97
97
+
}, nil
98
98
+
}
99
99
+
100
100
+
// Name returns the name of this verifier.
101
101
+
func (v *ATProtoVerifier) Name() string {
102
102
+
return v.name
103
103
+
}
104
104
+
105
105
+
// Type returns the type of this verifier.
106
106
+
func (v *ATProtoVerifier) Type() string {
107
107
+
return VerifierType
108
108
+
}
109
109
+
110
110
+
// CanVerify returns true if this verifier can verify the given artifact type.
111
111
+
func (v *ATProtoVerifier) CanVerify(artifactType string) bool {
112
112
+
return artifactType == ATProtoSignatureArtifactType
113
113
+
}
114
114
+
115
115
+
// VerifyReference verifies an ATProto signature artifact.
116
116
+
func (v *ATProtoVerifier) VerifyReference(
117
117
+
ctx context.Context,
118
118
+
subjectRef common.Reference,
119
119
+
referenceDesc ocispecs.ReferenceDescriptor,
120
120
+
store referrerstore.ReferrerStore,
121
121
+
) (verifier.VerifierResult, error) {
122
122
+
// 1. Fetch signature blob from store
123
123
+
sigBlob, err := store.GetBlobContent(ctx, subjectRef, referenceDesc.Digest)
124
124
+
if err != nil {
125
125
+
return v.failureResult(fmt.Sprintf("failed to fetch signature blob: %v", err)), err
126
126
+
}
127
127
+
128
128
+
// 2. Parse ATProto signature metadata
129
129
+
var sigData ATProtoSignature
130
130
+
if err := json.Unmarshal(sigBlob, &sigData); err != nil {
131
131
+
return v.failureResult(fmt.Sprintf("failed to parse signature metadata: %v", err)), err
132
132
+
}
133
133
+
134
134
+
// Validate signature format
135
135
+
if err := v.validateSignature(&sigData); err != nil {
136
136
+
return v.failureResult(fmt.Sprintf("invalid signature format: %v", err)), err
137
137
+
}
138
138
+
139
139
+
// 3. Check trust policy first (fail fast if DID not trusted)
140
140
+
if !v.trustStore.IsTrusted(sigData.ATProto.DID, time.Now()) {
141
141
+
return v.failureResult(fmt.Sprintf("DID %s not in trusted list", sigData.ATProto.DID)),
142
142
+
fmt.Errorf("untrusted DID")
143
143
+
}
144
144
+
145
145
+
// 4. Resolve DID to public key
146
146
+
pubKey, err := v.resolver.ResolveDIDToPublicKey(ctx, sigData.ATProto.DID)
147
147
+
if err != nil {
148
148
+
return v.failureResult(fmt.Sprintf("failed to resolve DID: %v", err)), err
149
149
+
}
150
150
+
151
151
+
// 5. Fetch repository commit from PDS
152
152
+
commit, err := v.verifier.FetchCommit(ctx, sigData.ATProto.PDSEndpoint,
153
153
+
sigData.ATProto.DID, sigData.ATProto.CommitCID)
154
154
+
if err != nil {
155
155
+
return v.failureResult(fmt.Sprintf("failed to fetch commit: %v", err)), err
156
156
+
}
157
157
+
158
158
+
// 6. Verify K-256 signature
159
159
+
if err := v.verifier.VerifySignature(pubKey, commit); err != nil {
160
160
+
return v.failureResult(fmt.Sprintf("signature verification failed: %v", err)), err
161
161
+
}
162
162
+
163
163
+
// 7. Success - return detailed result
164
164
+
return verifier.VerifierResult{
165
165
+
IsSuccess: true,
166
166
+
Name: v.name,
167
167
+
Type: v.Type(),
168
168
+
Message: fmt.Sprintf("Successfully verified ATProto signature for DID %s", sigData.ATProto.DID),
169
169
+
Extensions: map[string]interface{}{
170
170
+
"did": sigData.ATProto.DID,
171
171
+
"handle": sigData.ATProto.Handle,
172
172
+
"signedAt": sigData.ATProto.SignedAt,
173
173
+
"commitCid": sigData.ATProto.CommitCID,
174
174
+
"pdsEndpoint": sigData.ATProto.PDSEndpoint,
175
175
+
},
176
176
+
}, nil
177
177
+
}
178
178
+
179
179
+
// validateSignature validates the signature metadata format.
180
180
+
func (v *ATProtoVerifier) validateSignature(sig *ATProtoSignature) error {
181
181
+
if sig.Type != "io.atcr.atproto.signature" {
182
182
+
return fmt.Errorf("invalid signature type: %s", sig.Type)
183
183
+
}
184
184
+
if sig.ATProto.DID == "" {
185
185
+
return fmt.Errorf("missing DID")
186
186
+
}
187
187
+
if sig.ATProto.PDSEndpoint == "" {
188
188
+
return fmt.Errorf("missing PDS endpoint")
189
189
+
}
190
190
+
if sig.ATProto.CommitCID == "" {
191
191
+
return fmt.Errorf("missing commit CID")
192
192
+
}
193
193
+
if sig.Signature.Algorithm != "ECDSA-K256-SHA256" {
194
194
+
return fmt.Errorf("unsupported signature algorithm: %s", sig.Signature.Algorithm)
195
195
+
}
196
196
+
return nil
197
197
+
}
198
198
+
199
199
+
// failureResult creates a failure result with the given message.
200
200
+
func (v *ATProtoVerifier) failureResult(message string) verifier.VerifierResult {
201
201
+
return verifier.VerifierResult{
202
202
+
IsSuccess: false,
203
203
+
Name: v.name,
204
204
+
Type: v.Type(),
205
205
+
Message: message,
206
206
+
Extensions: map[string]interface{}{
207
207
+
"error": message,
208
208
+
},
209
209
+
}
210
210
+
}
211
211
+
212
212
+
// TODO: Implement resolver.go with DID resolution logic
213
213
+
// TODO: Implement crypto.go with K-256 signature verification
214
214
+
// TODO: Implement config.go with trust policy loading
+364
examples/verification/README.md
···
1
1
+
# ATProto Signature Verification Examples
2
2
+
3
3
+
This directory contains practical examples for verifying ATProto signatures on ATCR container images.
4
4
+
5
5
+
## Files
6
6
+
7
7
+
### Scripts
8
8
+
9
9
+
- **`atcr-verify.sh`** - Standalone signature verification script
10
10
+
- Verifies ATProto signatures using shell commands
11
11
+
- Requires: `curl`, `jq`, `crane`, `oras`
12
12
+
- Does everything except full cryptographic verification
13
13
+
- Use this until the `atcr-verify` CLI tool is built
14
14
+
15
15
+
- **`verify-and-pull.sh`** - Secure image pull wrapper
16
16
+
- Verifies signatures before pulling images
17
17
+
- Can be used as a `docker pull` replacement
18
18
+
- Configurable via environment variables
19
19
+
20
20
+
### Configuration
21
21
+
22
22
+
- **`trust-policy.yaml`** - Example trust policy configuration
23
23
+
- Defines which DIDs to trust
24
24
+
- Specifies policies for different image scopes
25
25
+
- Includes audit logging and reporting settings
26
26
+
27
27
+
- **`kubernetes-webhook.yaml`** - Kubernetes admission controller
28
28
+
- Validates signatures before pod creation
29
29
+
- Includes webhook deployment, service, and configuration
30
30
+
- Uses trust policy ConfigMap
31
31
+
32
32
+
## Quick Start
33
33
+
34
34
+
### 1. Verify an Image
35
35
+
36
36
+
```bash
37
37
+
# Make script executable
38
38
+
chmod +x atcr-verify.sh
39
39
+
40
40
+
# Verify an image
41
41
+
./atcr-verify.sh atcr.io/alice/myapp:latest
42
42
+
```
43
43
+
44
44
+
**Output:**
45
45
+
```
46
46
+
═══════════════════════════════════════════════════
47
47
+
ATProto Signature Verification
48
48
+
═══════════════════════════════════════════════════
49
49
+
Image: atcr.io/alice/myapp:latest
50
50
+
═══════════════════════════════════════════════════
51
51
+
52
52
+
[1/7] Resolving image digest...
53
53
+
→ sha256:abc123...
54
54
+
[2/7] Discovering ATProto signature artifacts...
55
55
+
→ Found 1 signature(s)
56
56
+
→ Signature digest: sha256:sig789...
57
57
+
→ Signed by DID: did:plc:alice123
58
58
+
[3/7] Fetching signature metadata...
59
59
+
→ DID: did:plc:alice123
60
60
+
→ Handle: alice.bsky.social
61
61
+
→ PDS: https://bsky.social
62
62
+
→ Record: at://did:plc:alice123/io.atcr.manifest/abc123
63
63
+
→ Signed at: 2025-10-31T12:34:56.789Z
64
64
+
[4/7] Resolving DID to public key...
65
65
+
→ Public key: zQ3shokFTS3brHcD...
66
66
+
[5/7] Querying PDS for signed record...
67
67
+
→ Record CID: bafyreig7...
68
68
+
[6/7] Verifying record integrity...
69
69
+
→ Record digest matches image digest
70
70
+
[7/7] Cryptographic signature verification...
71
71
+
⚠ Full cryptographic verification requires ATProto crypto library
72
72
+
73
73
+
═══════════════════════════════════════════════════
74
74
+
✓ Verification Completed
75
75
+
═══════════════════════════════════════════════════
76
76
+
77
77
+
Signed by: alice.bsky.social (did:plc:alice123)
78
78
+
Signed at: 2025-10-31T12:34:56.789Z
79
79
+
PDS: https://bsky.social
80
80
+
Record: at://did:plc:alice123/io.atcr.manifest/abc123
81
81
+
Signature: sha256:sig789...
82
82
+
83
83
+
═══════════════════════════════════════════════════
84
84
+
```
85
85
+
86
86
+
### 2. Secure Pull
87
87
+
88
88
+
```bash
89
89
+
# Make script executable
90
90
+
chmod +x verify-and-pull.sh
91
91
+
92
92
+
# Pull image with verification
93
93
+
./verify-and-pull.sh atcr.io/alice/myapp:latest
94
94
+
95
95
+
# With Docker options
96
96
+
./verify-and-pull.sh atcr.io/alice/myapp:latest --platform linux/amd64
97
97
+
```
98
98
+
99
99
+
**Create an alias for convenience:**
100
100
+
```bash
101
101
+
# Add to ~/.bashrc or ~/.zshrc
102
102
+
alias docker-pull-secure='/path/to/verify-and-pull.sh'
103
103
+
104
104
+
# Use it
105
105
+
docker-pull-secure atcr.io/alice/myapp:latest
106
106
+
```
107
107
+
108
108
+
### 3. Deploy Kubernetes Webhook
109
109
+
110
110
+
```bash
111
111
+
# 1. Generate TLS certificates for webhook
112
112
+
openssl req -x509 -newkey rsa:4096 -keyout tls.key -out tls.crt \
113
113
+
-days 365 -nodes -subj "/CN=atcr-verify-webhook.atcr-system.svc"
114
114
+
115
115
+
# 2. Create namespace and secret
116
116
+
kubectl create namespace atcr-system
117
117
+
kubectl create secret tls atcr-verify-webhook-certs \
118
118
+
--cert=tls.crt --key=tls.key -n atcr-system
119
119
+
120
120
+
# 3. Update CA bundle in kubernetes-webhook.yaml
121
121
+
cat tls.crt | base64 -w 0
122
122
+
# Copy output and replace caBundle in kubernetes-webhook.yaml
123
123
+
124
124
+
# 4. Deploy webhook
125
125
+
kubectl apply -f kubernetes-webhook.yaml
126
126
+
127
127
+
# 5. Enable verification for a namespace
128
128
+
kubectl label namespace production atcr-verify=enabled
129
129
+
130
130
+
# 6. Test with a pod
131
131
+
kubectl run test-pod --image=atcr.io/alice/myapp:latest -n production
132
132
+
```
133
133
+
134
134
+
## Prerequisites
135
135
+
136
136
+
### For Scripts
137
137
+
138
138
+
Install required tools:
139
139
+
140
140
+
**macOS (Homebrew):**
141
141
+
```bash
142
142
+
brew install curl jq crane oras
143
143
+
```
144
144
+
145
145
+
**Linux (apt):**
146
146
+
```bash
147
147
+
# curl and jq
148
148
+
sudo apt-get install curl jq
149
149
+
150
150
+
# crane
151
151
+
curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.15.2/go-containerregistry_Linux_x86_64.tar.gz" | tar -xz crane
152
152
+
sudo mv crane /usr/local/bin/
153
153
+
154
154
+
# oras
155
155
+
curl -LO "https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz"
156
156
+
tar -xzf oras_1.0.0_linux_amd64.tar.gz
157
157
+
sudo mv oras /usr/local/bin/
158
158
+
```
159
159
+
160
160
+
### For Kubernetes Webhook
161
161
+
162
162
+
Requirements:
163
163
+
- Kubernetes cluster (1.16+)
164
164
+
- `kubectl` configured
165
165
+
- Permission to create namespaces and webhooks
166
166
+
- Webhook container image (build from source or use pre-built)
167
167
+
168
168
+
## Configuration
169
169
+
170
170
+
### Environment Variables (verify-and-pull.sh)
171
171
+
172
172
+
- `VERIFY_SCRIPT` - Path to atcr-verify.sh (default: ./atcr-verify.sh)
173
173
+
- `TRUST_POLICY` - Path to trust policy (default: ./trust-policy.yaml)
174
174
+
- `REQUIRE_VERIFICATION` - Require verification (default: true)
175
175
+
- `SKIP_ATCR_IMAGES` - Skip verification for non-ATCR images (default: false)
176
176
+
177
177
+
**Example:**
178
178
+
```bash
179
179
+
# Skip verification for non-ATCR images
180
180
+
SKIP_ATCR_IMAGES=true ./verify-and-pull.sh docker.io/library/nginx:latest
181
181
+
182
182
+
# Allow pulling even if verification fails (NOT RECOMMENDED)
183
183
+
REQUIRE_VERIFICATION=false ./verify-and-pull.sh atcr.io/alice/myapp:latest
184
184
+
```
185
185
+
186
186
+
### Trust Policy
187
187
+
188
188
+
Edit `trust-policy.yaml` to customize:
189
189
+
190
190
+
1. **Add your DIDs:**
191
191
+
```yaml
192
192
+
trustedDIDs:
193
193
+
did:plc:your-did:
194
194
+
name: "Your Name"
195
195
+
validFrom: "2024-01-01T00:00:00Z"
196
196
+
```
197
197
+
198
198
+
2. **Define policies:**
199
199
+
```yaml
200
200
+
policies:
201
201
+
- name: my-policy
202
202
+
scope: "atcr.io/myorg/*"
203
203
+
require:
204
204
+
signature: true
205
205
+
trustedDIDs:
206
206
+
- did:plc:your-did
207
207
+
action: enforce
208
208
+
```
209
209
+
210
210
+
3. **Use with verification:**
211
211
+
```bash
212
212
+
# When atcr-verify CLI is available:
213
213
+
atcr-verify IMAGE --policy trust-policy.yaml
214
214
+
```
215
215
+
216
216
+
## Integration Patterns
217
217
+
218
218
+
### CI/CD (GitHub Actions)
219
219
+
220
220
+
```yaml
221
221
+
- name: Verify image signature
222
222
+
run: |
223
223
+
chmod +x examples/verification/atcr-verify.sh
224
224
+
./examples/verification/atcr-verify.sh ${{ env.IMAGE }}
225
225
+
226
226
+
- name: Deploy if verified
227
227
+
if: success()
228
228
+
run: kubectl set image deployment/app app=${{ env.IMAGE }}
229
229
+
```
230
230
+
231
231
+
### CI/CD (GitLab CI)
232
232
+
233
233
+
```yaml
234
234
+
verify:
235
235
+
script:
236
236
+
- chmod +x examples/verification/atcr-verify.sh
237
237
+
- ./examples/verification/atcr-verify.sh $IMAGE
238
238
+
239
239
+
deploy:
240
240
+
dependencies: [verify]
241
241
+
script:
242
242
+
- kubectl set image deployment/app app=$IMAGE
243
243
+
```
244
244
+
245
245
+
### Docker Alias
246
246
+
247
247
+
```bash
248
248
+
# ~/.bashrc or ~/.zshrc
249
249
+
function docker() {
250
250
+
if [ "$1" = "pull" ] && [[ "$2" =~ ^atcr\.io/ ]]; then
251
251
+
echo "Using secure pull with signature verification..."
252
252
+
/path/to/verify-and-pull.sh "${@:2}"
253
253
+
else
254
254
+
command docker "$@"
255
255
+
fi
256
256
+
}
257
257
+
```
258
258
+
259
259
+
### Systemd Service
260
260
+
261
261
+
```ini
262
262
+
# /etc/systemd/system/myapp.service
263
263
+
[Unit]
264
264
+
Description=My Application
265
265
+
After=docker.service
266
266
+
267
267
+
[Service]
268
268
+
Type=oneshot
269
269
+
ExecStartPre=/path/to/verify-and-pull.sh atcr.io/myorg/myapp:latest
270
270
+
ExecStart=/usr/bin/docker run atcr.io/myorg/myapp:latest
271
271
+
Restart=on-failure
272
272
+
273
273
+
[Install]
274
274
+
WantedBy=multi-user.target
275
275
+
```
276
276
+
277
277
+
## Troubleshooting
278
278
+
279
279
+
### "No ATProto signature found"
280
280
+
281
281
+
**Cause:** Image doesn't have a signature artifact
282
282
+
283
283
+
**Solutions:**
284
284
+
1. Check if image exists: `crane digest IMAGE`
285
285
+
2. Re-push image to generate signature
286
286
+
3. Verify referrers API is working:
287
287
+
```bash
288
288
+
curl "https://atcr.io/v2/REPO/referrers/DIGEST"
289
289
+
```
290
290
+
291
291
+
### "Failed to resolve DID"
292
292
+
293
293
+
**Cause:** DID resolution failed
294
294
+
295
295
+
**Solutions:**
296
296
+
1. Check internet connectivity
297
297
+
2. Verify DID is valid: `curl https://plc.directory/DID`
298
298
+
3. Check if DID document has verificationMethod
299
299
+
300
300
+
### "Failed to fetch record from PDS"
301
301
+
302
302
+
**Cause:** PDS is unreachable or record doesn't exist
303
303
+
304
304
+
**Solutions:**
305
305
+
1. Check PDS endpoint: `curl PDS_URL/xrpc/com.atproto.server.describeServer`
306
306
+
2. Verify record URI is correct
307
307
+
3. Check if record exists in PDS
308
308
+
309
309
+
### Webhook Pods Don't Start
310
310
+
311
311
+
**Cause:** Webhook is rejecting all pods
312
312
+
313
313
+
**Solutions:**
314
314
+
1. Check webhook logs: `kubectl logs -n atcr-system -l app=atcr-verify-webhook`
315
315
+
2. Disable webhook temporarily: `kubectl delete validatingwebhookconfiguration atcr-verify`
316
316
+
3. Fix issue and re-deploy
317
317
+
4. Test with labeled namespace first
318
318
+
319
319
+
## Security Best Practices
320
320
+
321
321
+
1. **Always verify in production**
322
322
+
- Enable webhook for production namespaces
323
323
+
- Set `failurePolicy: Fail` to block on errors
324
324
+
325
325
+
2. **Use trust policies**
326
326
+
- Define specific trusted DIDs
327
327
+
- Don't trust all signatures blindly
328
328
+
- Set expiration dates for temporary access
329
329
+
330
330
+
3. **Monitor verification**
331
331
+
- Enable audit logging
332
332
+
- Review verification failures
333
333
+
- Track signature coverage
334
334
+
335
335
+
4. **Rotate keys regularly**
336
336
+
- Update DID documents when keys change
337
337
+
- Revoke compromised keys immediately
338
338
+
- Monitor for unexpected key changes
339
339
+
340
340
+
5. **Secure webhook deployment**
341
341
+
- Use TLS for webhook communication
342
342
+
- Restrict webhook RBAC permissions
343
343
+
- Keep webhook image updated
344
344
+
345
345
+
## Next Steps
346
346
+
347
347
+
1. **Test verification** with your images
348
348
+
2. **Customize trust policy** for your organization
349
349
+
3. **Deploy webhook** to test clusters first
350
350
+
4. **Monitor** verification in CI/CD pipelines
351
351
+
5. **Gradually roll out** to production
352
352
+
353
353
+
## See Also
354
354
+
355
355
+
- [ATProto Signatures](../../docs/ATPROTO_SIGNATURES.md) - Technical details
356
356
+
- [Signature Integration](../../docs/SIGNATURE_INTEGRATION.md) - Integration guide
357
357
+
- [SBOM Scanning](../../docs/SBOM_SCANNING.md) - Similar ORAS pattern
358
358
+
359
359
+
## Support
360
360
+
361
361
+
For issues or questions:
362
362
+
- GitHub Issues: https://github.com/your-org/atcr/issues
363
363
+
- Documentation: https://docs.atcr.io
364
364
+
- Security: security@yourorg.com
+243
examples/verification/atcr-verify.sh
···
1
1
+
#!/bin/bash
2
2
+
# ATProto Signature Verification Script
3
3
+
#
4
4
+
# This script verifies ATProto signatures for container images stored in ATCR.
5
5
+
# It performs all steps except full cryptographic verification (which requires
6
6
+
# the indigo library). For production use, use the atcr-verify CLI tool.
7
7
+
#
8
8
+
# Usage: ./atcr-verify.sh IMAGE_REF
9
9
+
# Example: ./atcr-verify.sh atcr.io/alice/myapp:latest
10
10
+
#
11
11
+
# Requirements:
12
12
+
# - curl
13
13
+
# - jq
14
14
+
# - crane (https://github.com/google/go-containerregistry/releases)
15
15
+
# - oras (https://oras.land/docs/installation)
16
16
+
17
17
+
set -e
18
18
+
19
19
+
# Colors for output
20
20
+
RED='\033[0;31m'
21
21
+
GREEN='\033[0;32m'
22
22
+
YELLOW='\033[1;33m'
23
23
+
BLUE='\033[0;34m'
24
24
+
NC='\033[0m' # No Color
25
25
+
26
26
+
# Check dependencies
27
27
+
check_dependencies() {
28
28
+
local missing=0
29
29
+
30
30
+
for cmd in curl jq crane oras; do
31
31
+
if ! command -v $cmd &> /dev/null; then
32
32
+
echo -e "${RED}✗${NC} Missing dependency: $cmd"
33
33
+
missing=1
34
34
+
fi
35
35
+
done
36
36
+
37
37
+
if [ $missing -eq 1 ]; then
38
38
+
echo ""
39
39
+
echo "Install missing dependencies:"
40
40
+
echo " curl: https://curl.se/download.html"
41
41
+
echo " jq: https://stedolan.github.io/jq/download/"
42
42
+
echo " crane: https://github.com/google/go-containerregistry/releases"
43
43
+
echo " oras: https://oras.land/docs/installation"
44
44
+
exit 1
45
45
+
fi
46
46
+
}
47
47
+
48
48
+
# Print with color
49
49
+
print_step() {
50
50
+
echo -e "${BLUE}[$1/${TOTAL_STEPS}]${NC} $2..."
51
51
+
}
52
52
+
53
53
+
print_success() {
54
54
+
echo -e " ${GREEN}→${NC} $1"
55
55
+
}
56
56
+
57
57
+
print_error() {
58
58
+
echo -e " ${RED}✗${NC} $1"
59
59
+
}
60
60
+
61
61
+
print_warning() {
62
62
+
echo -e " ${YELLOW}⚠${NC} $1"
63
63
+
}
64
64
+
65
65
+
# Main verification function
66
66
+
verify_image() {
67
67
+
local image="$1"
68
68
+
69
69
+
if [ -z "$image" ]; then
70
70
+
echo "Usage: $0 IMAGE_REF"
71
71
+
echo "Example: $0 atcr.io/alice/myapp:latest"
72
72
+
exit 1
73
73
+
fi
74
74
+
75
75
+
TOTAL_STEPS=7
76
76
+
77
77
+
echo ""
78
78
+
echo "═══════════════════════════════════════════════════"
79
79
+
echo " ATProto Signature Verification"
80
80
+
echo "═══════════════════════════════════════════════════"
81
81
+
echo " Image: $image"
82
82
+
echo "═══════════════════════════════════════════════════"
83
83
+
echo ""
84
84
+
85
85
+
# Step 1: Resolve image digest
86
86
+
print_step 1 "Resolving image digest"
87
87
+
DIGEST=$(crane digest "$image" 2>&1)
88
88
+
if [ $? -ne 0 ]; then
89
89
+
print_error "Failed to resolve image digest"
90
90
+
echo "$DIGEST"
91
91
+
exit 1
92
92
+
fi
93
93
+
print_success "$DIGEST"
94
94
+
95
95
+
# Extract registry, repository, and tag
96
96
+
REGISTRY=$(echo "$image" | cut -d/ -f1)
97
97
+
REPO=$(echo "$image" | cut -d/ -f2-)
98
98
+
REPO_PATH=$(echo "$REPO" | cut -d: -f1)
99
99
+
100
100
+
# Step 2: Discover ATProto signature artifacts
101
101
+
print_step 2 "Discovering ATProto signature artifacts"
102
102
+
REFERRERS_URL="https://${REGISTRY}/v2/${REPO_PATH}/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json"
103
103
+
104
104
+
SIG_ARTIFACTS=$(curl -s -H "Accept: application/vnd.oci.image.index.v1+json" "$REFERRERS_URL")
105
105
+
106
106
+
if [ $? -ne 0 ]; then
107
107
+
print_error "Failed to query referrers API"
108
108
+
exit 1
109
109
+
fi
110
110
+
111
111
+
SIG_COUNT=$(echo "$SIG_ARTIFACTS" | jq '.manifests | length')
112
112
+
if [ "$SIG_COUNT" = "0" ]; then
113
113
+
print_error "No ATProto signature found"
114
114
+
echo ""
115
115
+
echo "This image does not have an ATProto signature."
116
116
+
echo "Signatures are automatically created when you push to ATCR."
117
117
+
exit 1
118
118
+
fi
119
119
+
120
120
+
print_success "Found $SIG_COUNT signature(s)"
121
121
+
122
122
+
# Get first signature digest
123
123
+
SIG_DIGEST=$(echo "$SIG_ARTIFACTS" | jq -r '.manifests[0].digest')
124
124
+
SIG_DID=$(echo "$SIG_ARTIFACTS" | jq -r '.manifests[0].annotations["io.atcr.atproto.did"]')
125
125
+
print_success "Signature digest: $SIG_DIGEST"
126
126
+
print_success "Signed by DID: $SIG_DID"
127
127
+
128
128
+
# Step 3: Fetch signature metadata
129
129
+
print_step 3 "Fetching signature metadata"
130
130
+
131
131
+
TMPDIR=$(mktemp -d)
132
132
+
trap "rm -rf $TMPDIR" EXIT
133
133
+
134
134
+
oras pull "${REGISTRY}/${REPO_PATH}@${SIG_DIGEST}" -o "$TMPDIR" --quiet 2>&1
135
135
+
if [ $? -ne 0 ]; then
136
136
+
print_error "Failed to fetch signature metadata"
137
137
+
exit 1
138
138
+
fi
139
139
+
140
140
+
# Find the JSON file
141
141
+
SIG_FILE=$(find "$TMPDIR" -name "*.json" -type f | head -n 1)
142
142
+
if [ -z "$SIG_FILE" ]; then
143
143
+
print_error "Signature metadata file not found"
144
144
+
exit 1
145
145
+
fi
146
146
+
147
147
+
DID=$(jq -r '.atproto.did' "$SIG_FILE")
148
148
+
HANDLE=$(jq -r '.atproto.handle // "unknown"' "$SIG_FILE")
149
149
+
PDS=$(jq -r '.atproto.pdsEndpoint' "$SIG_FILE")
150
150
+
RECORD_URI=$(jq -r '.atproto.recordUri' "$SIG_FILE")
151
151
+
COMMIT_CID=$(jq -r '.atproto.commitCid' "$SIG_FILE")
152
152
+
SIGNED_AT=$(jq -r '.atproto.signedAt' "$SIG_FILE")
153
153
+
154
154
+
print_success "DID: $DID"
155
155
+
print_success "Handle: $HANDLE"
156
156
+
print_success "PDS: $PDS"
157
157
+
print_success "Record: $RECORD_URI"
158
158
+
print_success "Signed at: $SIGNED_AT"
159
159
+
160
160
+
# Step 4: Resolve DID to public key
161
161
+
print_step 4 "Resolving DID to public key"
162
162
+
163
163
+
DID_DOC=$(curl -s "https://plc.directory/$DID")
164
164
+
if [ $? -ne 0 ]; then
165
165
+
print_error "Failed to resolve DID"
166
166
+
exit 1
167
167
+
fi
168
168
+
169
169
+
PUB_KEY_MB=$(echo "$DID_DOC" | jq -r '.verificationMethod[0].publicKeyMultibase')
170
170
+
if [ "$PUB_KEY_MB" = "null" ] || [ -z "$PUB_KEY_MB" ]; then
171
171
+
print_error "Public key not found in DID document"
172
172
+
exit 1
173
173
+
fi
174
174
+
175
175
+
print_success "Public key: ${PUB_KEY_MB:0:20}...${PUB_KEY_MB: -10}"
176
176
+
177
177
+
# Step 5: Query PDS for signed record
178
178
+
print_step 5 "Querying PDS for signed record"
179
179
+
180
180
+
# Extract collection and rkey from record URI (at://did/collection/rkey)
181
181
+
COLLECTION=$(echo "$RECORD_URI" | sed 's|at://[^/]*/\([^/]*\)/.*|\1|')
182
182
+
RKEY=$(echo "$RECORD_URI" | sed 's|at://.*/||')
183
183
+
184
184
+
RECORD_URL="${PDS}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=${COLLECTION}&rkey=${RKEY}"
185
185
+
RECORD=$(curl -s "$RECORD_URL")
186
186
+
187
187
+
if [ $? -ne 0 ]; then
188
188
+
print_error "Failed to fetch record from PDS"
189
189
+
exit 1
190
190
+
fi
191
191
+
192
192
+
RECORD_CID=$(echo "$RECORD" | jq -r '.cid')
193
193
+
if [ "$RECORD_CID" = "null" ] || [ -z "$RECORD_CID" ]; then
194
194
+
print_error "Record not found in PDS"
195
195
+
exit 1
196
196
+
fi
197
197
+
198
198
+
print_success "Record CID: $RECORD_CID"
199
199
+
200
200
+
# Step 6: Verify record matches image manifest
201
201
+
print_step 6 "Verifying record integrity"
202
202
+
203
203
+
RECORD_DIGEST=$(echo "$RECORD" | jq -r '.value.digest')
204
204
+
if [ "$RECORD_DIGEST" != "$DIGEST" ]; then
205
205
+
print_error "Record digest ($RECORD_DIGEST) doesn't match image digest ($DIGEST)"
206
206
+
exit 1
207
207
+
fi
208
208
+
209
209
+
print_success "Record digest matches image digest"
210
210
+
211
211
+
# Step 7: Signature verification status
212
212
+
print_step 7 "Cryptographic signature verification"
213
213
+
214
214
+
print_warning "Full cryptographic verification requires ATProto crypto library"
215
215
+
print_warning "This script verifies:"
216
216
+
echo " • Record exists in PDS"
217
217
+
echo " • DID resolved successfully"
218
218
+
echo " • Public key retrieved from DID document"
219
219
+
echo " • Record digest matches image digest"
220
220
+
echo ""
221
221
+
print_warning "For full cryptographic verification, use: atcr-verify $image"
222
222
+
223
223
+
# Summary
224
224
+
echo ""
225
225
+
echo "═══════════════════════════════════════════════════"
226
226
+
echo -e " ${GREEN}✓ Verification Completed${NC}"
227
227
+
echo "═══════════════════════════════════════════════════"
228
228
+
echo ""
229
229
+
echo " Signed by: $HANDLE ($DID)"
230
230
+
echo " Signed at: $SIGNED_AT"
231
231
+
echo " PDS: $PDS"
232
232
+
echo " Record: $RECORD_URI"
233
233
+
echo " Signature: $SIG_DIGEST"
234
234
+
echo ""
235
235
+
echo "═══════════════════════════════════════════════════"
236
236
+
echo ""
237
237
+
}
238
238
+
239
239
+
# Check dependencies first
240
240
+
check_dependencies
241
241
+
242
242
+
# Run verification
243
243
+
verify_image "$1"
+259
examples/verification/kubernetes-webhook.yaml
···
1
1
+
# Kubernetes Admission Webhook for ATProto Signature Verification
2
2
+
#
3
3
+
# This example shows how to deploy a validating admission webhook that
4
4
+
# verifies ATProto signatures before allowing pods to be created.
5
5
+
#
6
6
+
# Prerequisites:
7
7
+
# 1. Build and push the webhook image (see examples/webhook/ for code)
8
8
+
# 2. Generate TLS certificates for the webhook
9
9
+
# 3. Create trust policy ConfigMap
10
10
+
#
11
11
+
# Usage:
12
12
+
# kubectl apply -f kubernetes-webhook.yaml
13
13
+
# kubectl label namespace production atcr-verify=enabled
14
14
+
15
15
+
---
16
16
+
apiVersion: v1
17
17
+
kind: Namespace
18
18
+
metadata:
19
19
+
name: atcr-system
20
20
+
---
21
21
+
# ConfigMap with trust policy
22
22
+
apiVersion: v1
23
23
+
kind: ConfigMap
24
24
+
metadata:
25
25
+
name: atcr-trust-policy
26
26
+
namespace: atcr-system
27
27
+
data:
28
28
+
policy.yaml: |
29
29
+
version: 1.0
30
30
+
31
31
+
# Global settings
32
32
+
defaultAction: enforce # enforce, audit, or allow
33
33
+
34
34
+
# Policies by image pattern
35
35
+
policies:
36
36
+
- name: production-images
37
37
+
scope: "atcr.io/*/prod-*"
38
38
+
require:
39
39
+
signature: true
40
40
+
trustedDIDs:
41
41
+
- did:plc:your-org-devops
42
42
+
- did:plc:your-org-security
43
43
+
minSignatures: 1
44
44
+
action: enforce
45
45
+
46
46
+
- name: staging-images
47
47
+
scope: "atcr.io/*/staging-*"
48
48
+
require:
49
49
+
signature: true
50
50
+
trustedDIDs:
51
51
+
- did:plc:your-org-devops
52
52
+
- did:plc:your-org-security
53
53
+
- did:plc:your-developers
54
54
+
action: enforce
55
55
+
56
56
+
- name: dev-images
57
57
+
scope: "atcr.io/*/dev-*"
58
58
+
require:
59
59
+
signature: false
60
60
+
action: audit # Log but don't block
61
61
+
62
62
+
# Trusted DIDs configuration
63
63
+
trustedDIDs:
64
64
+
did:plc:your-org-devops:
65
65
+
name: "DevOps Team"
66
66
+
validFrom: "2024-01-01T00:00:00Z"
67
67
+
expiresAt: null
68
68
+
69
69
+
did:plc:your-org-security:
70
70
+
name: "Security Team"
71
71
+
validFrom: "2024-01-01T00:00:00Z"
72
72
+
expiresAt: null
73
73
+
74
74
+
did:plc:your-developers:
75
75
+
name: "Developer Team"
76
76
+
validFrom: "2024-06-01T00:00:00Z"
77
77
+
expiresAt: null
78
78
+
---
79
79
+
# Service for webhook
80
80
+
apiVersion: v1
81
81
+
kind: Service
82
82
+
metadata:
83
83
+
name: atcr-verify-webhook
84
84
+
namespace: atcr-system
85
85
+
spec:
86
86
+
selector:
87
87
+
app: atcr-verify-webhook
88
88
+
ports:
89
89
+
- name: https
90
90
+
port: 443
91
91
+
targetPort: 8443
92
92
+
---
93
93
+
# Deployment for webhook
94
94
+
apiVersion: apps/v1
95
95
+
kind: Deployment
96
96
+
metadata:
97
97
+
name: atcr-verify-webhook
98
98
+
namespace: atcr-system
99
99
+
spec:
100
100
+
replicas: 2
101
101
+
selector:
102
102
+
matchLabels:
103
103
+
app: atcr-verify-webhook
104
104
+
template:
105
105
+
metadata:
106
106
+
labels:
107
107
+
app: atcr-verify-webhook
108
108
+
spec:
109
109
+
containers:
110
110
+
- name: webhook
111
111
+
image: atcr.io/atcr/verify-webhook:latest
112
112
+
imagePullPolicy: Always
113
113
+
ports:
114
114
+
- containerPort: 8443
115
115
+
name: https
116
116
+
env:
117
117
+
- name: TLS_CERT_FILE
118
118
+
value: /etc/webhook/certs/tls.crt
119
119
+
- name: TLS_KEY_FILE
120
120
+
value: /etc/webhook/certs/tls.key
121
121
+
- name: POLICY_FILE
122
122
+
value: /etc/webhook/policy/policy.yaml
123
123
+
- name: LOG_LEVEL
124
124
+
value: info
125
125
+
volumeMounts:
126
126
+
- name: webhook-certs
127
127
+
mountPath: /etc/webhook/certs
128
128
+
readOnly: true
129
129
+
- name: policy
130
130
+
mountPath: /etc/webhook/policy
131
131
+
readOnly: true
132
132
+
resources:
133
133
+
requests:
134
134
+
memory: "64Mi"
135
135
+
cpu: "100m"
136
136
+
limits:
137
137
+
memory: "256Mi"
138
138
+
cpu: "500m"
139
139
+
livenessProbe:
140
140
+
httpGet:
141
141
+
path: /healthz
142
142
+
port: 8443
143
143
+
scheme: HTTPS
144
144
+
initialDelaySeconds: 10
145
145
+
periodSeconds: 10
146
146
+
readinessProbe:
147
147
+
httpGet:
148
148
+
path: /readyz
149
149
+
port: 8443
150
150
+
scheme: HTTPS
151
151
+
initialDelaySeconds: 5
152
152
+
periodSeconds: 5
153
153
+
volumes:
154
154
+
- name: webhook-certs
155
155
+
secret:
156
156
+
secretName: atcr-verify-webhook-certs
157
157
+
- name: policy
158
158
+
configMap:
159
159
+
name: atcr-trust-policy
160
160
+
---
161
161
+
# ValidatingWebhookConfiguration
162
162
+
apiVersion: admissionregistration.k8s.io/v1
163
163
+
kind: ValidatingWebhookConfiguration
164
164
+
metadata:
165
165
+
name: atcr-verify
166
166
+
webhooks:
167
167
+
- name: verify.atcr.io
168
168
+
admissionReviewVersions: ["v1", "v1beta1"]
169
169
+
sideEffects: None
170
170
+
171
171
+
# Client configuration
172
172
+
clientConfig:
173
173
+
service:
174
174
+
name: atcr-verify-webhook
175
175
+
namespace: atcr-system
176
176
+
path: /validate
177
177
+
port: 443
178
178
+
# CA bundle for webhook TLS (base64-encoded CA cert)
179
179
+
# Generate with: cat ca.crt | base64 -w 0
180
180
+
caBundle: LS0tLS1CRUdJTi... # Replace with your CA bundle
181
181
+
182
182
+
# Rules - what to validate
183
183
+
rules:
184
184
+
- operations: ["CREATE", "UPDATE"]
185
185
+
apiGroups: [""]
186
186
+
apiVersions: ["v1"]
187
187
+
resources: ["pods"]
188
188
+
scope: "Namespaced"
189
189
+
190
190
+
# Namespace selector - only validate labeled namespaces
191
191
+
namespaceSelector:
192
192
+
matchExpressions:
193
193
+
- key: atcr-verify
194
194
+
operator: In
195
195
+
values: ["enabled", "enforce"]
196
196
+
197
197
+
# Failure policy - what to do if webhook fails
198
198
+
failurePolicy: Fail # Reject pods if webhook is unavailable
199
199
+
200
200
+
# Timeout
201
201
+
timeoutSeconds: 10
202
202
+
203
203
+
# Match policy
204
204
+
matchPolicy: Equivalent
205
205
+
---
206
206
+
# Example: Label a namespace to enable verification
207
207
+
# kubectl label namespace production atcr-verify=enabled
208
208
+
---
209
209
+
# RBAC for webhook
210
210
+
apiVersion: v1
211
211
+
kind: ServiceAccount
212
212
+
metadata:
213
213
+
name: atcr-verify-webhook
214
214
+
namespace: atcr-system
215
215
+
---
216
216
+
apiVersion: rbac.authorization.k8s.io/v1
217
217
+
kind: ClusterRole
218
218
+
metadata:
219
219
+
name: atcr-verify-webhook
220
220
+
rules:
221
221
+
- apiGroups: [""]
222
222
+
resources: ["pods"]
223
223
+
verbs: ["get", "list"]
224
224
+
- apiGroups: [""]
225
225
+
resources: ["events"]
226
226
+
verbs: ["create", "patch"]
227
227
+
---
228
228
+
apiVersion: rbac.authorization.k8s.io/v1
229
229
+
kind: ClusterRoleBinding
230
230
+
metadata:
231
231
+
name: atcr-verify-webhook
232
232
+
roleRef:
233
233
+
apiGroup: rbac.authorization.k8s.io
234
234
+
kind: ClusterRole
235
235
+
name: atcr-verify-webhook
236
236
+
subjects:
237
237
+
- kind: ServiceAccount
238
238
+
name: atcr-verify-webhook
239
239
+
namespace: atcr-system
240
240
+
---
241
241
+
# Secret for TLS certificates
242
242
+
# Generate certificates with:
243
243
+
# openssl req -x509 -newkey rsa:4096 -keyout tls.key -out tls.crt \
244
244
+
# -days 365 -nodes -subj "/CN=atcr-verify-webhook.atcr-system.svc"
245
245
+
#
246
246
+
# Create secret with:
247
247
+
# kubectl create secret tls atcr-verify-webhook-certs \
248
248
+
# --cert=tls.crt --key=tls.key -n atcr-system
249
249
+
#
250
250
+
# (Commented out - create manually with your certs)
251
251
+
# apiVersion: v1
252
252
+
# kind: Secret
253
253
+
# metadata:
254
254
+
# name: atcr-verify-webhook-certs
255
255
+
# namespace: atcr-system
256
256
+
# type: kubernetes.io/tls
257
257
+
# data:
258
258
+
# tls.crt: <base64-encoded-cert>
259
259
+
# tls.key: <base64-encoded-key>
+247
examples/verification/trust-policy.yaml
···
1
1
+
# ATProto Signature Trust Policy
2
2
+
#
3
3
+
# This file defines which signatures to trust and what to do when
4
4
+
# signatures are invalid or missing.
5
5
+
#
6
6
+
# Usage with atcr-verify:
7
7
+
# atcr-verify IMAGE --policy trust-policy.yaml
8
8
+
9
9
+
version: 1.0
10
10
+
11
11
+
# Global settings
12
12
+
defaultAction: enforce # Options: enforce, audit, allow
13
13
+
requireSignature: true # Require at least one signature
14
14
+
15
15
+
# Policies matched by image scope (first match wins)
16
16
+
policies:
17
17
+
# Production images require signatures from trusted DIDs
18
18
+
- name: production-images
19
19
+
description: "Production images must be signed by DevOps or Security team"
20
20
+
scope: "atcr.io/*/prod-*"
21
21
+
require:
22
22
+
signature: true
23
23
+
trustedDIDs:
24
24
+
- did:plc:your-org-devops
25
25
+
- did:plc:your-org-security
26
26
+
minSignatures: 1
27
27
+
maxAge: 2592000 # 30 days in seconds
28
28
+
action: enforce # Reject if policy fails
29
29
+
30
30
+
# Critical infrastructure requires multi-signature
31
31
+
- name: critical-infrastructure
32
32
+
description: "Critical services require 2 signatures"
33
33
+
scope: "atcr.io/*/critical-*"
34
34
+
require:
35
35
+
signature: true
36
36
+
trustedDIDs:
37
37
+
- did:plc:your-org-security
38
38
+
- did:plc:your-org-devops
39
39
+
minSignatures: 2 # Require at least 2 signatures
40
40
+
algorithms:
41
41
+
- ECDSA-K256-SHA256 # Only allow specific algorithms
42
42
+
action: enforce
43
43
+
44
44
+
# Staging images require signature from any team member
45
45
+
- name: staging-images
46
46
+
description: "Staging images need any trusted signature"
47
47
+
scope: "atcr.io/*/staging-*"
48
48
+
require:
49
49
+
signature: true
50
50
+
trustedDIDs:
51
51
+
- did:plc:your-org-devops
52
52
+
- did:plc:your-org-security
53
53
+
- did:plc:your-org-developers
54
54
+
minSignatures: 1
55
55
+
action: enforce
56
56
+
57
57
+
# Development images are audited but not blocked
58
58
+
- name: dev-images
59
59
+
description: "Development images are monitored"
60
60
+
scope: "atcr.io/*/dev-*"
61
61
+
require:
62
62
+
signature: false # Don't require signatures
63
63
+
action: audit # Log but don't reject
64
64
+
65
65
+
# Test images from external sources
66
66
+
- name: external-test-images
67
67
+
description: "Test images from partners"
68
68
+
scope: "atcr.io/external/*"
69
69
+
require:
70
70
+
signature: true
71
71
+
trustedDIDs:
72
72
+
- did:plc:partner-acme
73
73
+
- did:plc:partner-widgets
74
74
+
minSignatures: 1
75
75
+
action: enforce
76
76
+
77
77
+
# Default fallback for all other images
78
78
+
- name: default
79
79
+
description: "All other images require signature"
80
80
+
scope: "atcr.io/*/*"
81
81
+
require:
82
82
+
signature: true
83
83
+
minSignatures: 1
84
84
+
action: enforce
85
85
+
86
86
+
# Trusted DID registry
87
87
+
trustedDIDs:
88
88
+
# Your organization's DevOps team
89
89
+
did:plc:your-org-devops:
90
90
+
name: "DevOps Team"
91
91
+
description: "Production deployment automation"
92
92
+
validFrom: "2024-01-01T00:00:00Z"
93
93
+
expiresAt: null # Never expires
94
94
+
contact: "devops@yourorg.com"
95
95
+
allowedScopes:
96
96
+
- "atcr.io/*/prod-*"
97
97
+
- "atcr.io/*/staging-*"
98
98
+
- "atcr.io/*/critical-*"
99
99
+
100
100
+
# Your organization's Security team
101
101
+
did:plc:your-org-security:
102
102
+
name: "Security Team"
103
103
+
description: "Security-reviewed images"
104
104
+
validFrom: "2024-01-01T00:00:00Z"
105
105
+
expiresAt: null
106
106
+
contact: "security@yourorg.com"
107
107
+
allowedScopes:
108
108
+
- "atcr.io/*/*" # Can sign any image
109
109
+
110
110
+
# Developer team (limited access)
111
111
+
did:plc:your-org-developers:
112
112
+
name: "Developer Team"
113
113
+
description: "Development and staging images"
114
114
+
validFrom: "2024-06-01T00:00:00Z"
115
115
+
expiresAt: "2025-12-31T23:59:59Z" # Temporary access
116
116
+
contact: "dev-team@yourorg.com"
117
117
+
allowedScopes:
118
118
+
- "atcr.io/*/dev-*"
119
119
+
- "atcr.io/*/staging-*"
120
120
+
notes: "Access expires end of 2025 - review then"
121
121
+
122
122
+
# External partner: ACME Corp
123
123
+
did:plc:partner-acme:
124
124
+
name: "ACME Corp Integration Team"
125
125
+
description: "Third-party integration images"
126
126
+
validFrom: "2024-09-01T00:00:00Z"
127
127
+
expiresAt: "2025-09-01T00:00:00Z"
128
128
+
contact: "integration@acme.example.com"
129
129
+
allowedScopes:
130
130
+
- "atcr.io/external/acme-*"
131
131
+
132
132
+
# External partner: Widgets Inc
133
133
+
did:plc:partner-widgets:
134
134
+
name: "Widgets Inc"
135
135
+
description: "Widgets service integration"
136
136
+
validFrom: "2024-10-01T00:00:00Z"
137
137
+
expiresAt: "2025-10-01T00:00:00Z"
138
138
+
contact: "api@widgets.example.com"
139
139
+
allowedScopes:
140
140
+
- "atcr.io/external/widgets-*"
141
141
+
142
142
+
# Signature validation settings
143
143
+
validation:
144
144
+
# Signature age limits
145
145
+
maxSignatureAge: 7776000 # 90 days in seconds (null = no limit)
146
146
+
147
147
+
# Allowed signature algorithms
148
148
+
allowedAlgorithms:
149
149
+
- ECDSA-K256-SHA256 # ATProto default
150
150
+
- ECDSA-P256-SHA256 # Alternative
151
151
+
152
152
+
# DID resolution settings
153
153
+
didResolver:
154
154
+
timeout: 10 # seconds
155
155
+
cache:
156
156
+
enabled: true
157
157
+
ttl: 3600 # 1 hour in seconds
158
158
+
fallbackResolvers:
159
159
+
- https://plc.directory
160
160
+
- https://backup-plc.example.com
161
161
+
162
162
+
# PDS connection settings
163
163
+
pds:
164
164
+
timeout: 15 # seconds
165
165
+
retries: 3
166
166
+
cache:
167
167
+
enabled: true
168
168
+
ttl: 600 # 10 minutes
169
169
+
170
170
+
# Audit logging
171
171
+
audit:
172
172
+
enabled: true
173
173
+
logLevel: info # debug, info, warn, error
174
174
+
175
175
+
# What to log
176
176
+
logEvents:
177
177
+
- signature_verified
178
178
+
- signature_missing
179
179
+
- signature_invalid
180
180
+
- signature_expired
181
181
+
- did_resolution_failed
182
182
+
- pds_query_failed
183
183
+
- policy_violation
184
184
+
185
185
+
# Log destinations
186
186
+
destinations:
187
187
+
- type: stdout
188
188
+
format: json
189
189
+
- type: file
190
190
+
path: /var/log/atcr-verify/audit.log
191
191
+
format: json
192
192
+
rotate: true
193
193
+
maxSize: 100MB
194
194
+
maxFiles: 10
195
195
+
196
196
+
# Reporting and metrics
197
197
+
reporting:
198
198
+
# Prometheus metrics
199
199
+
metrics:
200
200
+
enabled: true
201
201
+
port: 9090
202
202
+
path: /metrics
203
203
+
204
204
+
# Periodic reports
205
205
+
reports:
206
206
+
enabled: true
207
207
+
interval: 86400 # Daily in seconds
208
208
+
email:
209
209
+
- security@yourorg.com
210
210
+
- devops@yourorg.com
211
211
+
includeStatistics: true
212
212
+
213
213
+
# Emergency overrides
214
214
+
overrides:
215
215
+
# Allow bypassing verification in emergencies
216
216
+
enabled: false # Enable with extreme caution!
217
217
+
requireApproval: true
218
218
+
approvers:
219
219
+
- security@yourorg.com
220
220
+
validDuration: 3600 # Override valid for 1 hour
221
221
+
222
222
+
# Examples of policy evaluation:
223
223
+
#
224
224
+
# atcr.io/myorg/prod-api:v1.2.3
225
225
+
# → Matches: production-images
226
226
+
# → Requires: 1 signature from DevOps or Security
227
227
+
# → Action: enforce
228
228
+
#
229
229
+
# atcr.io/myorg/critical-auth:v2.0.0
230
230
+
# → Matches: critical-infrastructure
231
231
+
# → Requires: 2 signatures from Security and DevOps
232
232
+
# → Action: enforce
233
233
+
#
234
234
+
# atcr.io/myorg/staging-frontend:latest
235
235
+
# → Matches: staging-images
236
236
+
# → Requires: 1 signature from any team member
237
237
+
# → Action: enforce
238
238
+
#
239
239
+
# atcr.io/myorg/dev-experiment:test
240
240
+
# → Matches: dev-images
241
241
+
# → Requires: none
242
242
+
# → Action: audit (log only)
243
243
+
#
244
244
+
# atcr.io/external/acme-connector:v1.0
245
245
+
# → Matches: external-test-images
246
246
+
# → Requires: 1 signature from partner-acme
247
247
+
# → Action: enforce
+162
examples/verification/verify-and-pull.sh
···
1
1
+
#!/bin/bash
2
2
+
# Verify and Pull Script
3
3
+
#
4
4
+
# This script verifies ATProto signatures before pulling images with Docker.
5
5
+
# It acts as a wrapper around `docker pull` to enforce signature verification.
6
6
+
#
7
7
+
# Usage: ./verify-and-pull.sh IMAGE [DOCKER_PULL_OPTIONS]
8
8
+
# Example: ./verify-and-pull.sh atcr.io/alice/myapp:latest
9
9
+
# Example: ./verify-and-pull.sh atcr.io/alice/myapp:latest --platform linux/amd64
10
10
+
#
11
11
+
# To use this as a replacement for docker pull, create an alias:
12
12
+
# alias docker-pull-secure='/path/to/verify-and-pull.sh'
13
13
+
14
14
+
set -e
15
15
+
16
16
+
# Configuration
17
17
+
VERIFY_SCRIPT="${VERIFY_SCRIPT:-$(dirname $0)/atcr-verify.sh}"
18
18
+
TRUST_POLICY="${TRUST_POLICY:-$(dirname $0)/trust-policy.yaml}"
19
19
+
REQUIRE_VERIFICATION="${REQUIRE_VERIFICATION:-true}"
20
20
+
SKIP_ATCR_IMAGES="${SKIP_ATCR_IMAGES:-false}" # Skip verification for non-ATCR images
21
21
+
22
22
+
# Colors
23
23
+
RED='\033[0;31m'
24
24
+
GREEN='\033[0;32m'
25
25
+
YELLOW='\033[1;33m'
26
26
+
BLUE='\033[0;34m'
27
27
+
NC='\033[0m'
28
28
+
29
29
+
print_header() {
30
30
+
echo ""
31
31
+
echo -e "${BLUE}═══════════════════════════════════════════════════${NC}"
32
32
+
echo -e "${BLUE} Secure Image Pull with Signature Verification${NC}"
33
33
+
echo -e "${BLUE}═══════════════════════════════════════════════════${NC}"
34
34
+
echo ""
35
35
+
}
36
36
+
37
37
+
print_success() {
38
38
+
echo -e "${GREEN}✓${NC} $1"
39
39
+
}
40
40
+
41
41
+
print_error() {
42
42
+
echo -e "${RED}✗${NC} $1"
43
43
+
}
44
44
+
45
45
+
print_warning() {
46
46
+
echo -e "${YELLOW}⚠${NC} $1"
47
47
+
}
48
48
+
49
49
+
# Check if image is from ATCR
50
50
+
is_atcr_image() {
51
51
+
local image="$1"
52
52
+
if [[ "$image" =~ ^atcr\.io/ ]]; then
53
53
+
return 0
54
54
+
else
55
55
+
return 1
56
56
+
fi
57
57
+
}
58
58
+
59
59
+
# Main function
60
60
+
main() {
61
61
+
if [ $# -eq 0 ]; then
62
62
+
echo "Usage: $0 IMAGE [DOCKER_PULL_OPTIONS]"
63
63
+
echo ""
64
64
+
echo "Examples:"
65
65
+
echo " $0 atcr.io/alice/myapp:latest"
66
66
+
echo " $0 atcr.io/alice/myapp:latest --platform linux/amd64"
67
67
+
echo ""
68
68
+
echo "Environment variables:"
69
69
+
echo " VERIFY_SCRIPT - Path to verification script (default: ./atcr-verify.sh)"
70
70
+
echo " TRUST_POLICY - Path to trust policy (default: ./trust-policy.yaml)"
71
71
+
echo " REQUIRE_VERIFICATION - Require verification for ATCR images (default: true)"
72
72
+
echo " SKIP_ATCR_IMAGES - Skip verification for non-ATCR images (default: false)"
73
73
+
exit 1
74
74
+
fi
75
75
+
76
76
+
local image="$1"
77
77
+
shift
78
78
+
local docker_args="$@"
79
79
+
80
80
+
print_header
81
81
+
82
82
+
echo -e "${BLUE}Image:${NC} $image"
83
83
+
if [ -n "$docker_args" ]; then
84
84
+
echo -e "${BLUE}Docker options:${NC} $docker_args"
85
85
+
fi
86
86
+
echo ""
87
87
+
88
88
+
# Check if this is an ATCR image
89
89
+
if ! is_atcr_image "$image"; then
90
90
+
if [ "$SKIP_ATCR_IMAGES" = "true" ]; then
91
91
+
print_warning "Not an ATCR image - skipping signature verification"
92
92
+
echo ""
93
93
+
docker pull $docker_args "$image"
94
94
+
exit $?
95
95
+
else
96
96
+
print_warning "Not an ATCR image"
97
97
+
if [ "$REQUIRE_VERIFICATION" = "true" ]; then
98
98
+
print_error "Verification required but image is not from ATCR"
99
99
+
exit 1
100
100
+
else
101
101
+
print_warning "Proceeding without verification"
102
102
+
echo ""
103
103
+
docker pull $docker_args "$image"
104
104
+
exit $?
105
105
+
fi
106
106
+
fi
107
107
+
fi
108
108
+
109
109
+
# Step 1: Verify signature
110
110
+
echo -e "${BLUE}Step 1: Verifying ATProto signature${NC}"
111
111
+
echo ""
112
112
+
113
113
+
if [ ! -f "$VERIFY_SCRIPT" ]; then
114
114
+
print_error "Verification script not found: $VERIFY_SCRIPT"
115
115
+
exit 1
116
116
+
fi
117
117
+
118
118
+
# Run verification
119
119
+
if bash "$VERIFY_SCRIPT" "$image"; then
120
120
+
print_success "Signature verification passed"
121
121
+
echo ""
122
122
+
else
123
123
+
print_error "Signature verification failed"
124
124
+
echo ""
125
125
+
126
126
+
if [ "$REQUIRE_VERIFICATION" = "true" ]; then
127
127
+
echo -e "${RED}Image pull blocked due to failed signature verification${NC}"
128
128
+
echo ""
129
129
+
echo "To proceed anyway (NOT RECOMMENDED), run:"
130
130
+
echo " REQUIRE_VERIFICATION=false $0 $image $docker_args"
131
131
+
exit 1
132
132
+
else
133
133
+
print_warning "Verification failed but REQUIRE_VERIFICATION=false"
134
134
+
print_warning "Proceeding with pull (NOT RECOMMENDED)"
135
135
+
echo ""
136
136
+
fi
137
137
+
fi
138
138
+
139
139
+
# Step 2: Pull image
140
140
+
echo -e "${BLUE}Step 2: Pulling image${NC}"
141
141
+
echo ""
142
142
+
143
143
+
if docker pull $docker_args "$image"; then
144
144
+
print_success "Image pulled successfully"
145
145
+
else
146
146
+
print_error "Failed to pull image"
147
147
+
exit 1
148
148
+
fi
149
149
+
150
150
+
# Summary
151
151
+
echo ""
152
152
+
echo -e "${GREEN}═══════════════════════════════════════════════════${NC}"
153
153
+
echo -e "${GREEN} ✓ Secure pull completed successfully${NC}"
154
154
+
echo -e "${GREEN}═══════════════════════════════════════════════════${NC}"
155
155
+
echo ""
156
156
+
echo -e "${BLUE}Image:${NC} $image"
157
157
+
echo -e "${BLUE}Status:${NC} Verified and pulled"
158
158
+
echo ""
159
159
+
}
160
160
+
161
161
+
# Run main function
162
162
+
main "$@"
+8
-6
pkg/appview/handlers/manifest_health.go
···
43
43
44
44
reachable, err := h.HealthChecker.CheckHealth(ctx, endpoint)
45
45
46
46
-
if ctx.Err() == context.DeadlineExceeded {
47
47
-
// Still pending - render "Checking..." badge with HTMX retry
48
48
-
h.renderBadge(w, endpoint, false, true)
49
49
-
} else if err != nil {
46
46
+
// Check for HTTP errors first (connection refused, network unreachable, etc.)
47
47
+
// This ensures we catch real failures even when timing aligns with context timeout
48
48
+
if err != nil {
50
49
// Error - mark as unreachable
51
50
h.renderBadge(w, endpoint, false, false)
51
51
+
} else if ctx.Err() == context.DeadlineExceeded {
52
52
+
// Context timed out but no HTTP error yet - still pending
53
53
+
h.renderBadge(w, endpoint, false, true)
52
54
} else {
53
55
// Success
54
56
h.renderBadge(w, endpoint, reachable, false)
···
65
67
w.Write([]byte(`<span class="checking-badge"
66
68
hx-get="` + retryURL + `"
67
69
hx-trigger="load delay:3s"
68
68
-
hx-swap="outerHTML">🔄 Checking...</span>`))
70
70
+
hx-swap="outerHTML"><i data-lucide="refresh-ccw"></i> Checking...</span>`))
69
71
} else if !reachable {
70
72
// Unreachable - render offline badge
71
71
-
w.Write([]byte(`<span class="offline-badge">⚠️ Offline</span>`))
73
73
+
w.Write([]byte(`<span class="offline-badge"><i data-lucide="triangle-alert"></i> Offline</span>`))
72
74
} else {
73
75
// Reachable - no badge (empty response)
74
76
w.Write([]byte(``))
+1
-1
pkg/appview/templates/pages/repository.html
···
181
181
hx-get="/api/manifest-health?endpoint={{ .Manifest.HoldEndpoint | urlquery }}"
182
182
hx-trigger="load delay:2s"
183
183
hx-swap="outerHTML">
184
184
-
<i data-lucide="rotate-cw"></i> Checking...
184
184
+
<i data-lucide="refresh-ccw"></i> Checking...
185
185
</span>
186
186
{{ else if not .Reachable }}
187
187
<span class="offline-badge"><i data-lucide="alert-triangle"></i> Offline</span>