mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-19 16:54:15 +00:00
Update every direct dependency across all five workspace modules to latest. Notable jumps: syft v1.43.0 -> v1.51.1, grype v0.111.1 -> v0.118.0, stereoscope v0.1.23 -> v0.3.1, indigo -> 2026-09-01, aws-sdk-go-v2/service/s3 v1.99.1 -> v1.110.0, grpc v1.80.0 -> v1.83.2, x/crypto v0.50.0 -> v0.55.0. Three deps needed more than a version bump: go-libipfs could not be updated at all. The repo was renamed to boxo, so every tag past v0.7.0 declares `module github.com/ipfs/boxo` and cannot be required under the old path. sqlite_store.go already imported go-block-format alongside it and used the archived package exactly once, inside a function already returning blockformat.Block, so it was relying on structural interface satisfaction. Collapsing to the native type drops the archived dependency entirely. go-didplc moved its package from the repo root into a didplc/ subdir in v0.2.2. Package name is unchanged and every symbol we use (RegularOp, OpEnum, OpService, Client.DirectoryURL, Submit) is intact, so this is an import path change only. The go-diskfs replace in scanner/go.mod had inverted. It pinned v1.7.0 because syft v1.43 passed diskfs entries as os.FileInfo; syft v1.51.1 fixed that upstream and now requires v1.9.4, so the workaround had become the thing breaking the build. Removed per its own "Remove when syft ships a fix" note, closing anchore/syft#4796 for us. The indigo bump needed no code changes: of the 21 packages we import only 5 changed, and the repo/MST/CAR-store core is byte-identical. It does bring a util/ssrf fix blocking 6to4 addresses (2002::/16), which we inherit through atproto/auth/oauth. Go 1.26.7 across go.work, all five go.mod files, the four Dockerfiles, the three tangled workflows, and the stale references in docs/DEVELOPMENT.md. Verified golang:1.26.7-trixie resolves on mirror.gcr.io, which is what the Dockerfiles actually pull from. Makefile's TRIXIE_BUILDER_IMAGE stays on the floating golang:1-trixie. make test, make lint, and make test-race all pass, as do the scanner module's tests and the integration-tagged build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KWoKzpgtBJ33sCyGxJGR7x
244 lines
7.2 KiB
Go
244 lines
7.2 KiB
Go
package did
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/bluesky-social/indigo/atproto/atcrypto"
|
|
didplc "github.com/did-method-plc/go-didplc/didplc"
|
|
)
|
|
|
|
// fakePLC stands up an httptest server that serves a single op log entry and
|
|
// captures any submitted update op for inspection.
|
|
type fakePLC struct {
|
|
server *httptest.Server
|
|
did string
|
|
logEntries []didplc.OpEnum
|
|
submitted []didplc.RegularOp
|
|
}
|
|
|
|
func (f *fakePLC) URL() string { return f.server.URL }
|
|
|
|
func (f *fakePLC) Close() { f.server.Close() }
|
|
|
|
// newFakePLC creates a fake PLC directory pre-loaded with a single signed
|
|
// genesis op containing the given rotation keys (in priority order). Returns
|
|
// the fake server and the resulting did:plc DID derived from the genesis op.
|
|
func newFakePLC(t *testing.T, rotationKeys []*atcrypto.PrivateKeyK256, signer atcrypto.PrivateKey, signingKey *atcrypto.PrivateKeyK256) *fakePLC {
|
|
t.Helper()
|
|
|
|
rotationDIDKeys := make([]string, 0, len(rotationKeys))
|
|
for _, k := range rotationKeys {
|
|
pub, err := k.PublicKey()
|
|
if err != nil {
|
|
t.Fatalf("rotation key public: %v", err)
|
|
}
|
|
rotationDIDKeys = append(rotationDIDKeys, pub.DIDKey())
|
|
}
|
|
|
|
sigPub, err := signingKey.PublicKey()
|
|
if err != nil {
|
|
t.Fatalf("signing key public: %v", err)
|
|
}
|
|
|
|
op := &didplc.RegularOp{
|
|
Type: "plc_operation",
|
|
RotationKeys: rotationDIDKeys,
|
|
VerificationMethods: map[string]string{
|
|
"atproto": sigPub.DIDKey(),
|
|
},
|
|
AlsoKnownAs: []string{"at://example.test"},
|
|
Services: map[string]didplc.OpService{
|
|
"atproto_pds": {Type: "AtprotoPersonalDataServer", Endpoint: "https://example.test"},
|
|
},
|
|
Prev: nil,
|
|
}
|
|
if err := op.Sign(signer); err != nil {
|
|
t.Fatalf("sign genesis: %v", err)
|
|
}
|
|
did, err := op.DID()
|
|
if err != nil {
|
|
t.Fatalf("compute DID: %v", err)
|
|
}
|
|
|
|
f := &fakePLC{
|
|
did: did,
|
|
logEntries: []didplc.OpEnum{{Regular: op}},
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/log") {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(f.logEntries)
|
|
return
|
|
}
|
|
if r.Method == http.MethodPost && r.URL.Path == "/"+did {
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
var op didplc.RegularOp
|
|
if err := json.Unmarshal(body, &op); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
f.submitted = append(f.submitted, op)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
})
|
|
f.server = httptest.NewServer(mux)
|
|
return f
|
|
}
|
|
|
|
// generateK256 returns a fresh K-256 keypair, failing the test on error.
|
|
func generateK256(t *testing.T) *atcrypto.PrivateKeyK256 {
|
|
t.Helper()
|
|
k, err := atcrypto.GeneratePrivateKeyK256()
|
|
if err != nil {
|
|
t.Fatalf("generate K-256: %v", err)
|
|
}
|
|
return k
|
|
}
|
|
|
|
// writeSigningKey persists a signing key to a temp file and returns its path.
|
|
// Used so EnsureCurrent can load it via oauth.GenerateOrLoadPDSKey.
|
|
func writeSigningKey(t *testing.T, dir string, key *atcrypto.PrivateKeyK256) string {
|
|
t.Helper()
|
|
path := filepath.Join(dir, "signing.key")
|
|
if err := os.WriteFile(path, key.Bytes(), 0600); err != nil {
|
|
t.Fatalf("write signing key: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestEnsureCurrent_PreservesRotationKeys(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmp := t.TempDir()
|
|
|
|
// Server-side rotation key (the one stored in database.rotation_key) and an
|
|
// "offline" recovery key that lives only in PLC. The genesis op lists offline
|
|
// FIRST (highest priority).
|
|
serverRot := generateK256(t)
|
|
offlineRot := generateK256(t)
|
|
|
|
// Original signing key used to build genesis; the local signing key on disk
|
|
// will be different to force EnsureCurrent into the update path.
|
|
originalSigning := generateK256(t)
|
|
localSigning := generateK256(t)
|
|
writeSigningKey(t, tmp, localSigning)
|
|
|
|
fake := newFakePLC(t, []*atcrypto.PrivateKeyK256{offlineRot, serverRot}, serverRot, originalSigning)
|
|
defer fake.Close()
|
|
|
|
cfg := Config{
|
|
PublicURL: "https://example.test",
|
|
PLCDirectoryURL: fake.URL(),
|
|
VerificationKeyName: "atproto",
|
|
Services: map[string]Service{
|
|
"atproto_pds": {Type: "AtprotoPersonalDataServer", Endpoint: "https://example.test"},
|
|
},
|
|
}
|
|
|
|
if err := EnsureCurrent(ctx, fake.did, serverRot, localSigning, cfg); err != nil {
|
|
t.Fatalf("EnsureCurrent: %v", err)
|
|
}
|
|
|
|
if len(fake.submitted) != 1 {
|
|
t.Fatalf("expected exactly one update op submitted, got %d", len(fake.submitted))
|
|
}
|
|
got := fake.submitted[0]
|
|
|
|
offlinePub, _ := offlineRot.PublicKey()
|
|
serverPub, _ := serverRot.PublicKey()
|
|
want := []string{offlinePub.DIDKey(), serverPub.DIDKey()}
|
|
|
|
if len(got.RotationKeys) != len(want) {
|
|
t.Fatalf("rotation keys length: got %d want %d", len(got.RotationKeys), len(want))
|
|
}
|
|
for i := range want {
|
|
if got.RotationKeys[i] != want[i] {
|
|
t.Errorf("rotation key [%d]: got %s want %s", i, got.RotationKeys[i], want[i])
|
|
}
|
|
}
|
|
|
|
// Verify signing key was actually rotated (sanity check we hit the update path).
|
|
localPub, _ := localSigning.PublicKey()
|
|
if got.VerificationMethods["atproto"] != localPub.DIDKey() {
|
|
t.Errorf("expected signing key to update to local key %s, got %s",
|
|
localPub.DIDKey(), got.VerificationMethods["atproto"])
|
|
}
|
|
}
|
|
|
|
func TestEnsureCurrent_RefusesUpdateWhenLocalKeyMissing(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmp := t.TempDir()
|
|
|
|
// Genesis lists only the offline key. The local server has been rotated out.
|
|
offlineRot := generateK256(t)
|
|
localRot := generateK256(t) // not in PLC
|
|
|
|
originalSigning := generateK256(t)
|
|
localSigning := generateK256(t)
|
|
writeSigningKey(t, tmp, localSigning)
|
|
|
|
fake := newFakePLC(t, []*atcrypto.PrivateKeyK256{offlineRot}, offlineRot, originalSigning)
|
|
defer fake.Close()
|
|
|
|
cfg := Config{
|
|
PublicURL: "https://example.test",
|
|
PLCDirectoryURL: fake.URL(),
|
|
VerificationKeyName: "atproto",
|
|
Services: map[string]Service{
|
|
"atproto_pds": {Type: "AtprotoPersonalDataServer", Endpoint: "https://example.test"},
|
|
},
|
|
}
|
|
|
|
// Local signing key has drifted, which would normally trigger an update.
|
|
if err := EnsureCurrent(ctx, fake.did, localRot, localSigning, cfg); err != nil {
|
|
t.Fatalf("EnsureCurrent returned error: %v", err)
|
|
}
|
|
|
|
if len(fake.submitted) != 0 {
|
|
t.Fatalf("expected no update submission when local rotation key isn't in PLC list, got %d", len(fake.submitted))
|
|
}
|
|
}
|
|
|
|
func TestEnsureCurrent_NoOpWhenCurrent(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmp := t.TempDir()
|
|
|
|
serverRot := generateK256(t)
|
|
signing := generateK256(t)
|
|
writeSigningKey(t, tmp, signing)
|
|
|
|
fake := newFakePLC(t, []*atcrypto.PrivateKeyK256{serverRot}, serverRot, signing)
|
|
defer fake.Close()
|
|
|
|
cfg := Config{
|
|
PublicURL: "https://example.test",
|
|
PLCDirectoryURL: fake.URL(),
|
|
VerificationKeyName: "atproto",
|
|
Services: map[string]Service{
|
|
"atproto_pds": {Type: "AtprotoPersonalDataServer", Endpoint: "https://example.test"},
|
|
},
|
|
}
|
|
|
|
if err := EnsureCurrent(ctx, fake.did, serverRot, signing, cfg); err != nil {
|
|
t.Fatalf("EnsureCurrent: %v", err)
|
|
}
|
|
if len(fake.submitted) != 0 {
|
|
t.Fatalf("expected no update when state is current, got %d submitted", len(fake.submitted))
|
|
}
|
|
}
|