An atproto PDS written in Go
1package plc 2 3import ( 4 "bytes" 5 "context" 6 "crypto/sha256" 7 "encoding/base32" 8 "encoding/base64" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "net/url" 14 "strings" 15 16 "github.com/bluesky-social/indigo/atproto/atcrypto" 17 "github.com/bluesky-social/indigo/util" 18 "github.com/haileyok/cocoon/identity" 19) 20 21type Client struct { 22 h *http.Client 23 service string 24 pdsHostname string 25 rotationKey *atcrypto.PrivateKeyK256 26} 27 28type ClientArgs struct { 29 H *http.Client 30 Service string 31 RotationKey []byte 32 PdsHostname string 33} 34 35func NewClient(args *ClientArgs) (*Client, error) { 36 if args.Service == "" { 37 args.Service = "https://plc.directory" 38 } 39 40 if args.H == nil { 41 args.H = util.RobustHTTPClient() 42 } 43 44 rk, err := atcrypto.ParsePrivateBytesK256([]byte(args.RotationKey)) 45 if err != nil { 46 return nil, err 47 } 48 49 return &Client{ 50 h: args.H, 51 service: args.Service, 52 rotationKey: rk, 53 pdsHostname: args.PdsHostname, 54 }, nil 55} 56 57func (c *Client) CreateDID(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) { 58 pubsigkey, err := sigkey.PublicKey() 59 if err != nil { 60 return "", nil, err 61 } 62 63 pubrotkey, err := c.rotationKey.PublicKey() 64 if err != nil { 65 return "", nil, err 66 } 67 68 // todo 69 rotationKeys := []string{pubrotkey.DIDKey()} 70 if recovery != "" { 71 rotationKeys = func(recovery string) []string { 72 newRotationKeys := []string{recovery} 73 for _, k := range rotationKeys { 74 newRotationKeys = append(newRotationKeys, k) 75 } 76 return newRotationKeys 77 }(recovery) 78 } 79 80 op := Operation{ 81 Type: "plc_operation", 82 VerificationMethods: map[string]string{ 83 "atproto": pubsigkey.DIDKey(), 84 }, 85 RotationKeys: rotationKeys, 86 AlsoKnownAs: []string{ 87 "at://" + handle, 88 }, 89 Services: map[string]identity.OperationService{ 90 "atproto_pds": { 91 Type: "AtprotoPersonalDataServer", 92 Endpoint: "https://" + c.pdsHostname, 93 }, 94 }, 95 Prev: nil, 96 } 97 98 if err := c.SignOp(sigkey, &op); err != nil { 99 return "", nil, err 100 } 101 102 did, err := DidFromOp(&op) 103 if err != nil { 104 return "", nil, err 105 } 106 107 return did, &op, nil 108} 109 110func (c *Client) SignOp(sigkey *atcrypto.PrivateKeyK256, op *Operation) error { 111 b, err := op.MarshalCBOR() 112 if err != nil { 113 return err 114 } 115 116 sig, err := c.rotationKey.HashAndSign(b) 117 if err != nil { 118 return err 119 } 120 121 op.Sig = base64.RawURLEncoding.EncodeToString(sig) 122 123 return nil 124} 125 126func (c *Client) SendOperation(ctx context.Context, did string, op *Operation) error { 127 b, err := json.Marshal(op) 128 if err != nil { 129 return err 130 } 131 132 req, err := http.NewRequestWithContext(ctx, "POST", c.service+"/"+url.QueryEscape(did), bytes.NewBuffer(b)) 133 if err != nil { 134 return err 135 } 136 137 req.Header.Add("content-type", "application/json") 138 139 resp, err := c.h.Do(req) 140 if err != nil { 141 return err 142 } 143 defer resp.Body.Close() 144 145 b, err = io.ReadAll(resp.Body) 146 if err != nil { 147 return fmt.Errorf("error sending operation. status code: %d, response: %s", resp.StatusCode, string(b)) 148 } 149 150 return nil 151} 152 153func DidFromOp(op *Operation) (string, error) { 154 b, err := op.MarshalCBOR() 155 if err != nil { 156 return "", err 157 } 158 s := sha256.Sum256(b) 159 b32 := strings.ToLower(base32.StdEncoding.EncodeToString(s[:])) 160 return "did:plc:" + b32[0:24], nil 161}