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/crypto" 17 "github.com/bluesky-social/indigo/atproto/data" 18 "github.com/bluesky-social/indigo/util" 19) 20 21type Client struct { 22 h *http.Client 23 24 service string 25 rotationKey *crypto.PrivateKeyK256 26 recoveryKey string 27 pdsHostname string 28} 29 30type ClientArgs struct { 31 Service string 32 RotationKey []byte 33 RecoveryKey string 34 PdsHostname string 35} 36 37func NewClient(args *ClientArgs) (*Client, error) { 38 if args.Service == "" { 39 args.Service = "https://plc.directory" 40 } 41 42 rk, err := crypto.ParsePrivateBytesK256([]byte(args.RotationKey)) 43 if err != nil { 44 return nil, err 45 } 46 47 return &Client{ 48 h: util.RobustHTTPClient(), 49 service: args.Service, 50 rotationKey: rk, 51 recoveryKey: args.RecoveryKey, 52 pdsHostname: args.PdsHostname, 53 }, nil 54} 55 56func (c *Client) CreateDID(ctx context.Context, sigkey *crypto.PrivateKeyK256, recovery string, handle string) (string, map[string]any, error) { 57 pubrotkey, err := c.rotationKey.PublicKey() 58 if err != nil { 59 return "", nil, err 60 } 61 62 // todo 63 rotationKeys := []string{pubrotkey.DIDKey()} 64 if c.recoveryKey != "" { 65 rotationKeys = []string{c.recoveryKey, rotationKeys[0]} 66 } 67 if recovery != "" { 68 rotationKeys = func(recovery string) []string { 69 newRotationKeys := []string{recovery} 70 for _, k := range rotationKeys { 71 newRotationKeys = append(newRotationKeys, k) 72 } 73 return newRotationKeys 74 }(recovery) 75 } 76 77 op, err := c.FormatAndSignAtprotoOp(sigkey, handle, rotationKeys, nil) 78 if err != nil { 79 return "", nil, err 80 } 81 82 did, err := didForCreateOp(op) 83 if err != nil { 84 return "", nil, err 85 } 86 87 return did, op, nil 88} 89 90func (c *Client) UpdateUserHandle(ctx context.Context, didstr string, nhandle string) error { 91 return nil 92} 93 94func (c *Client) FormatAndSignAtprotoOp(sigkey *crypto.PrivateKeyK256, handle string, rotationKeys []string, prev *string) (map[string]any, error) { 95 pubsigkey, err := sigkey.PublicKey() 96 if err != nil { 97 return nil, err 98 } 99 100 op := map[string]any{ 101 "type": "plc_operation", 102 "verificationMethods": map[string]string{ 103 "atproto": pubsigkey.DIDKey(), 104 }, 105 "rotationKeys": rotationKeys, 106 "alsoKnownAs": []string{"at://" + handle}, 107 "services": map[string]any{ 108 "atproto_pds": map[string]string{ 109 "type": "AtprotoPersonalDataServer", 110 "endpoint": "https://" + c.pdsHostname, 111 }, 112 }, 113 "prev": prev, 114 } 115 116 b, err := data.MarshalCBOR(op) 117 if err != nil { 118 return nil, err 119 } 120 121 sig, err := c.rotationKey.HashAndSign(b) 122 if err != nil { 123 return nil, err 124 } 125 126 op["sig"] = base64.RawURLEncoding.EncodeToString(sig) 127 128 return op, nil 129} 130 131func didForCreateOp(op map[string]any) (string, error) { 132 b, err := data.MarshalCBOR(op) 133 if err != nil { 134 return "", err 135 } 136 137 h := sha256.New() 138 h.Write(b) 139 bs := h.Sum(nil) 140 141 b32 := strings.ToLower(base32.StdEncoding.EncodeToString(bs)) 142 143 return "did:plc:" + b32[0:24], nil 144} 145 146func (c *Client) SendOperation(ctx context.Context, did string, op any) error { 147 b, err := json.Marshal(op) 148 if err != nil { 149 return err 150 } 151 152 req, err := http.NewRequestWithContext(ctx, "POST", c.service+"/"+url.QueryEscape(did), bytes.NewBuffer(b)) 153 if err != nil { 154 return err 155 } 156 157 req.Header.Add("content-type", "application/json") 158 159 resp, err := c.h.Do(req) 160 if err != nil { 161 return err 162 } 163 defer resp.Body.Close() 164 165 fmt.Println(resp.StatusCode) 166 167 b, err = io.ReadAll(resp.Body) 168 if err != nil { 169 return err 170 } 171 172 fmt.Println(string(b)) 173 174 return nil 175}