Vow, uncensorable PDS written in Go

feat: use PDS rotation key for genesis commits, remove service auth cache

+29 -86
-18
models/models.go
··· 85 85 UsedAt time.Time 86 86 } 87 87 88 - // PendingWrite represents a write (or PLC operation) that has been prepared by 89 - // the PDS and is waiting for the user's client to sign it. Once the signature 90 - // is submitted via handleSubmitSignature the stored CommitData is used to 91 - // finalise the commit without the PDS ever having held the private key. 92 - type PendingWrite struct { 93 - ID string `gorm:"primaryKey"` 94 - Did string `gorm:"index"` 95 - PayloadHash string 96 - // Bytes to sign. 97 - Payload []byte 98 - // Original request. 99 - Data []byte 100 - // Serialized commit state. 101 - CommitData []byte 102 - CreatedAt time.Time 103 - ExpiresAt time.Time `gorm:"index"` 104 - } 105 - 106 88 type Token struct { 107 89 Token string `gorm:"primaryKey"` 108 90 Did string `gorm:"index"`
+14
plc/client.go
··· 140 140 return pub.DIDKey() 141 141 } 142 142 143 + // RotationKeyBytes returns the raw bytes of the PDS rotation key. This allows 144 + // callers to use the rotation key to sign genesis commits instead of 145 + // generating a throwaway ephemeral key. 146 + func (c *Client) RotationKeyBytes() []byte { 147 + return c.rotationKey.Bytes() 148 + } 149 + 150 + // RotationPrivateKey returns the PDS rotation key as a typed *atcrypto.PrivateKeyK256. 151 + // This allows callers to pass it directly to functions that expect a signing key, 152 + // such as CreateDID, without generating a throwaway ephemeral key. 153 + func (c *Client) RotationPrivateKey() *atcrypto.PrivateKeyK256 { 154 + return c.rotationKey 155 + } 156 + 143 157 // SignOp signs a PLC operation with the PDS rotation key. This is the only 144 158 // key that can authorise changes to a did:plc document (until the rotation 145 159 // key is transferred to the user via supplySigningKey).
+2 -10
server/handle_account_signup.go
··· 7 7 "time" 8 8 9 9 "github.com/bluesky-social/indigo/api/atproto" 10 - "github.com/bluesky-social/indigo/atproto/atcrypto" 11 10 atp "github.com/bluesky-social/indigo/atproto/repo" 12 11 "github.com/bluesky-social/indigo/atproto/repo/mst" 13 12 "github.com/bluesky-social/indigo/atproto/syntax" ··· 140 139 return 141 140 } 142 141 143 - k, err := atcrypto.GeneratePrivateKeyK256() 144 - if err != nil { 145 - logger.Error("error generating ephemeral key", "error", err) 146 - fail("Something went wrong. Please try again.") 147 - return 148 - } 149 - 150 - did, op, err := s.plcClient.CreateDID(k, "", handle) 142 + did, op, err := s.plcClient.CreateDID(s.plcClient.RotationPrivateKey(), "", handle) 151 143 if err != nil { 152 144 logger.Error("error creating PLC DID", "error", err) 153 145 fail("Something went wrong. Please try again.") ··· 202 194 RecordStore: bs, 203 195 } 204 196 205 - root, rev, err := commitRepo(ctx, bs, repo, k.Bytes()) 197 + root, rev, err := commitRepo(ctx, bs, repo, s.plcClient.RotationKeyBytes()) 206 198 if err != nil { 207 199 logger.Error("error committing genesis", "error", err) 208 200 fail("Something went wrong. Please try again.")
+5 -13
server/handle_import_repo.go
··· 7 7 "slices" 8 8 "strings" 9 9 10 - "github.com/bluesky-social/indigo/atproto/atcrypto" 11 10 "github.com/bluesky-social/indigo/atproto/syntax" 12 11 blocks "github.com/ipfs/go-block-format" 13 12 "github.com/ipfs/go-cid" ··· 108 107 109 108 tx.Commit() 110 109 111 - // The PDS never holds the user's private key. We generate an ephemeral key 112 - // solely to produce a valid commit block for the imported repo. The user 113 - // must call supplySigningKey via the account page after import so that 114 - // subsequent writes can be signed correctly. 115 - ephemeralKey, err := atcrypto.GeneratePrivateKeyK256() 116 - if err != nil { 117 - logger.Error("error generating ephemeral key for import commit", "error", err) 118 - helpers.ServerError(w, nil) 119 - return 120 - } 121 - 122 - root, rev, err := commitRepo(ctx, bs, atRepo, ephemeralKey.Bytes()) 110 + // Sign the import commit with the PDS rotation key instead of a throwaway 111 + // ephemeral key. The user can supply their own signing key later via the 112 + // account page; subsequent writes require a registered public key and a 113 + // connected signer. 114 + root, rev, err := commitRepo(ctx, bs, atRepo, s.plcClient.RotationKeyBytes()) 123 115 if err != nil { 124 116 logger.Error("error committing", "error", err) 125 117 helpers.ServerError(w, nil)
+6 -41
server/handle_server_create_account.go
··· 9 9 "time" 10 10 11 11 "github.com/bluesky-social/indigo/api/atproto" 12 - "github.com/bluesky-social/indigo/atproto/atcrypto" 12 + 13 13 atp "github.com/bluesky-social/indigo/atproto/repo" 14 14 "github.com/bluesky-social/indigo/atproto/repo/mst" 15 15 "github.com/bluesky-social/indigo/atproto/syntax" ··· 159 159 return 160 160 } 161 161 162 - // For the genesis commit we use a temporary ephemeral key. The PDS never 163 - // stores it — as soon as the commit is written the key is discarded. The 164 - // user must call supplySigningKey (via the account page) before any 165 - // subsequent write will succeed, because applyWrites requires a registered 166 - // public key and a connected signer. 167 - var k *atcrypto.PrivateKeyK256 168 - 169 - if signupDid != "" { 170 - reservedKey, err := s.getReservedKey(ctx, signupDid) 171 - if err != nil { 172 - logger.Error("error looking up reserved key", "error", err) 173 - } 174 - if reservedKey != nil { 175 - k, err = atcrypto.ParsePrivateBytesK256(reservedKey.PrivateKey) 176 - if err != nil { 177 - logger.Error("error parsing reserved key", "error", err) 178 - k = nil 179 - } else { 180 - defer func() { 181 - if delErr := s.deleteReservedKey(ctx, reservedKey.KeyDid, reservedKey.Did); delErr != nil { 182 - logger.Error("error deleting reserved key", "error", delErr) 183 - } 184 - }() 185 - } 186 - } 187 - } 188 - 189 - if k == nil { 190 - // Generate an ephemeral key for the genesis commit only. 191 - k, err = atcrypto.GeneratePrivateKeyK256() 192 - if err != nil { 193 - logger.Error("error generating ephemeral key", "endpoint", "com.atproto.server.createAccount", "error", err) 194 - helpers.ServerError(w, nil) 195 - return 196 - } 197 - } 162 + // Use the PDS rotation key to sign the genesis commit. The user can supply their own signing key later via 163 + // supplySigningKey; subsequent writes require a registered public key and a connected signer. 198 164 199 165 if signupDid == "" { 200 - did, op, err := s.plcClient.CreateDID(k, "", request.Handle) 166 + did, op, err := s.plcClient.CreateDID(s.plcClient.RotationPrivateKey(), "", request.Handle) 201 167 if err != nil { 202 168 logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err) 203 169 helpers.ServerError(w, nil) ··· 266 232 RecordStore: bs, 267 233 } 268 234 269 - // Sign the genesis commit with the ephemeral key. This key is never 270 - // persisted; it is only used to produce a valid initial commit block. 271 - root, rev, err := commitRepo(ctx, bs, r, k.Bytes()) 235 + // Sign the genesis commit with the PDS rotation key. 236 + root, rev, err := commitRepo(ctx, bs, r, s.plcClient.RotationKeyBytes()) 272 237 if err != nil { 273 238 logger.Error("error committing", "error", err) 274 239 helpers.ServerError(w, nil)
+2 -3
server/repo.go
··· 305 305 // TODO make use of swap commit 306 306 // pendingCommitState captures everything produced by the MST-building phase of 307 307 // applyWrites that is needed to finalise the commit once a signature arrives. 308 - // It is JSON-serialised into models.PendingWrite.CommitData so that 309 - // finaliseWriteFromSignature can reconstruct it without re-running the MST 310 - // logic. 308 + // It is held in memory and passed directly to finaliseWriteFromState after the 309 + // SignerHub WebSocket round-trip completes. 311 310 // 312 311 // NOTE: block data is stored as raw bytes slices (base64 in JSON) because CIDs 313 312 // and block objects are not JSON-serialisable out of the box with the standard
-1
server/server.go
··· 622 622 &models.InviteCode{}, 623 623 &models.InviteCodeUse{}, 624 624 625 - &models.PendingWrite{}, 626 625 &models.Token{}, 627 626 &models.RefreshToken{}, 628 627 &models.Record{},