diff --git a/api/plc.go b/api/plc.go index 94c62e7c..fc2775ae 100644 --- a/api/plc.go +++ b/api/plc.go @@ -13,7 +13,6 @@ import ( "net/url" "strings" - key "github.com/bluesky-social/indigo/key" did "github.com/whyrusleeping/go-did" otel "go.opentelemetry.io/otel" ) @@ -63,14 +62,14 @@ type CreateOp struct { Sig string `json:"sig" cborgen:"sig,omitempty"` } -func (s *PLCServer) CreateDID(ctx context.Context, sigkey *key.Key, recovery string, handle string, service string) (string, error) { +func (s *PLCServer) CreateDID(ctx context.Context, sigkey *did.PrivKey, recovery string, handle string, service string) (string, error) { if s.C == nil { s.C = http.DefaultClient } op := CreateOp{ Type: "create", - SigningKey: sigkey.DID(), + SigningKey: sigkey.Public().DID(), RecoveryKey: recovery, Handle: handle, Service: service, diff --git a/api/plc_test.go b/api/plc_test.go index 63f1b896..70f61fd2 100644 --- a/api/plc_test.go +++ b/api/plc_test.go @@ -8,8 +8,8 @@ import ( "fmt" "testing" - key "github.com/bluesky-social/indigo/key" "github.com/lestrrat-go/jwx/jwk" + did "github.com/whyrusleeping/go-did" ) type testVector struct { @@ -88,12 +88,12 @@ func TestPLCCreateVector(t *testing.T) { t.Fatal(err) } - mk := key.Key{ + mk := did.PrivKey{ Raw: &spk, - Type: "P-256", + Type: did.KeyTypeP256, } - if mk.DID() != tv.Did { + if mk.Public().DID() != tv.Did { t.Fatal("keys generated different DIDs") } diff --git a/bgs/bgs.go b/bgs/bgs.go index 46511794..8878ec33 100644 --- a/bgs/bgs.go +++ b/bgs/bgs.go @@ -175,13 +175,14 @@ func (bgs *BGS) handleFedEvent(ctx context.Context, host *models.PDS, evt *event u = new(User) u.ID = subj.Uid + u.Did = evt.Repo } // TODO: if the user is already in the 'slow' path, we shouldnt even bother trying to fast path this event - if err := bgs.repoman.HandleExternalUserEvent(ctx, host.ID, u.ID, evt.RepoAppend.Prev, evt.RepoAppend.Ops, evt.RepoAppend.Car); err != nil { + if err := bgs.repoman.HandleExternalUserEvent(ctx, host.ID, u.ID, u.Did, evt.RepoAppend.Prev, evt.RepoAppend.Ops, evt.RepoAppend.Car); err != nil { if !errors.Is(err, carstore.ErrRepoBaseMismatch) { - return err + return fmt.Errorf("handle user event failed: %w", err) } ai, err := bgs.Index.LookupUser(ctx, u.ID) diff --git a/carstore/repo_test.go b/carstore/repo_test.go index 4d8eba26..7697b012 100644 --- a/carstore/repo_test.go +++ b/carstore/repo_test.go @@ -12,6 +12,7 @@ import ( "github.com/bluesky-social/indigo/api" "github.com/bluesky-social/indigo/repo" + "github.com/bluesky-social/indigo/util" sqlbs "github.com/ipfs/go-bs-sqlite3" "github.com/ipfs/go-cid" flatfs "github.com/ipfs/go-ds-flatfs" @@ -110,7 +111,8 @@ func TestBasicOperation(t *testing.T) { t.Fatal(err) } - nroot, err := rr.Commit(ctx) + kmgr := &util.FakeKeyManager{} + nroot, err := rr.Commit(ctx, kmgr.SignForUser) if err != nil { t.Fatal(err) } @@ -132,7 +134,7 @@ func TestBasicOperation(t *testing.T) { } func setupRepo(ctx context.Context, bs blockstore.Blockstore) (cid.Cid, error) { - nr := repo.NewRepo(ctx, bs) + nr := repo.NewRepo(ctx, "did:foo", bs) if _, _, err := nr.CreateRecord(ctx, "app.bsky.feed.post", &api.PostRecord{ Text: fmt.Sprintf("hey look its a tweet %s", time.Now()), @@ -140,7 +142,8 @@ func setupRepo(ctx context.Context, bs blockstore.Blockstore) (cid.Cid, error) { return cid.Undef, err } - ncid, err := nr.Commit(ctx) + kmgr := &util.FakeKeyManager{} + ncid, err := nr.Commit(ctx, kmgr.SignForUser) if err != nil { return cid.Undef, err } @@ -190,7 +193,8 @@ func BenchmarkRepoWritesCarstore(b *testing.B) { b.Fatal(err) } - nroot, err := rr.Commit(ctx) + kmgr := &util.FakeKeyManager{} + nroot, err := rr.Commit(ctx, kmgr.SignForUser) if err != nil { b.Fatal(err) } @@ -232,7 +236,8 @@ func BenchmarkRepoWritesFlatfs(b *testing.B) { b.Fatal(err) } - nroot, err := rr.Commit(ctx) + kmgr := &util.FakeKeyManager{} + nroot, err := rr.Commit(ctx, kmgr.SignForUser) if err != nil { b.Fatal(err) } @@ -269,7 +274,8 @@ func BenchmarkRepoWritesSqlite(b *testing.B) { b.Fatal(err) } - nroot, err := rr.Commit(ctx) + kmgr := &util.FakeKeyManager{} + nroot, err := rr.Commit(ctx, kmgr.SignForUser) if err != nil { b.Fatal(err) } diff --git a/cmd/bigsky/main.go b/cmd/bigsky/main.go index 1f45c582..4ca0c0bc 100644 --- a/cmd/bigsky/main.go +++ b/cmd/bigsky/main.go @@ -114,7 +114,10 @@ func main() { return err } - repoman := repomgr.NewRepoManager(db, cstore) + didr := &api.PLCServer{Host: cctx.String("plc")} + kmgr := indexer.NewKeyManager(didr, nil) + + repoman := repomgr.NewRepoManager(db, cstore, kmgr) evtman := events.NewEventManager() @@ -124,8 +127,6 @@ func main() { // indexer just take optional callbacks for notification stuff notifman := notifs.NewNotificationManager(db, repoman.GetRecord) - didr := &api.PLCServer{Host: cctx.String("plc")} - ix, err := indexer.NewIndexer(db, notifman, evtman, didr, repoman, true) if err != nil { return err diff --git a/cmd/gosky/main.go b/cmd/gosky/main.go index cd19acf7..16225dff 100644 --- a/cmd/gosky/main.go +++ b/cmd/gosky/main.go @@ -15,7 +15,6 @@ import ( atproto "github.com/bluesky-social/indigo/api/atproto" bsky "github.com/bluesky-social/indigo/api/bsky" cliutil "github.com/bluesky-social/indigo/cmd/gosky/util" - "github.com/bluesky-social/indigo/key" "github.com/bluesky-social/indigo/repo" "github.com/ipfs/go-cid" "github.com/lestrrat-go/jwx/jwa" @@ -24,6 +23,7 @@ import ( rejson "github.com/polydawn/refmt/json" "github.com/polydawn/refmt/shared" cli "github.com/urfave/cli/v2" + "github.com/whyrusleeping/go-did" ) func main() { @@ -202,7 +202,7 @@ var didCreateCmd = &cli.Command{ return err } - fmt.Println("KEYDID: ", sigkey.DID()) + fmt.Println("KEYDID: ", sigkey.Public().DID()) ndid, err := s.CreateDID(context.TODO(), sigkey, recoverydid, handle, service) if err != nil { @@ -214,7 +214,7 @@ var didCreateCmd = &cli.Command{ }, } -func loadKey(kfile string) (*key.Key, error) { +func loadKey(kfile string) (*did.PrivKey, error) { kb, err := os.ReadFile(kfile) if err != nil { return nil, err @@ -234,9 +234,18 @@ func loadKey(kfile string) (*key.Key, error) { return nil, fmt.Errorf("need a curve set") } - return &key.Key{ + var out string + kts := string(curve.(jwa.EllipticCurveAlgorithm)) + switch kts { + case "P-256": + out = did.KeyTypeP256 + default: + return nil, fmt.Errorf("unrecognized key type: %s", kts) + } + + return &did.PrivKey{ Raw: &spk, - Type: string(curve.(jwa.EllipticCurveAlgorithm)), + Type: out, }, nil } diff --git a/cmd/stress/main.go b/cmd/stress/main.go index 43b1fc91..9b991998 100644 --- a/cmd/stress/main.go +++ b/cmd/stress/main.go @@ -135,7 +135,7 @@ var genRepoCmd = &cli.Command{ ctx := context.Background() - r := repo.NewRepo(ctx, membs) + r := repo.NewRepo(ctx, "did:plc:foobar", membs) root, err := testing.GenerateFakeRepo(r, l) if err != nil { diff --git a/go.mod b/go.mod index 92366706..838481a2 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/ipfs/go-log/v2 v2.5.1 github.com/ipld/go-car v0.5.0 github.com/ipld/go-car/v2 v2.5.1 + github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52 github.com/labstack/echo/v4 v4.10.0 github.com/lestrrat-go/jwx v1.2.25 github.com/lestrrat-go/jwx/v2 v2.0.8 @@ -29,7 +30,7 @@ require ( github.com/stretchr/testify v1.8.1 github.com/urfave/cli/v2 v2.23.7 github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa - github.com/whyrusleeping/go-did v0.0.0-20221105001742-8d9e0ffb0d59 + github.com/whyrusleeping/go-did v0.0.0-20230210051655-85c9ba6709ab go.opentelemetry.io/otel v1.11.2 go.opentelemetry.io/otel/exporters/jaeger v1.11.2 go.opentelemetry.io/otel/sdk v1.11.2 diff --git a/go.sum b/go.sum index 61e006b2..cac16871 100644 --- a/go.sum +++ b/go.sum @@ -101,7 +101,6 @@ github.com/ipfs/go-car v0.0.4 h1:zLhxykvk4SFU4oIpgcIoiolVL3jqcK0hjqcQfUSs4dk= github.com/ipfs/go-car v0.0.4/go.mod h1:eZX0EppfsvSQN8IsJnx57bheogWMgQjJVWU/fDA7ySQ= github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= -github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= github.com/ipfs/go-cid v0.0.4/go.mod h1:4LLaPOQwmk5z9LBgQnpkivrx8BJjUyGwTXCd5Xfj6+M= github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog= github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= @@ -144,8 +143,6 @@ github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyB github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ= github.com/ipfs/go-ipld-cbor v0.0.2/go.mod h1:wTBtrQZA3SoFKMVkp6cn6HMRteIB1VsmHA0AQFOn7Nc= -github.com/ipfs/go-ipld-cbor v0.0.6 h1:pYuWHyvSpIsOOLw4Jy7NbBkCyzLDcl64Bf/LZW7eBQ0= -github.com/ipfs/go-ipld-cbor v0.0.6/go.mod h1:ssdxxaLJPXH7OjF5V4NSjBbcfh+evoR4ukuru0oPXMA= github.com/ipfs/go-ipld-cbor v0.0.7-0.20230126201833-a73d038d90bc h1:eUEo764smNy0EVRuMTSmirmuh552Mf2aBjfpDcLnDa8= github.com/ipfs/go-ipld-cbor v0.0.7-0.20230126201833-a73d038d90bc/go.mod h1:X7SgEIwC4COC5OWfcepZBWafO5kA1Rmt9ZsLLbhihQk= github.com/ipfs/go-ipld-format v0.0.1/go.mod h1:kyJtbkDALmFHv3QR6et67i35QzO3S0dCDnkOJhcZkms= @@ -185,6 +182,8 @@ github.com/ipld/go-ipld-prime v0.9.1-0.20210324083106-dc342a9917db/go.mod h1:KvB github.com/ipld/go-ipld-prime v0.19.0 h1:5axC7rJmPc17Emw6TelxGwnzALk0PdupZ2oj2roDj04= github.com/ipld/go-ipld-prime v0.19.0/go.mod h1:Q9j3BaVXwaA3o5JUDNvptDDr/x8+F7FG6XJ8WI3ILg4= github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20211210234204-ce2a1c70cd73 h1:TsyATB2ZRRQGTwafJdgEUQkmjOExRV0DNokcihZxbnQ= +github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52 h1:QG4CGBqCeuBo6aZlGAamSkxWdgWfZGeE49eUOWJPA4c= +github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52/go.mod h1:fdg+/X9Gg4AsAIzWpEHwnqd+QY3b7lajxyjE1m4hkq4= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= @@ -442,14 +441,13 @@ github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a/go.mod h1:x6AKhvS github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= -github.com/whyrusleeping/cbor-gen v0.0.0-20200123233031-1cdf64d27158/go.mod h1:Xj/M2wWU+QdTdRbu/L/1dIZY8/Wb2K9pAhtroQuxJJI= -github.com/whyrusleeping/cbor-gen v0.0.0-20230109192608-0173f1e641ac h1:vSeRURgERu0v7h+bKvlP0wuT+inofyu61R15qka/Xh0= -github.com/whyrusleeping/cbor-gen v0.0.0-20230109192608-0173f1e641ac/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa h1:EyA027ZAkuaCLoxVX4r1TZMPy1d31fM6hbfQ4OU4I5o= github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= -github.com/whyrusleeping/go-did v0.0.0-20221105001742-8d9e0ffb0d59 h1:dRYr/sfpZjX8evmbFrOG7ldkzdk5TLMGRVM40k1AZPQ= -github.com/whyrusleeping/go-did v0.0.0-20221105001742-8d9e0ffb0d59/go.mod h1:mX/AQ/SS9KrCwO8V+IWyIozytxw5gw75cMHymoJvMGo= +github.com/whyrusleeping/go-did v0.0.0-20230209234736-e14671c25e01 h1:YPGD8CeME7k4QgW0/lb1bR8nYSQDQbxoTPqb4zKke78= +github.com/whyrusleeping/go-did v0.0.0-20230209234736-e14671c25e01/go.mod h1:mX/AQ/SS9KrCwO8V+IWyIozytxw5gw75cMHymoJvMGo= +github.com/whyrusleeping/go-did v0.0.0-20230210051655-85c9ba6709ab h1:u1P8OjfkqR7LGZV91TMS76NEecUPa/boX29FBMNq+nw= +github.com/whyrusleeping/go-did v0.0.0-20230210051655-85c9ba6709ab/go.mod h1:qPtRyexGM5XMHFIfjH+EiA/A/1n2JakWEdMPC53pJAE= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM= github.com/whyrusleeping/go-notifier v0.0.0-20170827234753-097c5d47330f/go.mod h1:cZNvX9cFybI01GriPRMXDtczuvUhgbcYr9iCGaNlRv8= diff --git a/indexer/keymgr.go b/indexer/keymgr.go new file mode 100644 index 00000000..2eeb8e6f --- /dev/null +++ b/indexer/keymgr.go @@ -0,0 +1,62 @@ +package indexer + +import ( + "context" + "crypto" + "fmt" + "time" + + "github.com/bluesky-social/indigo/plc" + did "github.com/whyrusleeping/go-did" +) + +type KeyManager struct { + didr plc.PLCClient + + signingKey *did.PrivKey +} + +func NewKeyManager(didr plc.PLCClient, k *did.PrivKey) *KeyManager { + return &KeyManager{ + didr: didr, + signingKey: k, + } +} + +type cachedKey struct { + cachedAt time.Time + pub crypto.PublicKey +} + +func (km *KeyManager) VerifyUserSignature(ctx context.Context, did string, sig []byte, msg []byte) error { + k, err := km.getKey(ctx, did) + if err != nil { + return err + } + + return k.Verify(msg, sig) +} + +func (km *KeyManager) getKey(ctx context.Context, did string) (*did.PubKey, error) { + // TODO: caching should be done at the DID document level, that way we can + // have a thing that subscribes to plc updates for cache busting + doc, err := km.didr.GetDocument(ctx, did) + if err != nil { + return nil, err + } + + pubk, err := doc.GetPublicKey("#signingKey") + if err != nil { + return nil, err + } + + return pubk, nil +} + +func (km *KeyManager) SignForUser(ctx context.Context, did string, msg []byte) ([]byte, error) { + if km.signingKey == nil { + return nil, fmt.Errorf("key manager does not have a signing key, cannot sign") + } + + return km.signingKey.Sign(msg) +} diff --git a/indexer/posts_test.go b/indexer/posts_test.go index 44eb4e89..a156d2a3 100644 --- a/indexer/posts_test.go +++ b/indexer/posts_test.go @@ -56,7 +56,7 @@ func testIndexer(t *testing.T) *testIx { t.Fatal(err) } - repoman := repomgr.NewRepoManager(maindb, cs) + repoman := repomgr.NewRepoManager(maindb, cs, &util.FakeKeyManager{}) notifman := notifs.NewNotificationManager(maindb, repoman.GetRecord) evtman := events.NewEventManager() diff --git a/key/key.go b/key/key.go deleted file mode 100644 index 0600b112..00000000 --- a/key/key.go +++ /dev/null @@ -1,87 +0,0 @@ -package key - -import ( - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/sha256" - "crypto/x509" - "fmt" - - "github.com/multiformats/go-multibase" - "github.com/multiformats/go-varint" -) - -const ( - MCed25519 = 0xED - MCP256 = 0x1200 -) - -type Key struct { - Raw interface{} - Type string -} - -func (k *Key) Sign(b []byte) ([]byte, error) { - switch k.Type { - case "ed25519": - return ed25519.Sign(k.Raw.(ed25519.PrivateKey), b), nil - case "P-256": - h := sha256.Sum256(b) - //return ecdsa.SignASN1(rand.Reader, k.Raw.(*ecdsa.PrivateKey), h[:]) - r, s, err := ecdsa.Sign(rand.Reader, k.Raw.(*ecdsa.PrivateKey), h[:]) - if err != nil { - return nil, err - } - - return append(r.Bytes(), s.Bytes()...), nil - default: - return nil, fmt.Errorf("unsupported key type: %s", k.Type) - } -} - -func (k *Key) DID() string { - var buf []byte - switch k.Type { - case "ed25519": - kb := k.Raw.(ed25519.PrivateKey) - buf := make([]byte, 8+len(kb)) - n := varint.PutUvarint(buf, MCed25519) - copy(buf[n:], kb) - buf = buf[:n+len(kb)] - case "P-256": - sk := k.Raw.(*ecdsa.PrivateKey) - enc := elliptic.MarshalCompressed(elliptic.P256(), sk.X, sk.Y) - - buf = make([]byte, 8+len(enc)) - n := varint.PutUvarint(buf, MCP256) - copy(buf[n:], enc) - buf = buf[:n+len(enc)] - default: - return "" - } - - kstr, err := multibase.Encode(multibase.Base58BTC, buf) - if err != nil { - panic(err) - } - - return "did:key:" + kstr -} - -func (k *Key) RawBytes() ([]byte, error) { - switch k.Type { - case "ed25519": - return k.Raw.([]byte), nil - case "P-256": - b, err := x509.MarshalECPrivateKey(k.Raw.(*ecdsa.PrivateKey)) - if err != nil { - return nil, err - } - - return b, nil - default: - return nil, fmt.Errorf("unsupported key type: %q", k.Type) - } -} diff --git a/pds/auth.go b/pds/auth.go index 28d16d15..6f796aa7 100644 --- a/pds/auth.go +++ b/pds/auth.go @@ -55,7 +55,7 @@ func (s *Server) createCrossServerAuthToken(ctx context.Context, otherpds string // setting this is a little weird, // since the token isn't signed by this key, we dont have a way to validate... - accessTok.Set("pds", s.signingKey.DID()) + accessTok.Set("pds", s.signingKey.Public().DID()) rval := make([]byte, 10) rand.Read(rval) diff --git a/pds/fedmgr.go b/pds/fedmgr.go index 25474007..1a4af5aa 100644 --- a/pds/fedmgr.go +++ b/pds/fedmgr.go @@ -10,10 +10,10 @@ import ( "time" "github.com/bluesky-social/indigo/events" - "github.com/bluesky-social/indigo/key" "github.com/gorilla/websocket" cbg "github.com/whyrusleeping/cbor-gen" + "github.com/whyrusleeping/go-did" "gorm.io/gorm" ) @@ -27,10 +27,10 @@ type Slurper struct { cb IndexCallback db *gorm.DB - signingKey *key.Key + signingKey *did.PrivKey } -func NewSlurper(cb IndexCallback, db *gorm.DB, signingKey *key.Key) Slurper { +func NewSlurper(cb IndexCallback, db *gorm.DB, signingKey *did.PrivKey) Slurper { return Slurper{ cb: cb, db: db, @@ -55,7 +55,7 @@ func (s *Slurper) subscribeWithRedialer(host *Peering) { var backoff int for { h := http.Header{ - "DID": []string{s.signingKey.DID()}, + "DID": []string{s.signingKey.Public().DID()}, } con, res, err := d.Dial("ws://"+host.Host+"/events", h) diff --git a/pds/handlers.go b/pds/handlers.go index f24d6abb..a69c9266 100644 --- a/pds/handlers.go +++ b/pds/handlers.go @@ -394,7 +394,7 @@ func (s *Server) handleComAtprotoAccountCreate(ctx context.Context, input *comat } if recoveryKey == "" { - recoveryKey = s.signingKey.DID() + recoveryKey = s.signingKey.Public().DID() } d, err := s.plc.CreateDID(ctx, s.signingKey, recoveryKey, input.Handle, s.serviceUrl) @@ -461,7 +461,7 @@ func (s *Server) handleComAtprotoBlobUpload(ctx context.Context, r io.Reader, ct func (s *Server) handleComAtprotoHandleResolve(ctx context.Context, handle string) (*comatprototypes.HandleResolve_Output, error) { if handle == "" { - return &comatprototypes.HandleResolve_Output{Did: s.signingKey.DID()}, nil + return &comatprototypes.HandleResolve_Output{Did: s.signingKey.Public().DID()}, nil } u, err := s.lookupUserByHandle(ctx, handle) if err != nil { diff --git a/pds/server.go b/pds/server.go index 2904c8ae..7680c032 100644 --- a/pds/server.go +++ b/pds/server.go @@ -17,7 +17,6 @@ import ( "github.com/bluesky-social/indigo/carstore" "github.com/bluesky-social/indigo/events" "github.com/bluesky-social/indigo/indexer" - "github.com/bluesky-social/indigo/key" "github.com/bluesky-social/indigo/lex/util" "github.com/bluesky-social/indigo/models" "github.com/bluesky-social/indigo/notifs" @@ -33,6 +32,7 @@ import ( "github.com/lestrrat-go/jwx/jwa" jwk "github.com/lestrrat-go/jwx/jwk" jwt "github.com/lestrrat-go/jwx/jwt" + "github.com/whyrusleeping/go-did" "gorm.io/gorm" ) @@ -47,7 +47,7 @@ type Server struct { indexer *indexer.Indexer events *events.EventManager slurper *Slurper - signingKey *key.Key + signingKey *did.PrivKey echo *echo.Echo jwtSigningKey []byte enforcePeering bool @@ -72,7 +72,9 @@ func NewServer(db *gorm.DB, cs *carstore.CarStore, kfile string, handleSuffix, s evtman := events.NewEventManager() - repoman := repomgr.NewRepoManager(db, cs) + kmgr := indexer.NewKeyManager(didr, serkey) + + repoman := repomgr.NewRepoManager(db, cs, kmgr) notifman := notifs.NewNotificationManager(db, repoman.GetRecord) ix, err := indexer.NewIndexer(db, notifman, evtman, didr, repoman, false) @@ -145,7 +147,7 @@ func (s *Server) handleFedEvent(ctx context.Context, host *Peering, evt *events. u.ID = subj.Uid } - return s.repoman.HandleExternalUserEvent(ctx, host.ID, u.ID, evt.RepoAppend.Prev, evt.RepoAppend.Ops, evt.RepoAppend.Car) + return s.repoman.HandleExternalUserEvent(ctx, host.ID, u.ID, u.Did, evt.RepoAppend.Prev, evt.RepoAppend.Ops, evt.RepoAppend.Car) default: return fmt.Errorf("invalid fed event") } @@ -292,7 +294,7 @@ func (s *Server) readRecordFunc(ctx context.Context, user uint, c cid.Cid) (util return util.CborDecodeValue(blk.RawData()) } -func loadKey(kfile string) (*key.Key, error) { +func loadKey(kfile string) (*did.PrivKey, error) { kb, err := os.ReadFile(kfile) if err != nil { return nil, err @@ -312,9 +314,18 @@ func loadKey(kfile string) (*key.Key, error) { return nil, fmt.Errorf("need a curve set") } - return &key.Key{ + var out string + kts := string(curve.(jwa.EllipticCurveAlgorithm)) + switch kts { + case "P-256": + out = did.KeyTypeP256 + default: + return nil, fmt.Errorf("unrecognized key type: %s", kts) + } + + return &did.PrivKey{ Raw: &spk, - Type: string(curve.(jwa.EllipticCurveAlgorithm)), + Type: out, }, nil } diff --git a/plc/fakedid.go b/plc/fakedid.go index 62d931eb..8c972b4c 100644 --- a/plc/fakedid.go +++ b/plc/fakedid.go @@ -5,16 +5,17 @@ import ( "crypto/rand" "encoding/hex" - "github.com/bluesky-social/indigo/key" "github.com/whyrusleeping/go-did" "gorm.io/gorm" ) type FakeDidMapping struct { gorm.Model - Handle string - Did string `gorm:"index"` - Service string + Handle string + Did string `gorm:"index"` + Service string + KeyType string + PubKeyMbase string } type FakeDid struct { @@ -46,7 +47,14 @@ func (fd *FakeDid) GetDocument(ctx context.Context, udid string) (*did.Document, //Authentication []interface{} `json:"authentication"` - //VerificationMethod []VerificationMethod `json:"verificationMethod"` + VerificationMethod: []did.VerificationMethod{ + did.VerificationMethod{ + ID: "#signingKey", + Type: rec.KeyType, + PublicKeyMultibase: &rec.PubKeyMbase, + Controller: rec.Did, + }, + }, Service: []did.Service{ did.Service{ @@ -58,15 +66,17 @@ func (fd *FakeDid) GetDocument(ctx context.Context, udid string) (*did.Document, }, nil } -func (fd *FakeDid) CreateDID(ctx context.Context, sigkey *key.Key, recovery string, handle string, service string) (string, error) { +func (fd *FakeDid) CreateDID(ctx context.Context, sigkey *did.PrivKey, recovery string, handle string, service string) (string, error) { buf := make([]byte, 8) rand.Read(buf) d := "did:plc:" + hex.EncodeToString(buf) if err := fd.db.Create(&FakeDidMapping{ - Handle: handle, - Did: d, - Service: service, + Handle: handle, + Did: d, + Service: service, + PubKeyMbase: sigkey.Public().MultibaseString(), + KeyType: sigkey.KeyType(), }).Error; err != nil { return "", err } diff --git a/plc/plc.go b/plc/plc.go index d9b2ec32..6357795b 100644 --- a/plc/plc.go +++ b/plc/plc.go @@ -3,11 +3,10 @@ package plc import ( "context" - "github.com/bluesky-social/indigo/key" "github.com/whyrusleeping/go-did" ) type PLCClient interface { GetDocument(ctx context.Context, didstr string) (*did.Document, error) - CreateDID(ctx context.Context, sigkey *key.Key, recovery string, handle string, service string) (string, error) + CreateDID(ctx context.Context, sigkey *did.PrivKey, recovery string, handle string, service string) (string, error) } diff --git a/repo/repo.go b/repo/repo.go index 53eb64c9..5cb0b893 100644 --- a/repo/repo.go +++ b/repo/repo.go @@ -85,12 +85,19 @@ func ReadRepoFromCar(ctx context.Context, r io.Reader) (*Repo, error) { return OpenRepo(ctx, bs, root) } -func NewRepo(ctx context.Context, bs blockstore.Blockstore) *Repo { +func NewRepo(ctx context.Context, did string, bs blockstore.Blockstore) *Repo { cst := util.CborStore(bs) t := mst.NewMST(cst, 32, cid.Undef, []mst.NodeEntry{}, 0) + meta := Meta{ + Datastore: "TODO", + Did: did, + Version: 1, + } + return &Repo{ + meta: meta, cst: cst, bs: bs, mst: t, @@ -106,11 +113,22 @@ func OpenRepo(ctx context.Context, bs blockstore.Blockstore, root cid.Cid) (*Rep return nil, fmt.Errorf("loading root from blockstore: %w", err) } + var rt Root + if err := cst.Get(ctx, sr.Root, &rt); err != nil { + return nil, fmt.Errorf("loading root: %w", err) + } + + var meta Meta + if err := cst.Get(ctx, rt.Meta, &meta); err != nil { + return nil, fmt.Errorf("loading meta: %w", err) + } + return &Repo{ sr: sr, bs: bs, cst: cst, repoCid: root, + meta: meta, }, nil } @@ -118,8 +136,24 @@ type CborMarshaler interface { MarshalCBOR(w io.Writer) error } -func (r *Repo) PrevCommit(ctx context.Context) (*cid.Cid, error) { +func (r *Repo) MetaCid(ctx context.Context) (cid.Cid, error) { + var root Root + if err := r.cst.Get(ctx, r.sr.Root, &root); err != nil { + return cid.Undef, err + } + + return root.Meta, nil +} +func (r *Repo) RepoDid() string { + if r.meta.Did == "" { + panic("repo has unset did") + } + + return r.meta.Did +} + +func (r *Repo) PrevCommit(ctx context.Context) (*cid.Cid, error) { var c Root if err := r.cst.Get(ctx, r.sr.Root, &c); err != nil { return nil, fmt.Errorf("loading previous commit: %w", err) @@ -132,6 +166,10 @@ func (r *Repo) CommitRoot() cid.Cid { return r.sr.Root } +func (r *Repo) SignedCommit() SignedCommit { + return r.sr +} + func (r *Repo) Blockstore() blockstore.Blockstore { return r.bs } @@ -205,7 +243,7 @@ func (r *Repo) DeleteRecord(ctx context.Context, rpath string) error { return nil } -func (r *Repo) Commit(ctx context.Context) (cid.Cid, error) { +func (r *Repo) Commit(ctx context.Context, signer func(context.Context, string, []byte) ([]byte, error)) (cid.Cid, error) { ctx, span := otel.Tracer("repo").Start(ctx, "Commit") defer span.End() @@ -235,6 +273,7 @@ func (r *Repo) Commit(ctx context.Context) (cid.Cid, error) { if err != nil { return cid.Undef, err } + fmt.Println("PUT NEW META: ", mcid) nroot.Meta = mcid } @@ -243,8 +282,16 @@ func (r *Repo) Commit(ctx context.Context) (cid.Cid, error) { return cid.Undef, err } + did := r.RepoDid() + + sig, err := signer(ctx, did, ncomcid.Bytes()) + if err != nil { + return cid.Undef, fmt.Errorf("failed to sign root: %w", err) + } + nsroot := SignedCommit{ Root: ncomcid, + Sig: sig, } nsrootcid, err := r.cst.Put(ctx, &nsroot) diff --git a/repomgr/ingest_test.go b/repomgr/ingest_test.go index 7ca75178..43afd1fd 100644 --- a/repomgr/ingest_test.go +++ b/repomgr/ingest_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/bluesky-social/indigo/carstore" + "github.com/bluesky-social/indigo/util" "github.com/ipfs/go-cid" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -53,7 +54,7 @@ func TestLoadNewRepo(t *testing.T) { t.Fatal(err) } - repoman := NewRepoManager(maindb, cs) + repoman := NewRepoManager(maindb, cs, &util.FakeKeyManager{}) fi, err := os.Open("testrepo.car") if err != nil { diff --git a/repomgr/repomgr.go b/repomgr/repomgr.go index a6cb5d1e..1597f86f 100644 --- a/repomgr/repomgr.go +++ b/repomgr/repomgr.go @@ -30,23 +30,30 @@ import ( var log = logging.Logger("repomgr") -func NewRepoManager(db *gorm.DB, cs *carstore.CarStore) *RepoManager { +func NewRepoManager(db *gorm.DB, cs *carstore.CarStore, kmgr KeyManager) *RepoManager { db.AutoMigrate(RepoHead{}) return &RepoManager{ db: db, cs: cs, userLocks: make(map[uint]*userLock), + kmgr: kmgr, } } +type KeyManager interface { + VerifyUserSignature(context.Context, string, []byte, []byte) error + SignForUser(context.Context, string, []byte) ([]byte, error) +} + func (rm *RepoManager) SetEventHandler(cb func(context.Context, *RepoEvent)) { rm.events = cb } type RepoManager struct { - cs *carstore.CarStore - db *gorm.DB + cs *carstore.CarStore + db *gorm.DB + kmgr KeyManager lklk sync.Mutex userLocks map[uint]*userLock @@ -193,7 +200,7 @@ func (rm *RepoManager) CreateRecord(ctx context.Context, user uint, collection s return "", cid.Undef, err } - nroot, err := r.Commit(ctx) + nroot, err := r.Commit(ctx, rm.kmgr.SignForUser) if err != nil { return "", cid.Undef, err } @@ -255,7 +262,7 @@ func (rm *RepoManager) UpdateRecord(ctx context.Context, user uint, collection, return cid.Undef, err } - nroot, err := r.Commit(ctx) + nroot, err := r.Commit(ctx, rm.kmgr.SignForUser) if err != nil { return cid.Undef, err } @@ -316,7 +323,7 @@ func (rm *RepoManager) DeleteRecord(ctx context.Context, user uint, collection, return err } - nroot, err := r.Commit(ctx) + nroot, err := r.Commit(ctx, rm.kmgr.SignForUser) if err != nil { return err } @@ -366,7 +373,7 @@ func (rm *RepoManager) InitNewActor(ctx context.Context, user uint, handle, did, return err } - r := repo.NewRepo(ctx, ds) + r := repo.NewRepo(ctx, did, ds) profile := &bsky.ActorProfile{ DisplayName: displayname, @@ -391,9 +398,9 @@ func (rm *RepoManager) InitNewActor(ctx context.Context, user uint, handle, did, // TODO: set declaration? - root, err := r.Commit(ctx) + root, err := r.Commit(ctx, rm.kmgr.SignForUser) if err != nil { - return err + return fmt.Errorf("committing repo for actor init: %w", err) } rslice, err := ds.CloseWithRoot(ctx, root) @@ -497,7 +504,7 @@ func (rm *RepoManager) GetProfile(ctx context.Context, uid uint) (*bsky.ActorPro return ap, nil } -func (rm *RepoManager) HandleExternalUserEvent(ctx context.Context, pdsid uint, uid uint, prev *cid.Cid, ops []*events.RepoOp, carslice []byte) error { +func (rm *RepoManager) HandleExternalUserEvent(ctx context.Context, pdsid uint, uid uint, did string, prev *cid.Cid, ops []*events.RepoOp, carslice []byte) error { ctx, span := otel.Tracer("repoman").Start(ctx, "HandleExternalUserEvent") defer span.End() @@ -516,6 +523,18 @@ func (rm *RepoManager) HandleExternalUserEvent(ctx context.Context, pdsid uint, return fmt.Errorf("opening external user repo: %w", err) } + repoDid := r.RepoDid() + + if did != repoDid { + return fmt.Errorf("DID in repo did not match (%q != %q)", did, repoDid) + } + + scom := r.SignedCommit() + + if err := rm.kmgr.VerifyUserSignature(ctx, repoDid, scom.Sig, scom.Root.Bytes()); err != nil { + return fmt.Errorf("signature check failed: %w", err) + } + log.Infow("external event", "uid", uid, "ops", ops) var evtops []RepoOp @@ -675,7 +694,7 @@ func (rm *RepoManager) BatchWrite(ctx context.Context, user uint, writes []*atpr } } - nroot, err := r.Commit(ctx) + nroot, err := r.Commit(ctx, rm.kmgr.SignForUser) if err != nil { return err } @@ -813,6 +832,33 @@ func (rm *RepoManager) processNewRepo(ctx context.Context, user uint, r io.Reade membs := blockstore.NewBlockstore(datastore.NewMapDatastore()) + // mild hack: without access to the 'meta' object, we cant properly verify each new repo slice has the right DID in the case of a gap fill procedure + if until.Defined() { + robs, err := rm.cs.ReadOnlySession(user) + if err != nil { + return err + } + + old, err := repo.OpenRepo(ctx, robs, until) + if err != nil { + return err + } + + mcid, err := old.MetaCid(ctx) + if err != nil { + return err + } + + blk, err := robs.Get(ctx, mcid) + if err != nil { + return err + } + + if err := membs.Put(ctx, blk); err != nil { + return err + } + } + for { blk, err := carr.Next() if err != nil { diff --git a/testing/utils.go b/testing/utils.go index adf7d9cf..98ce6292 100644 --- a/testing/utils.go +++ b/testing/utils.go @@ -399,7 +399,9 @@ func SetupBGS(host string, didr plc.PLCClient) (*testBGS, error) { return nil, err } - repoman := repomgr.NewRepoManager(maindb, cs) + kmgr := indexer.NewKeyManager(didr, nil) + + repoman := repomgr.NewRepoManager(maindb, cs, kmgr) notifman := notifs.NewNotificationManager(maindb, repoman.GetRecord) @@ -761,7 +763,9 @@ func GenerateFakeRepo(r *repo.Repo, size int) (cid.Cid, error) { } } - nroot, err := r.Commit(ctx) + kmgr := &bsutil.FakeKeyManager{} + + nroot, err := r.Commit(ctx, kmgr.SignForUser) if err != nil { return cid.Undef, err } diff --git a/util/fakekey.go b/util/fakekey.go new file mode 100644 index 00000000..17514f5b --- /dev/null +++ b/util/fakekey.go @@ -0,0 +1,14 @@ +package util + +import "context" + +type FakeKeyManager struct { +} + +func (km *FakeKeyManager) VerifyUserSignature(context.Context, string, []byte, []byte) error { + return nil +} + +func (km *FakeKeyManager) SignForUser(ctx context.Context, did string, msg []byte) ([]byte, error) { + return []byte("signature"), nil +}