this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

remove didplc (moved to go-didplc)

-1501
-212
cmd/plcli/main.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "io" 8 - "log/slog" 9 - "os" 10 - 11 - "github.com/bluesky-social/indigo/atproto/crypto" 12 - "github.com/bluesky-social/indigo/atproto/syntax" 13 - "tangled.sh/bnewbold.net/cobalt/didplc" 14 - 15 - "github.com/urfave/cli/v2" 16 - ) 17 - 18 - func main() { 19 - app := cli.App{ 20 - Name: "plcli", 21 - Usage: "simple CLI client tool for PLC operations", 22 - } 23 - app.Flags = []cli.Flag{ 24 - &cli.StringFlag{ 25 - Name: "plc-host", 26 - Usage: "method, hostname, and port of PLC registry", 27 - Value: "https://plc.directory", 28 - EnvVars: []string{"PLC_HOST"}, 29 - }, 30 - } 31 - app.Commands = []*cli.Command{ 32 - &cli.Command{ 33 - Name: "resolve", 34 - Usage: "resolve a DID from remote PLC directory", 35 - ArgsUsage: "<did>", 36 - Action: runResolve, 37 - }, 38 - &cli.Command{ 39 - Name: "submit", 40 - Usage: "submit a PLC operation (reads JSON from stdin)", 41 - ArgsUsage: "<did>", 42 - Action: runSubmit, 43 - Flags: []cli.Flag{ 44 - &cli.StringFlag{ 45 - Name: "plc-private-rotation-key", 46 - Usage: "private key used as a rotation key, if operation is not signed (multibase syntax)", 47 - EnvVars: []string{"PLC_PRIVATE_ROTATION_KEY"}, 48 - }, 49 - }, 50 - }, 51 - &cli.Command{ 52 - Name: "oplog", 53 - Usage: "fetch log of operations from PLC directory, for a single DID", 54 - ArgsUsage: "<did>", 55 - Action: runOpLog, 56 - Flags: []cli.Flag{ 57 - &cli.BoolFlag{ 58 - Name: "audit", 59 - Usage: "audit mode, with nullified entries included", 60 - }, 61 - }, 62 - }, 63 - &cli.Command{ 64 - Name: "verify", 65 - Usage: "fetch audit log for a DID, and verify all operations", 66 - ArgsUsage: "<did>", 67 - Action: runVerify, 68 - Flags: []cli.Flag{ 69 - &cli.BoolFlag{ 70 - Name: "audit", 71 - Usage: "audit mode, with nullified entries included", 72 - }, 73 - }, 74 - }, 75 - } 76 - h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) 77 - slog.SetDefault(slog.New(h)) 78 - app.RunAndExitOnError() 79 - } 80 - 81 - func runResolve(cctx *cli.Context) error { 82 - ctx := context.Background() 83 - s := cctx.Args().First() 84 - if s == "" { 85 - fmt.Println("need to provide DID as an argument") 86 - os.Exit(-1) 87 - } 88 - 89 - did, err := syntax.ParseDID(s) 90 - if err != nil { 91 - fmt.Println(err) 92 - os.Exit(-1) 93 - } 94 - 95 - c := didplc.Client{ 96 - DirectoryURL: cctx.String("plc-host"), 97 - } 98 - doc, err := c.Resolve(ctx, did.String()) 99 - if err != nil { 100 - return err 101 - } 102 - jsonBytes, err := json.Marshal(&doc) 103 - if err != nil { 104 - return err 105 - } 106 - fmt.Println(string(jsonBytes)) 107 - return nil 108 - } 109 - 110 - func runSubmit(cctx *cli.Context) error { 111 - ctx := context.Background() 112 - s := cctx.Args().First() 113 - if s == "" { 114 - fmt.Println("need to provide DID as an argument") 115 - os.Exit(-1) 116 - } 117 - 118 - did, err := syntax.ParseDID(s) 119 - if err != nil { 120 - return err 121 - } 122 - 123 - c := didplc.Client{ 124 - DirectoryURL: cctx.String("plc-host"), 125 - } 126 - 127 - inBytes, err := io.ReadAll(os.Stdin) 128 - if err != nil { 129 - return err 130 - } 131 - var enum didplc.OpEnum 132 - if err := json.Unmarshal(inBytes, &enum); err != nil { 133 - return err 134 - } 135 - op := enum.AsOperation() 136 - 137 - if !op.IsSigned() { 138 - privStr := cctx.String("plc-private-rotation-key") 139 - if privStr == "" { 140 - return fmt.Errorf("operation is not signed and no privte key provided") 141 - } 142 - priv, err := crypto.ParsePrivateMultibase(privStr) 143 - if err != nil { 144 - return err 145 - } 146 - if err := op.Sign(priv); err != nil { 147 - return err 148 - } 149 - } 150 - 151 - entry, err := c.Submit(ctx, did.String(), op) 152 - if err != nil { 153 - return err 154 - } 155 - jsonBytes, err := json.Marshal(&entry) 156 - if err != nil { 157 - return err 158 - } 159 - fmt.Println(string(jsonBytes)) 160 - return nil 161 - } 162 - 163 - func fetchOplog(cctx *cli.Context) ([]didplc.LogEntry, error) { 164 - ctx := context.Background() 165 - s := cctx.Args().First() 166 - if s == "" { 167 - return nil, fmt.Errorf("need to provide DID as an argument") 168 - } 169 - 170 - did, err := syntax.ParseDID(s) 171 - if err != nil { 172 - return nil, err 173 - } 174 - 175 - c := didplc.Client{ 176 - DirectoryURL: cctx.String("plc-host"), 177 - } 178 - entries, err := c.OpLog(ctx, did.String(), cctx.Bool("audit")) 179 - if err != nil { 180 - return nil, err 181 - } 182 - return entries, nil 183 - } 184 - 185 - func runOpLog(cctx *cli.Context) error { 186 - entries, err := fetchOplog(cctx) 187 - if err != nil { 188 - return err 189 - } 190 - 191 - jsonBytes, err := json.Marshal(&entries) 192 - if err != nil { 193 - return err 194 - } 195 - fmt.Println(string(jsonBytes)) 196 - return nil 197 - } 198 - 199 - func runVerify(cctx *cli.Context) error { 200 - entries, err := fetchOplog(cctx) 201 - if err != nil { 202 - return err 203 - } 204 - 205 - err = didplc.VerifyOpLog(entries) 206 - if err != nil { 207 - return err 208 - } 209 - 210 - fmt.Println("valid") 211 - return nil 212 - }
-155
didplc/client.go
··· 1 - package didplc 2 - 3 - import ( 4 - "bytes" 5 - "context" 6 - "encoding/json" 7 - "errors" 8 - "fmt" 9 - "io" 10 - "net/http" 11 - "strings" 12 - 13 - "github.com/bluesky-social/indigo/atproto/crypto" 14 - ) 15 - 16 - // the zero-value of this client is fully functional 17 - type Client struct { 18 - DirectoryURL string 19 - UserAgent *string 20 - HTTPClient http.Client 21 - RotationKey *crypto.PrivateKey 22 - } 23 - 24 - var ( 25 - ErrDIDNotFound = errors.New("DID not found in PLC directory") 26 - DefaultDirectoryURL = "https://plc.directory" 27 - ) 28 - 29 - func (c *Client) Resolve(ctx context.Context, did string) (*Doc, error) { 30 - if !strings.HasPrefix(did, "did:plc:") { 31 - return nil, fmt.Errorf("expected a did:plc, got: %s", did) 32 - } 33 - 34 - plcURL := c.DirectoryURL 35 - if plcURL == "" { 36 - plcURL = DefaultDirectoryURL 37 - } 38 - 39 - url := plcURL + "/" + did 40 - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 41 - if err != nil { 42 - return nil, err 43 - } 44 - if c.UserAgent != nil { 45 - req.Header.Set("User-Agent", *c.UserAgent) 46 - } else { 47 - req.Header.Set("User-Agent", "go-did-method-plc") 48 - } 49 - 50 - resp, err := c.HTTPClient.Do(req) 51 - if err != nil { 52 - return nil, fmt.Errorf("failed did:plc directory resolution: %w", err) 53 - } 54 - if resp.StatusCode == http.StatusNotFound { 55 - return nil, ErrDIDNotFound 56 - } 57 - if resp.StatusCode != http.StatusOK { 58 - return nil, fmt.Errorf("failed did:web well-known fetch, HTTP status: %d", resp.StatusCode) 59 - } 60 - 61 - var doc Doc 62 - if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil { 63 - return nil, fmt.Errorf("failed parse of did:plc document JSON: %w", err) 64 - } 65 - return &doc, nil 66 - } 67 - 68 - func (c *Client) Submit(ctx context.Context, did string, op Operation) (*LogEntry, error) { 69 - if !strings.HasPrefix(did, "did:plc:") { 70 - return nil, fmt.Errorf("expected a did:plc, got: %s", did) 71 - } 72 - 73 - plcURL := c.DirectoryURL 74 - if plcURL == "" { 75 - plcURL = DefaultDirectoryURL 76 - } 77 - 78 - var body io.Reader 79 - b, err := json.Marshal(op) 80 - if err != nil { 81 - return nil, err 82 - } 83 - body = bytes.NewReader(b) 84 - 85 - url := plcURL + "/" + did 86 - req, err := http.NewRequestWithContext(ctx, "POST", url, body) 87 - if err != nil { 88 - return nil, err 89 - } 90 - req.Header.Set("Content-Type", "application/json") 91 - if c.UserAgent != nil { 92 - req.Header.Set("User-Agent", *c.UserAgent) 93 - } else { 94 - req.Header.Set("User-Agent", "go-did-method-plc") 95 - } 96 - 97 - resp, err := c.HTTPClient.Do(req) 98 - if err != nil { 99 - return nil, fmt.Errorf("did:plc operation submission failed: %w", err) 100 - } 101 - if resp.StatusCode == http.StatusNotFound { 102 - return nil, ErrDIDNotFound 103 - } 104 - if resp.StatusCode != http.StatusOK { 105 - return nil, fmt.Errorf("failed did:plc operation submission, HTTP status: %d", resp.StatusCode) 106 - } 107 - 108 - var entry LogEntry 109 - if err := json.NewDecoder(resp.Body).Decode(&entry); err != nil { 110 - return nil, fmt.Errorf("failed parse of did:plc op log entry: %w", err) 111 - } 112 - return &entry, nil 113 - } 114 - 115 - func (c *Client) OpLog(ctx context.Context, did string, audit bool) ([]LogEntry, error) { 116 - if !strings.HasPrefix(did, "did:plc:") { 117 - return nil, fmt.Errorf("expected a did:plc, got: %s", did) 118 - } 119 - 120 - plcURL := c.DirectoryURL 121 - if plcURL == "" { 122 - plcURL = DefaultDirectoryURL 123 - } 124 - 125 - url := plcURL + "/" + did + "/log" 126 - if audit { 127 - url += "/audit" 128 - } 129 - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 130 - if err != nil { 131 - return nil, err 132 - } 133 - if c.UserAgent != nil { 134 - req.Header.Set("User-Agent", *c.UserAgent) 135 - } else { 136 - req.Header.Set("User-Agent", "go-did-method-plc") 137 - } 138 - 139 - resp, err := c.HTTPClient.Do(req) 140 - if err != nil { 141 - return nil, fmt.Errorf("failed did:plc directory resolution: %w", err) 142 - } 143 - if resp.StatusCode == http.StatusNotFound { 144 - return nil, ErrDIDNotFound 145 - } 146 - if resp.StatusCode != http.StatusOK { 147 - return nil, fmt.Errorf("failed did:web well-known fetch, HTTP status: %d", resp.StatusCode) 148 - } 149 - 150 - var entries []LogEntry 151 - if err := json.NewDecoder(resp.Body).Decode(&entries); err != nil { 152 - return nil, fmt.Errorf("failed parse of did:plc document JSON: %w", err) 153 - } 154 - return entries, nil 155 - }
-23
didplc/diddoc.go
··· 1 - package didplc 2 - 3 - import () 4 - 5 - type DocVerificationMethod struct { 6 - ID string `json:"id"` 7 - Type string `json:"type"` 8 - Controller string `json:"controller"` 9 - PublicKeyMultibase string `json:"publicKeyMultibase"` 10 - } 11 - 12 - type DocService struct { 13 - ID string `json:"id"` 14 - Type string `json:"type"` 15 - ServiceEndpoint string `json:"serviceEndpoint"` 16 - } 17 - 18 - type Doc struct { 19 - ID string `json:"id"` 20 - AlsoKnownAs []string `json:"alsoKnownAs,omitempty"` 21 - VerificationMethod []DocVerificationMethod `json:"verificationMethod,omitempty"` 22 - Service []DocService `json:"service,omitempty"` 23 - }
-174
didplc/log.go
··· 1 - package didplc 2 - 3 - import ( 4 - "fmt" 5 - "time" 6 - 7 - "github.com/bluesky-social/indigo/atproto/crypto" 8 - "github.com/bluesky-social/indigo/atproto/syntax" 9 - ) 10 - 11 - type LogEntry struct { 12 - DID string `json:"did"` 13 - Operation OpEnum `json:"operation"` 14 - CID string `json:"cid"` 15 - Nullified bool `json:"nullified"` 16 - CreatedAt string `json:"createdAt"` 17 - } 18 - 19 - // Checks self-consistency of this log entry in isolation. Does not access other context or log entries. 20 - func (le *LogEntry) Validate() error { 21 - 22 - if le.Operation.Regular != nil { 23 - if le.CID != le.Operation.Regular.CID().String() { 24 - return fmt.Errorf("log entry CID didn't match computed operation CID") 25 - } 26 - // NOTE: for non-genesis ops, the rotation key may have bene in a previous op 27 - if le.Operation.Regular.IsGenesis() { 28 - did, err := le.Operation.Regular.DID() 29 - if err != nil { 30 - return err 31 - } 32 - if le.DID != did { 33 - return fmt.Errorf("log entry DID didn't match computed genesis operation DID") 34 - } 35 - if err := VerifySignatureAny(le.Operation.Regular, le.Operation.Regular.RotationKeys); err != nil { 36 - return fmt.Errorf("failed to validate op genesis signature: %v", err) 37 - } 38 - } 39 - } else if le.Operation.Legacy != nil { 40 - if le.CID != le.Operation.Legacy.CID().String() { 41 - return fmt.Errorf("log entry CID didn't match computed operation CID") 42 - } 43 - // NOTE: for non-genesis ops, the rotation key may have bene in a previous op 44 - if le.Operation.Legacy.IsGenesis() { 45 - did, err := le.Operation.Legacy.DID() 46 - if err != nil { 47 - return err 48 - } 49 - if le.DID != did { 50 - return fmt.Errorf("log entry DID didn't match computed genesis operation DID") 51 - } 52 - // TODO: try both signing and recovery key? 53 - pub, err := crypto.ParsePublicDIDKey(le.Operation.Legacy.SigningKey) 54 - if err != nil { 55 - return fmt.Errorf("could not parse recovery key: %v", err) 56 - } 57 - if err := le.Operation.Legacy.VerifySignature(pub); err != nil { 58 - return fmt.Errorf("failed to validate legacy op genesis signature: %v", err) 59 - } 60 - } 61 - } else if le.Operation.Tombstone != nil { 62 - if le.CID != le.Operation.Tombstone.CID().String() { 63 - return fmt.Errorf("log entry CID didn't match computed operation CID") 64 - } 65 - // NOTE: for tombstones, the rotation key is always in a previous op 66 - } else { 67 - return fmt.Errorf("expected tombstone, legacy, or regular PLC operation") 68 - } 69 - 70 - return nil 71 - } 72 - 73 - // checks and ordered list of operations for a single DID. 74 - // 75 - // can be a full audit log (with nullified entries), or a simple log (only "active" entries) 76 - func VerifyOpLog(entries []LogEntry) error { 77 - if len(entries) == 0 { 78 - return fmt.Errorf("can't verify empty operation log") 79 - } 80 - tombstoned := false 81 - earliestNullified := "" 82 - lastTS := "" 83 - var last *RegularOp 84 - var err error 85 - 86 - for _, oe := range entries { 87 - var op RegularOp 88 - 89 - if err = oe.Validate(); err != nil { 90 - return err 91 - } 92 - 93 - if last == nil { 94 - // special processing of first operation 95 - if oe.Operation.Regular != nil { 96 - op = *oe.Operation.Regular 97 - } else if oe.Operation.Legacy != nil { 98 - op = oe.Operation.Legacy.RegularOp() 99 - } else { 100 - return fmt.Errorf("first log entry must be a plc_operation or create (legacy)") 101 - } 102 - 103 - err := VerifySignatureAny(&op, op.RotationKeys) 104 - if err != nil { 105 - return err 106 - } 107 - 108 - if oe.Nullified { 109 - return fmt.Errorf("first log entry can't be nullified") 110 - } 111 - 112 - last = &op 113 - lastTS = oe.CreatedAt 114 - continue 115 - } 116 - 117 - if oe.CreatedAt < lastTS { 118 - return fmt.Errorf("operation log was not ordered by timestamp") 119 - } 120 - if tombstoned { 121 - return fmt.Errorf("account was successfully tombstoned, expect end of op log") 122 - } 123 - 124 - if !oe.Nullified && earliestNullified != "" { 125 - earliest, err := syntax.ParseDatetime(earliestNullified) 126 - if err != nil { 127 - return err 128 - } 129 - current, err := syntax.ParseDatetime(oe.CreatedAt) 130 - if err != nil { 131 - return err 132 - } 133 - if current.Time().Sub(earliest.Time()) > 72*time.Hour { 134 - return fmt.Errorf("time gap between nullified event and overriding event more than recovery window") 135 - } 136 - earliestNullified = "" 137 - } 138 - 139 - if oe.Nullified && earliestNullified == "" { 140 - earliestNullified = oe.CreatedAt 141 - } 142 - 143 - if oe.Operation.Tombstone != nil { 144 - if err := VerifySignatureAny(oe.Operation.Tombstone, last.RotationKeys); err != nil { 145 - return err 146 - } 147 - if oe.Nullified { 148 - continue 149 - } 150 - tombstoned = true 151 - lastTS = oe.CreatedAt 152 - continue 153 - } else if oe.Operation.Regular != nil { 154 - op = *oe.Operation.Regular 155 - } else { 156 - return fmt.Errorf("expected a plc_operation or plc_tombstone operation") 157 - } 158 - 159 - if err := VerifySignatureAny(&op, last.RotationKeys); err != nil { 160 - return err 161 - } 162 - if oe.Nullified { 163 - continue 164 - } else { 165 - last = &op 166 - lastTS = oe.CreatedAt 167 - } 168 - } 169 - 170 - if earliestNullified != "" { 171 - return fmt.Errorf("outstanding 'nullified' op at end of log") 172 - } 173 - return nil 174 - }
-1
didplc/main.go
··· 1 - package didplc
-129
didplc/manual_test.go
··· 1 - package didplc 2 - 3 - import ( 4 - "encoding/base64" 5 - "testing" 6 - 7 - "github.com/bluesky-social/indigo/atproto/crypto" 8 - 9 - cbor "github.com/ipfs/go-ipld-cbor" 10 - "github.com/stretchr/testify/assert" 11 - ) 12 - 13 - func TestVerifySignatureHardWay(t *testing.T) { 14 - assert := assert.New(t) 15 - 16 - sig := "n-VWsPZY4xkFN8wlg-kJBU_yzWTNd2oBnbjkjxXu3HdjbBLaEB7K39JHIPn_DZVALKRjts6bUicjSEecZy8eIw" 17 - didKey := "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 18 - pub, err := crypto.ParsePublicDIDKey(didKey) 19 - if err != nil { 20 - t.Fatal(err) 21 - } 22 - 23 - obj := map[string]interface{}{ 24 - "prev": "bafyreigcxay6ucqlwowfpu35alyxqtv3c4vsj7gmdtmnidsnqs6nblyarq", 25 - "type": "plc_operation", 26 - "services": map[string]any{ 27 - "atproto_pds": map[string]string{ 28 - "type": "AtprotoPersonalDataServer", 29 - "endpoint": "https://bsky.social", 30 - }, 31 - }, 32 - "alsoKnownAs": []string{ 33 - "at://dholms.xyz", 34 - }, 35 - "rotationKeys": []string{ 36 - "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 37 - "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ", 38 - }, 39 - "verificationMethods": map[string]string{ 40 - "atproto": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ", 41 - }, 42 - //"sig": nil, 43 - } 44 - objBytes, err := cbor.DumpObject(obj) 45 - if err != nil { 46 - t.Fatal(err) 47 - } 48 - 49 - sigBytes, err := base64.RawURLEncoding.DecodeString(sig) 50 - if err != nil { 51 - t.Fatal(err) 52 - } 53 - //fmt.Println(len(sigBytes)) 54 - assert.NoError(pub.HashAndVerify(objBytes, sigBytes)) 55 - } 56 - 57 - func TestVerifySignatureHardWayNew(t *testing.T) { 58 - assert := assert.New(t) 59 - 60 - sig := "v9rHEhW4XVwMKRSd2yeFgk4-mZthHSZwJ4tShNPqDP4NH3w79CkxIOmJ393D6MEyWZLN1qxS1qBIbFEGtfoDDw" 61 - didKey := "did:key:zQ3shcciz4AvrLyDnUdZLpQys3kyCsesojRNzJAieyDStGxGo" 62 - pub, err := crypto.ParsePublicDIDKey(didKey) 63 - if err != nil { 64 - t.Fatal(err) 65 - } 66 - 67 - obj := map[string]interface{}{ 68 - "prev": nil, 69 - "type": "plc_operation", 70 - "services": map[string]any{ 71 - "atproto_pds": map[string]string{ 72 - "type": "AtprotoPersonalDataServer", 73 - "endpoint": "https://pds.robocracy.org", 74 - }, 75 - }, 76 - "alsoKnownAs": []string{ 77 - "at://bnewbold.pds.robocracy.org", 78 - }, 79 - "rotationKeys": []string{ 80 - "did:key:zQ3shcciz4AvrLyDnUdZLpQys3kyCsesojRNzJAieyDStGxGo", 81 - }, 82 - "verificationMethods": map[string]string{ 83 - "atproto": "did:key:zQ3shazA2airLo8gNJvxGMFZWPJDRkLGNR6mn9Txsc8YYndwy", 84 - }, 85 - //"sig": nil, 86 - } 87 - objBytes, err := cbor.DumpObject(obj) 88 - if err != nil { 89 - t.Fatal(err) 90 - } 91 - 92 - sigBytes, err := base64.RawURLEncoding.DecodeString(sig) 93 - if err != nil { 94 - t.Fatal(err) 95 - } 96 - assert.NoError(pub.HashAndVerify(objBytes, sigBytes)) 97 - assert.Equal("bafyreih7k7a7v7ez7qzzxj7ywomk5hgtidpzuodjsw2kldtepdadob4hdi", computeCID(objBytes).String()) 98 - } 99 - 100 - func TestVerifySignatureLegacyGenesis(t *testing.T) { 101 - assert := assert.New(t) 102 - 103 - sig := "7QTzqO1BcL3eDzP4P_YBxMmv5U4brHzAItkM9w5o8gZA7ElZkrVYEwsfQCfk5EoWLk58Z1y6fyNP9x1pthJnlw" 104 - didKey := "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" // signing, not recovery 105 - pub, err := crypto.ParsePublicDIDKey(didKey) 106 - if err != nil { 107 - t.Fatal(err) 108 - } 109 - 110 - obj := map[string]interface{}{ 111 - "prev": nil, 112 - "type": "create", 113 - "handle": "dan.bsky.social", 114 - "service": "https://bsky.social", 115 - "signingKey": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ", 116 - "recoveryKey": "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 117 - //"sig": nil, 118 - } 119 - objBytes, err := cbor.DumpObject(obj) 120 - if err != nil { 121 - t.Fatal(err) 122 - } 123 - 124 - sigBytes, err := base64.RawURLEncoding.DecodeString(sig) 125 - if err != nil { 126 - t.Fatal(err) 127 - } 128 - assert.NoError(pub.HashAndVerify(objBytes, sigBytes)) 129 - }
-471
didplc/operation.go
··· 1 - package didplc 2 - 3 - import ( 4 - "crypto/sha256" 5 - "encoding/base32" 6 - "encoding/base64" 7 - "encoding/json" 8 - "errors" 9 - "fmt" 10 - "strings" 11 - 12 - "github.com/bluesky-social/indigo/atproto/crypto" 13 - 14 - "github.com/ipfs/go-cid" 15 - cbor "github.com/ipfs/go-ipld-cbor" 16 - ) 17 - 18 - type Operation interface { 19 - // CID of the full (signed) operation 20 - CID() cid.Cid 21 - // serializes a copy of the op as CBOR, with the `sig` field omitted 22 - UnsignedCBORBytes() []byte 23 - // serializes a copy of the op as CBOR, with the `sig` field included 24 - SignedCBORBytes() []byte 25 - // whether this operation is a genesis (creation) op 26 - IsGenesis() bool 27 - // whether this operation has a signature or is unsigned 28 - IsSigned() bool 29 - // returns the DID for a genesis op (errors if this op is not a genesis op) 30 - DID() (string, error) 31 - // signs the object in-place 32 - Sign(priv crypto.PrivateKey) error 33 - // verifiy signature. returns crypto.ErrInvalidSignature if appropriate 34 - VerifySignature(pub crypto.PublicKey) error 35 - // returns a DID doc 36 - Doc(did string) (Doc, error) 37 - } 38 - 39 - type OpService struct { 40 - Type string `json:"type" cborgen:"type"` 41 - Endpoint string `json:"endpoint" cborgen:"endpoint"` 42 - } 43 - 44 - type RegularOp struct { 45 - Type string `json:"type,const=plc_operation" cborgen:"type,const=plc_operation"` 46 - RotationKeys []string `json:"rotationKeys" cborgen:"rotationKeys"` 47 - VerificationMethods map[string]string `json:"verificationMethods" cborgen:"verificationMethods"` 48 - AlsoKnownAs []string `json:"alsoKnownAs" cborgen:"alsoKnownAs"` 49 - Services map[string]OpService `json:"services" cborgen:"services"` 50 - Prev *string `json:"prev" cborgen:"prev"` 51 - Sig *string `json:"sig,omitempty" cborgen:"sig,omitempty" refmt:"sig,omitempty"` 52 - } 53 - 54 - type TombstoneOp struct { 55 - Type string `json:"type,const=plc_tombstone" cborgen:"type,const=plc_tombstone"` 56 - Prev string `json:"prev" cborgen:"prev"` 57 - Sig *string `json:"sig,omitempty" cborgen:"sig,omitempty" refmt:"sig,omitempty"` 58 - } 59 - 60 - type LegacyOp struct { 61 - Type string `json:"type,const=create" cborgen:"type,const=create"` 62 - SigningKey string `json:"signingKey" cborgen:"signingKey"` 63 - RecoveryKey string `json:"recoveryKey" cborgen:"recoveryKey"` 64 - Handle string `json:"handle" cborgen:"handle"` 65 - Service string `json:"service" cborgen:"service"` 66 - Prev *string `json:"prev" cborgen:"prev"` 67 - Sig *string `json:"sig,omitempty" cborgen:"sig,omitempty" refmt:"sig,omitempty"` 68 - } 69 - 70 - var _ Operation = (*RegularOp)(nil) 71 - var _ Operation = (*TombstoneOp)(nil) 72 - var _ Operation = (*LegacyOp)(nil) 73 - 74 - // any of: Op, TombstoneOp, or LegacyOp 75 - type OpEnum struct { 76 - Regular *RegularOp 77 - Tombstone *TombstoneOp 78 - Legacy *LegacyOp 79 - } 80 - 81 - var ErrNotGenesisOp = errors.New("not a genesis PLC operation") 82 - 83 - func init() { 84 - cbor.RegisterCborType(OpService{}) 85 - cbor.RegisterCborType(RegularOp{}) 86 - cbor.RegisterCborType(TombstoneOp{}) 87 - cbor.RegisterCborType(LegacyOp{}) 88 - } 89 - 90 - func computeCID(b []byte) cid.Cid { 91 - cidBuilder := cid.V1Builder{Codec: 0x71, MhType: 0x12, MhLength: 0} 92 - c, err := cidBuilder.Sum(b) 93 - if err != nil { 94 - return cid.Undef 95 - } 96 - return c 97 - } 98 - 99 - func (op *RegularOp) CID() cid.Cid { 100 - return computeCID(op.SignedCBORBytes()) 101 - } 102 - 103 - func (op *RegularOp) UnsignedCBORBytes() []byte { 104 - unsigned := RegularOp{ 105 - Type: op.Type, 106 - RotationKeys: op.RotationKeys, 107 - VerificationMethods: op.VerificationMethods, 108 - AlsoKnownAs: op.AlsoKnownAs, 109 - Services: op.Services, 110 - Prev: op.Prev, 111 - Sig: nil, 112 - } 113 - 114 - out, err := cbor.DumpObject(unsigned) 115 - if err != nil { 116 - return nil 117 - } 118 - return out 119 - } 120 - 121 - func (op *RegularOp) SignedCBORBytes() []byte { 122 - out, err := cbor.DumpObject(op) 123 - if err != nil { 124 - return nil 125 - } 126 - return out 127 - } 128 - 129 - func (op *RegularOp) IsGenesis() bool { 130 - return op.Prev == nil 131 - } 132 - 133 - func (op *RegularOp) IsSigned() bool { 134 - return op.Sig != nil && *op.Sig != "" 135 - } 136 - 137 - func (op *RegularOp) DID() (string, error) { 138 - if !op.IsGenesis() { 139 - return "", ErrNotGenesisOp 140 - } 141 - hash := sha256.Sum256(op.SignedCBORBytes()) 142 - suffix := base32.StdEncoding.EncodeToString(hash[:])[:24] 143 - return "did:plc:" + strings.ToLower(suffix), nil 144 - } 145 - 146 - func signOp(op Operation, priv crypto.PrivateKey) (string, error) { 147 - b := op.UnsignedCBORBytes() 148 - sig, err := priv.HashAndSign(b) 149 - if err != nil { 150 - return "", err 151 - } 152 - b64 := base64.RawURLEncoding.EncodeToString(sig) 153 - return b64, nil 154 - } 155 - 156 - func (op *RegularOp) Sign(priv crypto.PrivateKey) error { 157 - sig, err := signOp(op, priv) 158 - if err != nil { 159 - return err 160 - } 161 - op.Sig = &sig 162 - return nil 163 - } 164 - 165 - func verifySigOp(op Operation, pub crypto.PublicKey, sig *string) error { 166 - if sig == nil || *sig == "" { 167 - return fmt.Errorf("can't verify empty signature") 168 - } 169 - b := op.UnsignedCBORBytes() 170 - sigBytes, err := base64.RawURLEncoding.DecodeString(*sig) 171 - if err != nil { 172 - return err 173 - } 174 - return pub.HashAndVerify(b, sigBytes) 175 - } 176 - 177 - // parsing errors are not ignored (will be returned immediately if found) 178 - func VerifySignatureAny(op Operation, didKeys []string) error { 179 - if len(didKeys) == 0 { 180 - return fmt.Errorf("no keys to verify against") 181 - } 182 - for _, dk := range didKeys { 183 - pub, err := crypto.ParsePublicDIDKey(dk) 184 - if err != nil { 185 - return err 186 - } 187 - err = op.VerifySignature(pub) 188 - if err != crypto.ErrInvalidSignature { 189 - return err 190 - } 191 - if nil == err { 192 - return nil 193 - } 194 - } 195 - return crypto.ErrInvalidSignature 196 - } 197 - 198 - func (op *RegularOp) VerifySignature(pub crypto.PublicKey) error { 199 - return verifySigOp(op, pub, op.Sig) 200 - } 201 - 202 - func (op *RegularOp) Doc(did string) (Doc, error) { 203 - svc := []DocService{} 204 - for key, s := range op.Services { 205 - svc = append(svc, DocService{ 206 - ID: did + "#" + key, 207 - Type: s.Type, 208 - ServiceEndpoint: s.Endpoint, 209 - }) 210 - } 211 - vm := []DocVerificationMethod{} 212 - for name, didKey := range op.VerificationMethods { 213 - pub, err := crypto.ParsePublicDIDKey(didKey) 214 - if err != nil { 215 - return Doc{}, err 216 - } 217 - vm = append(vm, DocVerificationMethod{ 218 - ID: did + "#" + name, 219 - Type: "Multikey", 220 - Controller: did, 221 - PublicKeyMultibase: pub.Multibase(), 222 - }) 223 - } 224 - doc := Doc{ 225 - ID: did, 226 - AlsoKnownAs: op.AlsoKnownAs, 227 - VerificationMethod: vm, 228 - Service: svc, 229 - } 230 - return doc, nil 231 - } 232 - 233 - func (op *LegacyOp) CID() cid.Cid { 234 - return computeCID(op.SignedCBORBytes()) 235 - } 236 - 237 - func (op *LegacyOp) UnsignedCBORBytes() []byte { 238 - unsigned := LegacyOp{ 239 - Type: op.Type, 240 - SigningKey: op.SigningKey, 241 - RecoveryKey: op.RecoveryKey, 242 - Handle: op.Handle, 243 - Service: op.Service, 244 - Prev: op.Prev, 245 - Sig: nil, 246 - } 247 - out, err := cbor.DumpObject(unsigned) 248 - if err != nil { 249 - return nil 250 - } 251 - return out 252 - } 253 - 254 - func (op *LegacyOp) SignedCBORBytes() []byte { 255 - out, err := cbor.DumpObject(op) 256 - if err != nil { 257 - return nil 258 - } 259 - return out 260 - } 261 - 262 - func (op *LegacyOp) IsGenesis() bool { 263 - return op.Prev == nil 264 - } 265 - 266 - func (op *LegacyOp) IsSigned() bool { 267 - return op.Sig != nil && *op.Sig != "" 268 - } 269 - 270 - func (op *LegacyOp) DID() (string, error) { 271 - if !op.IsGenesis() { 272 - return "", ErrNotGenesisOp 273 - } 274 - hash := sha256.Sum256(op.SignedCBORBytes()) 275 - suffix := base32.StdEncoding.EncodeToString(hash[:])[:24] 276 - return "did:plc:" + strings.ToLower(suffix), nil 277 - } 278 - 279 - func (op *LegacyOp) Sign(priv crypto.PrivateKey) error { 280 - sig, err := signOp(op, priv) 281 - if err != nil { 282 - return err 283 - } 284 - op.Sig = &sig 285 - return nil 286 - } 287 - 288 - func (op *LegacyOp) VerifySignature(pub crypto.PublicKey) error { 289 - return verifySigOp(op, pub, op.Sig) 290 - } 291 - 292 - func (op *LegacyOp) Doc(did string) (Doc, error) { 293 - // NOTE: could re-implement this by calling op.RegularOp().Doc() 294 - svc := []DocService{ 295 - DocService{ 296 - ID: did + "#atproto_pds", 297 - Type: "AtprotoPersonalDataServer", 298 - ServiceEndpoint: op.Service, 299 - }, 300 - } 301 - vm := []DocVerificationMethod{ 302 - DocVerificationMethod{ 303 - ID: did + "#atproto", 304 - Type: "Multikey", 305 - Controller: did, 306 - PublicKeyMultibase: strings.TrimPrefix(op.SigningKey, "did:key:"), 307 - }, 308 - } 309 - doc := Doc{ 310 - ID: did, 311 - AlsoKnownAs: []string{"at://" + op.Handle}, 312 - VerificationMethod: vm, 313 - Service: svc, 314 - } 315 - return doc, nil 316 - } 317 - 318 - // converts a legacy "create" op to an (unsigned) "plc_operation" 319 - func (op *LegacyOp) RegularOp() RegularOp { 320 - return RegularOp{ 321 - RotationKeys: []string{op.RecoveryKey}, 322 - VerificationMethods: map[string]string{ 323 - "atproto": op.SigningKey, 324 - }, 325 - AlsoKnownAs: []string{"at://" + op.Handle}, 326 - Services: map[string]OpService{ 327 - "atproto_pds": OpService{ 328 - Type: "AtprotoPersonalDataServer", 329 - Endpoint: op.Service, 330 - }, 331 - }, 332 - Prev: nil, // always a create 333 - Sig: nil, // don't have private key 334 - } 335 - } 336 - 337 - func (op *TombstoneOp) CID() cid.Cid { 338 - return computeCID(op.SignedCBORBytes()) 339 - } 340 - 341 - func (op *TombstoneOp) UnsignedCBORBytes() []byte { 342 - unsigned := TombstoneOp{ 343 - Type: op.Type, 344 - Prev: op.Prev, 345 - Sig: nil, 346 - } 347 - out, err := cbor.DumpObject(unsigned) 348 - if err != nil { 349 - return nil 350 - } 351 - return out 352 - } 353 - 354 - func (op *TombstoneOp) SignedCBORBytes() []byte { 355 - out, err := cbor.DumpObject(op) 356 - if err != nil { 357 - return nil 358 - } 359 - return out 360 - } 361 - 362 - func (op *TombstoneOp) IsGenesis() bool { 363 - return false 364 - } 365 - 366 - func (op *TombstoneOp) IsSigned() bool { 367 - return op.Sig != nil && *op.Sig != "" 368 - } 369 - 370 - func (op *TombstoneOp) DID() (string, error) { 371 - return "", ErrNotGenesisOp 372 - } 373 - 374 - func (op *TombstoneOp) Sign(priv crypto.PrivateKey) error { 375 - sig, err := signOp(op, priv) 376 - if err != nil { 377 - return err 378 - } 379 - op.Sig = &sig 380 - return nil 381 - } 382 - 383 - func (op *TombstoneOp) VerifySignature(pub crypto.PublicKey) error { 384 - return verifySigOp(op, pub, op.Sig) 385 - } 386 - 387 - func (op *TombstoneOp) Doc(did string) (Doc, error) { 388 - return Doc{}, fmt.Errorf("tombstones do not have a DID document representation") 389 - } 390 - 391 - func (o *OpEnum) MarshalJSON() ([]byte, error) { 392 - if o.Regular != nil { 393 - return json.Marshal(o.Regular) 394 - } else if o.Legacy != nil { 395 - return json.Marshal(o.Legacy) 396 - } else if o.Tombstone != nil { 397 - return json.Marshal(o.Tombstone) 398 - } 399 - return nil, fmt.Errorf("can't marshal empty OpEnum") 400 - } 401 - 402 - func (o *OpEnum) UnmarshalJSON(b []byte) error { 403 - var typeMap map[string]interface{} 404 - err := json.Unmarshal(b, &typeMap) 405 - if err != nil { 406 - return err 407 - } 408 - typ, ok := typeMap["type"] 409 - if !ok { 410 - return fmt.Errorf("did not find expected operation 'type' field") 411 - } 412 - 413 - switch typ { 414 - case "plc_operation": 415 - o.Regular = &RegularOp{} 416 - return json.Unmarshal(b, o.Regular) 417 - case "create": 418 - o.Legacy = &LegacyOp{} 419 - return json.Unmarshal(b, o.Legacy) 420 - case "plc_tombstone": 421 - o.Tombstone = &TombstoneOp{} 422 - return json.Unmarshal(b, o.Tombstone) 423 - default: 424 - return fmt.Errorf("unexpected operation type: %s", typ) 425 - } 426 - } 427 - 428 - // returns a new signed PLC operation using the provided atproto-specific metdata 429 - func NewAtproto(priv crypto.PrivateKey, handle string, pdsEndpoint string, rotationKeys []string) (RegularOp, error) { 430 - 431 - pub, err := priv.PublicKey() 432 - if err != nil { 433 - return RegularOp{}, err 434 - } 435 - if len(rotationKeys) == 0 { 436 - return RegularOp{}, fmt.Errorf("at least one rotation key is required") 437 - } 438 - handleURI := "at://" + handle 439 - op := RegularOp{ 440 - RotationKeys: rotationKeys, 441 - VerificationMethods: map[string]string{ 442 - "atproto": pub.DIDKey(), 443 - }, 444 - AlsoKnownAs: []string{handleURI}, 445 - Services: map[string]OpService{ 446 - "atproto_pds": OpService{ 447 - Type: "AtprotoPersonalDataServer", 448 - Endpoint: pdsEndpoint, 449 - }, 450 - }, 451 - Prev: nil, 452 - Sig: nil, 453 - } 454 - if err := op.Sign(priv); err != nil { 455 - return RegularOp{}, err 456 - } 457 - return op, nil 458 - } 459 - 460 - func (oe *OpEnum) AsOperation() Operation { 461 - if oe.Regular != nil { 462 - return oe.Regular 463 - } else if oe.Legacy != nil { 464 - return oe.Legacy 465 - } else if oe.Tombstone != nil { 466 - return oe.Tombstone 467 - } else { 468 - // TODO; something more safe here? 469 - return nil 470 - } 471 - }
-101
didplc/operation_test.go
··· 1 - package didplc 2 - 3 - import ( 4 - "encoding/json" 5 - "io" 6 - "os" 7 - "testing" 8 - 9 - "github.com/bluesky-social/indigo/atproto/crypto" 10 - "github.com/bluesky-social/indigo/atproto/syntax" 11 - 12 - "github.com/stretchr/testify/assert" 13 - ) 14 - 15 - func loadTestLogEntries(t *testing.T, p string) []LogEntry { 16 - f, err := os.Open(p) 17 - if err != nil { 18 - t.Fatal(err) 19 - } 20 - defer func() { _ = f.Close() }() 21 - 22 - fileBytes, err := io.ReadAll(f) 23 - if err != nil { 24 - t.Fatal(err) 25 - } 26 - 27 - var entries []LogEntry 28 - if err := json.Unmarshal(fileBytes, &entries); err != nil { 29 - t.Fatal(err) 30 - } 31 - 32 - return entries 33 - } 34 - 35 - func TestLogEntryValidate(t *testing.T) { 36 - assert := assert.New(t) 37 - 38 - list := []string{ 39 - "testdata/log_bskyapp.json", 40 - "testdata/log_legacy_dholms.json", 41 - "testdata/log_bnewbold_robocracy.json", 42 - } 43 - for _, p := range list { 44 - entries := loadTestLogEntries(t, p) 45 - for _, le := range entries { 46 - assert.NoError(le.Validate()) 47 - } 48 - } 49 - } 50 - 51 - func TestCreatePLC(t *testing.T) { 52 - assert := assert.New(t) 53 - 54 - priv, err := crypto.GeneratePrivateKeyP256() 55 - if err != nil { 56 - t.Fatal(err) 57 - } 58 - pub, err := priv.PublicKey() 59 - if err != nil { 60 - t.Fatal(err) 61 - } 62 - pubDIDKey := pub.DIDKey() 63 - handleURI := "at://handle.example.com" 64 - endpoint := "https://pds.example.com" 65 - op := RegularOp{ 66 - Type: "plc_operation", 67 - RotationKeys: []string{pubDIDKey}, 68 - VerificationMethods: map[string]string{ 69 - "atproto": pubDIDKey, 70 - }, 71 - AlsoKnownAs: []string{handleURI}, 72 - Services: map[string]OpService{ 73 - "atproto_pds": OpService{ 74 - Type: "AtprotoPersonalDataServer", 75 - Endpoint: endpoint, 76 - }, 77 - }, 78 - Prev: nil, 79 - Sig: nil, 80 - } 81 - assert.NoError(op.Sign(priv)) 82 - assert.NoError(op.VerifySignature(pub)) 83 - did, err := op.DID() 84 - if err != nil { 85 - t.Fatal(err) 86 - } 87 - _, err = syntax.ParseDID(did) 88 - assert.NoError(err) 89 - 90 - le := LogEntry{ 91 - DID: did, 92 - Operation: OpEnum{Regular: &op}, 93 - CID: op.CID().String(), 94 - Nullified: false, 95 - CreatedAt: syntax.DatetimeNow().String(), 96 - } 97 - assert.NoError(le.Validate()) 98 - 99 - _, err = op.Doc(did) 100 - assert.NoError(err) 101 - }
-54
didplc/testdata/log_bnewbold_robocracy.json
··· 1 - [ 2 - { 3 - "did": "did:plc:nhxcyu4ewwhl5pqil4dotqjo", 4 - "operation": { 5 - "sig": "v9rHEhW4XVwMKRSd2yeFgk4-mZthHSZwJ4tShNPqDP4NH3w79CkxIOmJ393D6MEyWZLN1qxS1qBIbFEGtfoDDw", 6 - "prev": null, 7 - "type": "plc_operation", 8 - "services": { 9 - "atproto_pds": { 10 - "type": "AtprotoPersonalDataServer", 11 - "endpoint": "https://pds.robocracy.org" 12 - } 13 - }, 14 - "alsoKnownAs": [ 15 - "at://bnewbold.pds.robocracy.org" 16 - ], 17 - "rotationKeys": [ 18 - "did:key:zQ3shcciz4AvrLyDnUdZLpQys3kyCsesojRNzJAieyDStGxGo" 19 - ], 20 - "verificationMethods": { 21 - "atproto": "did:key:zQ3shazA2airLo8gNJvxGMFZWPJDRkLGNR6mn9Txsc8YYndwy" 22 - } 23 - }, 24 - "cid": "bafyreidj5ywfhbfvr27l4cc7a3u4clup5vx343ufuzdliethuhzzxjbg4q", 25 - "nullified": false, 26 - "createdAt": "2024-02-22T04:31:08.867Z" 27 - }, 28 - { 29 - "did": "did:plc:nhxcyu4ewwhl5pqil4dotqjo", 30 - "operation": { 31 - "sig": "P8TrUomEKSnJpyuoyqdaqv-KilKbQKoi6MNf8DNN8LdFn1cA3_BtkqVYAjmucpQ8DDSze-jG4YDvC6HFK9QPOA", 32 - "prev": "bafyreidj5ywfhbfvr27l4cc7a3u4clup5vx343ufuzdliethuhzzxjbg4q", 33 - "type": "plc_operation", 34 - "services": { 35 - "atproto_pds": { 36 - "type": "AtprotoPersonalDataServer", 37 - "endpoint": "https://pds.robocracy.org" 38 - } 39 - }, 40 - "alsoKnownAs": [ 41 - "at://bnewbold.robocracy.org" 42 - ], 43 - "rotationKeys": [ 44 - "did:key:zQ3shcciz4AvrLyDnUdZLpQys3kyCsesojRNzJAieyDStGxGo" 45 - ], 46 - "verificationMethods": { 47 - "atproto": "did:key:zQ3shazA2airLo8gNJvxGMFZWPJDRkLGNR6mn9Txsc8YYndwy" 48 - } 49 - }, 50 - "cid": "bafyreiemmb2ephqyt7orhiv4vp7gmdohnzhgkehwzfrvirgvg7fg352ysi", 51 - "nullified": false, 52 - "createdAt": "2024-02-22T04:39:57.282Z" 53 - } 54 - ]
-56
didplc/testdata/log_bskyapp.json
··· 1 - [ 2 - { 3 - "did": "did:plc:z72i7hdynmk6r22z27h6tvur", 4 - "operation": { 5 - "sig": "9NuYV7AqwHVTc0YuWzNV3CJafsSZWH7qCxHRUIP2xWlB-YexXC1OaYAnUayiCXLVzRQ8WBXIqF-SvZdNalwcjA", 6 - "prev": null, 7 - "type": "plc_operation", 8 - "services": { 9 - "atproto_pds": { 10 - "type": "AtprotoPersonalDataServer", 11 - "endpoint": "https://bsky.social" 12 - } 13 - }, 14 - "alsoKnownAs": [ 15 - "at://bluesky-team.bsky.social" 16 - ], 17 - "rotationKeys": [ 18 - "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 19 - "did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK" 20 - ], 21 - "verificationMethods": { 22 - "atproto": "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 23 - } 24 - }, 25 - "cid": "bafyreigp6shzy6dlcxuowwoxz7u5nemdrkad2my5zwzpwilcnhih7bw6zm", 26 - "nullified": false, 27 - "createdAt": "2023-04-12T04:53:57.057Z" 28 - }, 29 - { 30 - "did": "did:plc:z72i7hdynmk6r22z27h6tvur", 31 - "operation": { 32 - "sig": "1mEWzRtFOgeRXH-YCSPTxb990JOXxa__n8Qw6BOKl7Ndm6OFFmwYKiiMqMCpAbxpnGjF5abfIsKc7u3a77Cbnw", 33 - "prev": "bafyreigp6shzy6dlcxuowwoxz7u5nemdrkad2my5zwzpwilcnhih7bw6zm", 34 - "type": "plc_operation", 35 - "services": { 36 - "atproto_pds": { 37 - "type": "AtprotoPersonalDataServer", 38 - "endpoint": "https://bsky.social" 39 - } 40 - }, 41 - "alsoKnownAs": [ 42 - "at://bsky.app" 43 - ], 44 - "rotationKeys": [ 45 - "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 46 - "did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK" 47 - ], 48 - "verificationMethods": { 49 - "atproto": "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 50 - } 51 - }, 52 - "cid": "bafyreihmuvr3frdvd6vmdhucih277prdcfcezf67lasg5oekxoimnunjoq", 53 - "nullified": false, 54 - "createdAt": "2023-04-12T17:26:46.468Z" 55 - } 56 - ]
-125
didplc/testdata/log_legacy_dholms.json
··· 1 - [ 2 - { 3 - "did": "did:plc:yk4dd2qkboz2yv6tpubpc6co", 4 - "operation": { 5 - "sig": "7QTzqO1BcL3eDzP4P_YBxMmv5U4brHzAItkM9w5o8gZA7ElZkrVYEwsfQCfk5EoWLk58Z1y6fyNP9x1pthJnlw", 6 - "prev": null, 7 - "type": "create", 8 - "handle": "dan.bsky.social", 9 - "service": "https://bsky.social", 10 - "signingKey": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ", 11 - "recoveryKey": "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg" 12 - }, 13 - "cid": "bafyreigcxay6ucqlwowfpu35alyxqtv3c4vsj7gmdtmnidsnqs6nblyarq", 14 - "nullified": false, 15 - "createdAt": "2022-11-17T01:07:13.996Z" 16 - }, 17 - { 18 - "did": "did:plc:yk4dd2qkboz2yv6tpubpc6co", 19 - "operation": { 20 - "sig": "n-VWsPZY4xkFN8wlg-kJBU_yzWTNd2oBnbjkjxXu3HdjbBLaEB7K39JHIPn_DZVALKRjts6bUicjSEecZy8eIw", 21 - "prev": "bafyreigcxay6ucqlwowfpu35alyxqtv3c4vsj7gmdtmnidsnqs6nblyarq", 22 - "type": "plc_operation", 23 - "services": { 24 - "atproto_pds": { 25 - "type": "AtprotoPersonalDataServer", 26 - "endpoint": "https://bsky.social" 27 - } 28 - }, 29 - "alsoKnownAs": [ 30 - "at://dholms.xyz" 31 - ], 32 - "rotationKeys": [ 33 - "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 34 - "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 35 - ], 36 - "verificationMethods": { 37 - "atproto": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 38 - } 39 - }, 40 - "cid": "bafyreiho5sanautvnw3det66jcwic4vkeabc35y7iou3ygwj2l3xqcxdau", 41 - "nullified": false, 42 - "createdAt": "2023-03-06T18:47:09.501Z" 43 - }, 44 - { 45 - "did": "did:plc:yk4dd2qkboz2yv6tpubpc6co", 46 - "operation": { 47 - "sig": "HWgrfQXxUN3mhR5TR-nrwGJwVr9RDbyDn6eCmqBg32x2zIjhe98YxOtFOLI9jQkBlTTzqzUOwJh1KZd4O2pDOw", 48 - "prev": "bafyreiho5sanautvnw3det66jcwic4vkeabc35y7iou3ygwj2l3xqcxdau", 49 - "type": "plc_operation", 50 - "services": { 51 - "atproto_pds": { 52 - "type": "AtprotoPersonalDataServer", 53 - "endpoint": "https://bsky.social" 54 - } 55 - }, 56 - "alsoKnownAs": [ 57 - "at://dholms.bsky.social" 58 - ], 59 - "rotationKeys": [ 60 - "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 61 - "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 62 - ], 63 - "verificationMethods": { 64 - "atproto": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 65 - } 66 - }, 67 - "cid": "bafyreic3am2nmgykxtwsxwigzn6faibxv5ef5kalcv7li3eatcqldcqrku", 68 - "nullified": false, 69 - "createdAt": "2023-03-06T19:50:49.987Z" 70 - }, 71 - { 72 - "did": "did:plc:yk4dd2qkboz2yv6tpubpc6co", 73 - "operation": { 74 - "sig": "9Fy2iHCSK5mtgLNCkS9CyI0r7lu6H1SVgusaD1jQdsMUySUU6apde0z7SobpYZKp4sThk4hxOWtO-bXhu1cNjg", 75 - "prev": "bafyreic3am2nmgykxtwsxwigzn6faibxv5ef5kalcv7li3eatcqldcqrku", 76 - "type": "plc_operation", 77 - "services": { 78 - "atproto_pds": { 79 - "type": "AtprotoPersonalDataServer", 80 - "endpoint": "https://bsky.social" 81 - } 82 - }, 83 - "alsoKnownAs": [ 84 - "at://dholms.xyz" 85 - ], 86 - "rotationKeys": [ 87 - "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 88 - "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 89 - ], 90 - "verificationMethods": { 91 - "atproto": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 92 - } 93 - }, 94 - "cid": "bafyreicwybxr6h6vkxpoarismso3liozdzswshmzcvl4tyckdazn5lxjte", 95 - "nullified": false, 96 - "createdAt": "2023-03-06T19:51:09.950Z" 97 - }, 98 - { 99 - "did": "did:plc:yk4dd2qkboz2yv6tpubpc6co", 100 - "operation": { 101 - "sig": "lBXd8rHZ84hCuQysGdi_5A9C8yPHTHasPibO4DZiuZVrehs2hiBcjAL0srLSTsF1kvsHTw1ddai-QwH0Wd_drQ", 102 - "prev": "bafyreicwybxr6h6vkxpoarismso3liozdzswshmzcvl4tyckdazn5lxjte", 103 - "type": "plc_operation", 104 - "services": { 105 - "atproto_pds": { 106 - "type": "AtprotoPersonalDataServer", 107 - "endpoint": "https://bsky.social" 108 - } 109 - }, 110 - "alsoKnownAs": [ 111 - "at://dholms.xyz" 112 - ], 113 - "rotationKeys": [ 114 - "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 115 - "did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK" 116 - ], 117 - "verificationMethods": { 118 - "atproto": "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 119 - } 120 - }, 121 - "cid": "bafyreidfrpuegbqd5r56shka4duythb7phb6d7i3bck2dkeb5fjppwd7gi", 122 - "nullified": false, 123 - "createdAt": "2023-03-09T23:18:31.709Z" 124 - } 125 - ]