An atproto PDS written in Go
at main 3.7 kB view raw
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 creds, err := c.CreateDidCredentials(sigkey, recovery, handle) 59 if err != nil { 60 return "", nil, err 61 } 62 63 op := Operation{ 64 Type: "plc_operation", 65 VerificationMethods: creds.VerificationMethods, 66 RotationKeys: creds.RotationKeys, 67 AlsoKnownAs: creds.AlsoKnownAs, 68 Services: creds.Services, 69 Prev: nil, 70 } 71 72 if err := c.SignOp(sigkey, &op); err != nil { 73 return "", nil, err 74 } 75 76 did, err := DidFromOp(&op) 77 if err != nil { 78 return "", nil, err 79 } 80 81 return did, &op, nil 82} 83 84func (c *Client) CreateDidCredentials(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (*DidCredentials, error) { 85 pubsigkey, err := sigkey.PublicKey() 86 if err != nil { 87 return nil, err 88 } 89 90 pubrotkey, err := c.rotationKey.PublicKey() 91 if err != nil { 92 return nil, err 93 } 94 95 // todo 96 rotationKeys := []string{pubrotkey.DIDKey()} 97 if recovery != "" { 98 rotationKeys = func(recovery string) []string { 99 newRotationKeys := []string{recovery} 100 for _, k := range rotationKeys { 101 newRotationKeys = append(newRotationKeys, k) 102 } 103 return newRotationKeys 104 }(recovery) 105 } 106 107 creds := DidCredentials{ 108 VerificationMethods: map[string]string{ 109 "atproto": pubsigkey.DIDKey(), 110 }, 111 RotationKeys: rotationKeys, 112 AlsoKnownAs: []string{ 113 "at://" + handle, 114 }, 115 Services: map[string]identity.OperationService{ 116 "atproto_pds": { 117 Type: "AtprotoPersonalDataServer", 118 Endpoint: "https://" + c.pdsHostname, 119 }, 120 }, 121 } 122 123 return &creds, nil 124} 125 126func (c *Client) SignOp(sigkey *atcrypto.PrivateKeyK256, op *Operation) error { 127 b, err := op.MarshalCBOR() 128 if err != nil { 129 return err 130 } 131 132 sig, err := c.rotationKey.HashAndSign(b) 133 if err != nil { 134 return err 135 } 136 137 op.Sig = base64.RawURLEncoding.EncodeToString(sig) 138 139 return nil 140} 141 142func (c *Client) SendOperation(ctx context.Context, did string, op *Operation) error { 143 b, err := json.Marshal(op) 144 if err != nil { 145 return err 146 } 147 148 req, err := http.NewRequestWithContext(ctx, "POST", c.service+"/"+url.QueryEscape(did), bytes.NewBuffer(b)) 149 if err != nil { 150 return err 151 } 152 153 req.Header.Add("content-type", "application/json") 154 155 resp, err := c.h.Do(req) 156 if err != nil { 157 return err 158 } 159 defer resp.Body.Close() 160 161 b, err = io.ReadAll(resp.Body) 162 if err != nil { 163 return fmt.Errorf("error sending operation. status code: %d, response: %s", resp.StatusCode, string(b)) 164 } 165 166 return nil 167} 168 169func DidFromOp(op *Operation) (string, error) { 170 b, err := op.MarshalCBOR() 171 if err != nil { 172 return "", err 173 } 174 s := sha256.Sum256(b) 175 b32 := strings.ToLower(base32.StdEncoding.EncodeToString(s[:])) 176 return "did:plc:" + b32[0:24], nil 177}