Files
Evan JarrettandClaude Opus 5 15871ad188 deps: update all modules, bump go and builder images to 1.26.7
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
2026-09-01 09:02:33 -05:00

168 lines
5.3 KiB
Go

package did
import (
"context"
"fmt"
"log/slog"
"slices"
"github.com/bluesky-social/indigo/atproto/atcrypto"
didplc "github.com/did-method-plc/go-didplc/didplc"
)
// CreateIdentity builds a genesis PLC operation with the configured verification key and
// services, signs it with the rotation key, and submits it to the PLC directory.
func CreateIdentity(ctx context.Context, rotationKey atcrypto.PrivateKey, signingKey *atcrypto.PrivateKeyK256, cfg Config) (string, error) {
rotPub, err := rotationKey.PublicKey()
if err != nil {
return "", fmt.Errorf("did: failed to get rotation public key: %w", err)
}
sigPub, err := signingKey.PublicKey()
if err != nil {
return "", fmt.Errorf("did: failed to get signing public key: %w", err)
}
host, err := hostWithPort(cfg.PublicURL)
if err != nil {
return "", err
}
op := &didplc.RegularOp{
Type: "plc_operation",
RotationKeys: []string{rotPub.DIDKey()},
VerificationMethods: map[string]string{
cfg.VerificationKeyName: sigPub.DIDKey(),
},
AlsoKnownAs: []string{"at://" + host},
Services: toOpServices(cfg.Services),
Prev: nil,
}
if err := op.Sign(rotationKey); err != nil {
return "", fmt.Errorf("did: failed to sign genesis operation: %w", err)
}
d, err := op.DID()
if err != nil {
return "", fmt.Errorf("did: failed to compute DID from genesis: %w", err)
}
client := &didplc.Client{DirectoryURL: cfg.PLCDirectoryURL}
if err := client.Submit(ctx, d, op); err != nil {
return "", fmt.Errorf("did: failed to submit genesis operation: %w", err)
}
return d, nil
}
// EnsureCurrent reconciles the published DID document with the local config; if the local
// signing key, public URL, or service set differs, an update operation is signed and submitted.
// Without a rotation key, mismatches log a warning but are not fatal.
func EnsureCurrent(ctx context.Context, did string, rotationKey atcrypto.PrivateKey, signingKey *atcrypto.PrivateKeyK256, cfg Config) error {
client := &didplc.Client{DirectoryURL: cfg.PLCDirectoryURL}
opLog, err := client.OpLog(ctx, did)
if err != nil {
return fmt.Errorf("did: failed to fetch op log for %s: %w", did, err)
}
if len(opLog) == 0 {
return fmt.Errorf("did: empty op log for %s", did)
}
lastEntry := opLog[len(opLog)-1]
lastOp := lastEntry.Regular
if lastOp == nil {
slog.Warn("Last PLC operation is not a regular op, skipping auto-update", "did", did)
return nil
}
sigPub, err := signingKey.PublicKey()
if err != nil {
return fmt.Errorf("did: failed to get signing public key: %w", err)
}
localKey := sigPub.DIDKey()
plcKey := lastOp.VerificationMethods[cfg.VerificationKeyName]
keyMatch := localKey == plcKey
servicesMatch := true
for name, svc := range cfg.Services {
plcSvc, ok := lastOp.Services[name]
if !ok || plcSvc.Type != svc.Type || plcSvc.Endpoint != svc.Endpoint {
servicesMatch = false
break
}
}
if keyMatch && servicesMatch {
slog.Info("PLC identity is current", "did", did)
return nil
}
slog.Info("PLC identity needs update",
"did", did, "signing_key_changed", !keyMatch, "services_changed", !servicesMatch)
if rotationKey == nil {
slog.Warn("PLC document doesn't match local state but no rotation key available. Provide rotation key to auto-update.",
"did", did, "signing_key_changed", !keyMatch, "services_changed", !servicesMatch)
return nil
}
rotPub, err := rotationKey.PublicKey()
if err != nil {
return fmt.Errorf("did: failed to get rotation public key: %w", err)
}
localRotKey := rotPub.DIDKey()
// Verify the local rotation key still has authority on the PLC document.
// If it's been rotated out (possibly maliciously), refuse to submit — PLC would
// reject anyway, and silent failure here would be confusing.
localRotKeyPresent := slices.Contains(lastOp.RotationKeys, localRotKey)
if !localRotKeyPresent {
slog.Warn("Local rotation key not present in PLC document — refusing to update. Possible compromise or out-of-band rotation. Recover with offline key if available.",
"did", did, "local_rotation_key", localRotKey, "plc_rotation_keys", lastOp.RotationKeys)
return nil
}
host, err := hostWithPort(cfg.PublicURL)
if err != nil {
return err
}
prevCID := lastEntry.AsOperation().CID().String()
op := &didplc.RegularOp{
Type: "plc_operation",
RotationKeys: lastOp.RotationKeys,
VerificationMethods: map[string]string{
cfg.VerificationKeyName: localKey,
},
AlsoKnownAs: []string{"at://" + host},
Services: toOpServices(cfg.Services),
Prev: &prevCID,
}
if err := op.Sign(rotationKey); err != nil {
return fmt.Errorf("did: failed to sign update operation: %w", err)
}
if err := client.Submit(ctx, did, op); err != nil {
return fmt.Errorf("did: failed to submit update: %w", err)
}
slog.Info("Updated PLC identity",
"did", did, "signing_key_rotated", !keyMatch, "services_changed", !servicesMatch)
return nil
}
func toOpServices(in map[string]Service) map[string]didplc.OpService {
out := make(map[string]didplc.OpService, len(in))
for name, svc := range in {
out[name] = didplc.OpService{Type: svc.Type, Endpoint: svc.Endpoint}
}
return out
}
func parseOptionalMultibaseKey(encoded string) (atcrypto.PrivateKeyExportable, error) {
if encoded == "" {
return nil, nil
}
key, err := atcrypto.ParsePrivateMultibase(encoded)
if err != nil {
return nil, fmt.Errorf("did: failed to parse multibase key: %w", err)
}
return key, nil
}