An atproto PDS written in Go

Sign plc operations to allow GOAT migration with cocoon as source (#39)

authored by Ed Costello and committed by GitHub 64fa62eb 9fe89b4b

+5 -5
README.md
··· 143 143 144 144 ### Identity 145 145 146 - - [ ] `com.atproto.identity.getRecommendedDidCredentials` 147 - - [ ] `com.atproto.identity.requestPlcOperationSignature` 146 + - [x] `com.atproto.identity.getRecommendedDidCredentials` 147 + - [x] `com.atproto.identity.requestPlcOperationSignature` 148 148 - [x] `com.atproto.identity.resolveHandle` 149 - - [ ] `com.atproto.identity.signPlcOperation` 150 - - [ ] `com.atproto.identity.submitPlcOperation` 149 + - [x] `com.atproto.identity.signPlcOperation` 150 + - [x] `com.atproto.identity.submitPlcOperation` 151 151 - [x] `com.atproto.identity.updateHandle` 152 152 153 153 ### Repo ··· 158 158 - [x] `com.atproto.repo.deleteRecord` 159 159 - [x] `com.atproto.repo.describeRepo` 160 160 - [x] `com.atproto.repo.getRecord` 161 - - [x] `com.atproto.repo.importRepo` (Works "okay". You still have to handle PLC operations on your own when migrating. Use with extreme caution.) 161 + - [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.) 162 162 - [x] `com.atproto.repo.listRecords` 163 163 - [ ] `com.atproto.repo.listMissingBlobs` 164 164
+2
models/models.go
··· 19 19 EmailUpdateCodeExpiresAt *time.Time 20 20 PasswordResetCode *string 21 21 PasswordResetCodeExpiresAt *time.Time 22 + PlcOperationCode *string 23 + PlcOperationCodeExpiresAt *time.Time 22 24 Password string 23 25 SigningKey []byte 24 26 Rev string
+29
server/handle_identity_request_plc_operation.go
··· 1 + package server 2 + 3 + import ( 4 + "fmt" 5 + "time" 6 + 7 + "github.com/haileyok/cocoon/internal/helpers" 8 + "github.com/haileyok/cocoon/models" 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + func (s *Server) handleIdentityRequestPlcOperationSignature(e echo.Context) error { 13 + urepo := e.Get("repo").(*models.RepoActor) 14 + 15 + code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 16 + eat := time.Now().Add(10 * time.Minute).UTC() 17 + 18 + if err := s.db.Exec("UPDATE repos SET plc_operation_code = ?, plc_operation_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil { 19 + s.logger.Error("error updating user", "error", err) 20 + return helpers.ServerError(e, nil) 21 + } 22 + 23 + if err := s.sendPlcTokenReset(urepo.Email, urepo.Handle, code); err != nil { 24 + s.logger.Error("error sending mail", "error", err) 25 + return helpers.ServerError(e, nil) 26 + } 27 + 28 + return e.NoContent(200) 29 + }
+103
server/handle_identity_sign_plc_operation.go
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "strings" 6 + "time" 7 + 8 + "github.com/Azure/go-autorest/autorest/to" 9 + "github.com/bluesky-social/indigo/atproto/atcrypto" 10 + "github.com/haileyok/cocoon/identity" 11 + "github.com/haileyok/cocoon/internal/helpers" 12 + "github.com/haileyok/cocoon/models" 13 + "github.com/haileyok/cocoon/plc" 14 + "github.com/labstack/echo/v4" 15 + ) 16 + 17 + type ComAtprotoSignPlcOperationRequest struct { 18 + Token string `json:"token"` 19 + VerificationMethods *map[string]string `json:"verificationMethods"` 20 + RotationKeys *[]string `json:"rotationKeys"` 21 + AlsoKnownAs *[]string `json:"alsoKnownAs"` 22 + Services *map[string]identity.OperationService `json:"services"` 23 + } 24 + 25 + type ComAtprotoSignPlcOperationResponse struct { 26 + Operation plc.Operation `json:"operation"` 27 + } 28 + 29 + func (s *Server) handleSignPlcOperation(e echo.Context) error { 30 + repo := e.Get("repo").(*models.RepoActor) 31 + 32 + var req ComAtprotoSignPlcOperationRequest 33 + if err := e.Bind(&req); err != nil { 34 + s.logger.Error("error binding", "error", err) 35 + return helpers.ServerError(e, nil) 36 + } 37 + 38 + if !strings.HasPrefix(repo.Repo.Did, "did:plc:") { 39 + return helpers.InputError(e, nil) 40 + } 41 + 42 + if repo.PlcOperationCode == nil || repo.PlcOperationCodeExpiresAt == nil { 43 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 44 + } 45 + 46 + if *repo.PlcOperationCode != req.Token { 47 + return helpers.InvalidTokenError(e) 48 + } 49 + 50 + if time.Now().UTC().After(*repo.PlcOperationCodeExpiresAt) { 51 + return helpers.ExpiredTokenError(e) 52 + } 53 + 54 + ctx := context.WithValue(e.Request().Context(), "skip-cache", true) 55 + log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did) 56 + if err != nil { 57 + s.logger.Error("error fetching doc", "error", err) 58 + return helpers.ServerError(e, nil) 59 + } 60 + 61 + latest := log[len(log)-1] 62 + 63 + op := plc.Operation{ 64 + Type: "plc_operation", 65 + VerificationMethods: latest.Operation.VerificationMethods, 66 + RotationKeys: latest.Operation.RotationKeys, 67 + AlsoKnownAs: latest.Operation.AlsoKnownAs, 68 + Services: latest.Operation.Services, 69 + Prev: &latest.Cid, 70 + } 71 + if req.VerificationMethods != nil { 72 + op.VerificationMethods = *req.VerificationMethods 73 + } 74 + if req.RotationKeys != nil { 75 + op.RotationKeys = *req.RotationKeys 76 + } 77 + if req.AlsoKnownAs != nil { 78 + op.AlsoKnownAs = *req.AlsoKnownAs 79 + } 80 + if req.Services != nil { 81 + op.Services = *req.Services 82 + } 83 + 84 + k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) 85 + if err != nil { 86 + s.logger.Error("error parsing signing key", "error", err) 87 + return helpers.ServerError(e, nil) 88 + } 89 + 90 + if err := s.plcClient.SignOp(k, &op); err != nil { 91 + s.logger.Error("error signing plc operation", "error", err) 92 + return helpers.ServerError(e, nil) 93 + } 94 + 95 + if err := s.db.Exec("UPDATE repos SET plc_operation_code = NULL, plc_operation_code_expires_at = NULL WHERE did = ?", nil, repo.Repo.Did).Error; err != nil { 96 + s.logger.Error("error updating repo", "error", err) 97 + return helpers.ServerError(e, nil) 98 + } 99 + 100 + return e.JSON(200, ComAtprotoSignPlcOperationResponse{ 101 + Operation: op, 102 + }) 103 + }
+19
server/mail.go
··· 40 40 return nil 41 41 } 42 42 43 + func (s *Server) sendPlcTokenReset(email, handle, code string) error { 44 + if s.mail == nil { 45 + return nil 46 + } 47 + 48 + s.mailLk.Lock() 49 + defer s.mailLk.Unlock() 50 + 51 + s.mail.To(email) 52 + s.mail.Subject("PLC token for " + s.config.Hostname) 53 + s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your PLC operation code is %s. This code will expire in ten minutes.", handle, code)) 54 + 55 + if err := s.mail.Send(); err != nil { 56 + return err 57 + } 58 + 59 + return nil 60 + } 61 + 43 62 func (s *Server) sendEmailUpdate(email, handle, code string) error { 44 63 if s.mail == nil { 45 64 return nil
+2
server/server.go
··· 461 461 s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 462 462 s.echo.GET("/xrpc/com.atproto.identity.getRecommendedDidCredentials", s.handleGetRecommendedDidCredentials, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 463 463 s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 464 + s.echo.POST("/xrpc/com.atproto.identity.requestPlcOperationSignature", s.handleIdentityRequestPlcOperationSignature, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 465 + s.echo.POST("/xrpc/com.atproto.identity.signPlcOperation", s.handleSignPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 464 466 s.echo.POST("/xrpc/com.atproto.identity.submitPlcOperation", s.handleSubmitPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 465 467 s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 466 468 s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)