From de2170b71b69e9b2e4cda2fa130508b057d1c465 Mon Sep 17 00:00:00 2001 From: gabe Date: Fri, 8 Dec 2023 14:25:01 -0800 Subject: [PATCH 01/12] Swagger stuff --- impl/cmd/main.go | 9 --------- impl/docs/swagger.yaml | 4 ++++ impl/magefile.go | 2 +- impl/pkg/server/server.go | 11 +++++++++++ 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/impl/cmd/main.go b/impl/cmd/main.go index 2f996430..0c6cda18 100644 --- a/impl/cmd/main.go +++ b/impl/cmd/main.go @@ -16,15 +16,6 @@ import ( var commitHash string -// main godoc -// -// @title The DID DHT Service -// @description The DID DHT Service -// @contact.name TBD -// @contact.url https://github.com/TBD54566975/did-dht-method -// @contact.email tbd-developer@squareup.com -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html func main() { logrus.Info("Starting up...") diff --git a/impl/docs/swagger.yaml b/impl/docs/swagger.yaml index b8389710..13c0f29c 100644 --- a/impl/docs/swagger.yaml +++ b/impl/docs/swagger.yaml @@ -5,6 +5,9 @@ definitions: description: Status is always equal to `OK`. type: string type: object +externalDocs: + description: OpenAPI + url: https://swagger.io/resources/open-api/ info: contact: email: tbd-developer@squareup.com @@ -15,6 +18,7 @@ info: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html title: The DID DHT Service + version: "0.1" paths: /{id}: get: diff --git a/impl/magefile.go b/impl/magefile.go index d60977b9..6b48e6bd 100644 --- a/impl/magefile.go +++ b/impl/magefile.go @@ -118,7 +118,7 @@ func Spec() error { return err } - return sh.Run(swagCommand, "init", "-g", "cmd/main.go", "--overridesFile", "docs/overrides.swaggo", "--pd", "--parseInternal", "-ot", "yaml") + return sh.Run(swagCommand, "init", "-g", "pkg/server/server.go", "--overridesFile", "docs/overrides.swaggo", "--pd", "--parseInternal", "-ot", "yaml") } func ColorizeTestOutput(w io.Writer) io.Writer { diff --git a/impl/pkg/server/server.go b/impl/pkg/server/server.go index f3e1e44e..361b4185 100644 --- a/impl/pkg/server/server.go +++ b/impl/pkg/server/server.go @@ -33,6 +33,17 @@ type Server struct { } // NewServer returns a new instance of Server with the given db and host. +// +// @title The DID DHT Service +// @version 0.1 +// @description The DID DHT Service +// @contact.name TBD +// @contact.url https://github.com/TBD54566975/did-dht-method +// @contact.email tbd-developer@squareup.com +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ func NewServer(cfg *config.Config, shutdown chan os.Signal) (*Server, error) { // set up server prerequisites setupLogger(cfg.ServerConfig.LogLevel) From afca9074129138a32c99608d462b9247e8ecfb11 Mon Sep 17 00:00:00 2001 From: gabe Date: Fri, 8 Dec 2023 14:44:45 -0800 Subject: [PATCH 02/12] scaffolding --- impl/pkg/server/gateway.go | 34 ++++++++++++++++++ impl/pkg/server/server.go | 35 ++++++++++++++----- impl/pkg/service/gateway.go | 69 +++++++++++++++++++++++++++++++++++++ spec/api.yaml | 22 ++++++------ 4 files changed, 140 insertions(+), 20 deletions(-) create mode 100644 impl/pkg/service/gateway.go diff --git a/impl/pkg/server/gateway.go b/impl/pkg/server/gateway.go index abb4e431..5da7d0d1 100644 --- a/impl/pkg/server/gateway.go +++ b/impl/pkg/server/gateway.go @@ -1 +1,35 @@ package server + +import ( + "github.com/gin-gonic/gin" + + "github.com/TBD54566975/did-dht-method/pkg/service" +) + +type GatewayRouter struct { + service *service.GatewayService +} + +func NewGatewayRouter(service *service.GatewayService) (*GatewayRouter, error) { + return &GatewayRouter{service: service}, nil +} + +func (r *GatewayRouter) PublishDID(c *gin.Context) { + +} + +func (r *GatewayRouter) GetDID(c *gin.Context) { + +} + +func (r *GatewayRouter) GetTypes(c *gin.Context) { + +} + +func (r *GatewayRouter) GetDIDsForType(c *gin.Context) { + +} + +func (r *GatewayRouter) GetDifficulty(c *gin.Context) { + +} diff --git a/impl/pkg/server/server.go b/impl/pkg/server/server.go index 361b4185..d8acd6b0 100644 --- a/impl/pkg/server/server.go +++ b/impl/pkg/server/server.go @@ -58,6 +58,10 @@ func NewServer(cfg *config.Config, shutdown chan os.Signal) (*Server, error) { if err != nil { return nil, util.LoggingErrorMsg(err, "could not instantiate pkarr service") } + gatewayService, err := service.NewGatewayService(cfg, db, pkarrService) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not instantiate gateway service") + } handler.GET("/health", Health) @@ -69,6 +73,12 @@ func NewServer(cfg *config.Config, shutdown chan os.Signal) (*Server, error) { if err = PkarrAPI(&handler.RouterGroup, pkarrService); err != nil { return nil, util.LoggingErrorMsg(err, "could not setup pkarr API") } + + // gateway API + if err = GatewayAPI(&handler.RouterGroup, gatewayService); err != nil { + return nil, util.LoggingErrorMsg(err, "could not setup gateway API") + } + return &Server{ Server: &http.Server{ Addr: fmt.Sprintf("%s:%d", cfg.ServerConfig.APIHost, cfg.ServerConfig.APIPort), @@ -134,12 +144,19 @@ func PkarrAPI(rg *gin.RouterGroup, service *service.PkarrService) error { return nil } -// func GatewayAPI(rg *gin.RouterGroup, service *service.PkarrService) error { -// gatewayRouter, err := NewGatewayRouter(service) -// if err != nil { -// return util.LoggingErrorMsg(err, "could not instantiate gateway router") -// } -// -// rg.GET("/did", gatewayRouter.GetRecord) -// return nil -// } +// GatewayAPI sets up the gateway API routes according to the spec https://did-dht.com/#gateway-api +func GatewayAPI(rg *gin.RouterGroup, service *service.GatewayService) error { + gatewayRouter, err := NewGatewayRouter(service) + if err != nil { + return util.LoggingErrorMsg(err, "could not instantiate gateway router") + } + + rg.GET("/difficulty", gatewayRouter.GetDifficulty) + + didsAPI := rg.Group("/dids") + didsAPI.PUT("", gatewayRouter.PublishDID) + didsAPI.GET("/:id", gatewayRouter.GetDID) + didsAPI.GET("/types", gatewayRouter.GetDIDsForType) + didsAPI.GET("/types/:id", gatewayRouter.GetDIDsForType) + return nil +} diff --git a/impl/pkg/service/gateway.go b/impl/pkg/service/gateway.go new file mode 100644 index 00000000..2214ac36 --- /dev/null +++ b/impl/pkg/service/gateway.go @@ -0,0 +1,69 @@ +package service + +import ( + "github.com/TBD54566975/ssi-sdk/util" + + "github.com/TBD54566975/did-dht-method/config" + "github.com/TBD54566975/did-dht-method/pkg/storage" +) + +type GatewayService struct { + cfg *config.Config + db *storage.Storage + pkarr *PkarrService +} + +func NewGatewayService(cfg *config.Config, db *storage.Storage, pkarrService *PkarrService) (*GatewayService, error) { + if cfg == nil { + return nil, util.LoggingNewError("config is required") + } + if db == nil && !db.IsOpen() { + return nil, util.LoggingNewError("storage is required be non-nil and to be open") + } + if pkarrService == nil { + return nil, util.LoggingNewError("pkarr service is required") + } + return &GatewayService{ + cfg: cfg, + db: db, + pkarr: pkarrService, + }, nil +} + +type PublishDIDRequest struct { +} + +func (s *GatewayService) PublishDID(req *PublishDIDRequest) error { + return nil +} + +type GetDIDResponse struct { +} + +func (s *GatewayService) GetDID(id string) (*GetDIDResponse, error) { + return nil, nil +} + +type GetTypesResponse struct { +} + +func (s *GatewayService) GetTypes() (*GetTypesResponse, error) { + return nil, nil +} + +type GetDIDsForTypeRequest struct { +} + +type GetDIDsForTypeResponse struct { +} + +func (s *GatewayService) GetDIDsForType(req *GetDIDsForTypeRequest) (*GetDIDsForTypeResponse, error) { + return nil, nil +} + +type GetDIDsForTypesResponse struct { +} + +func (s *GatewayService) GetDifficulty() { + +} diff --git a/spec/api.yaml b/spec/api.yaml index 5a7ac863..286cebe0 100644 --- a/spec/api.yaml +++ b/spec/api.yaml @@ -5,7 +5,7 @@ info: license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html - version: Working Draft + version: v0.1 paths: /{id}: get: @@ -83,12 +83,12 @@ paths: application/json: schema: type: string - /did: + /dids: put: tags: - DID - summary: Register or Update a DID - description: Register or Updte a DID in the DHT + summary: Publish a DID + description: Register or Update a DID in the DHT requestBody: description: A deconstructed Pkarr request object content: @@ -101,13 +101,13 @@ paths: description: The DID to register or update. sig: type: string - descrption: A base64URL-encoded signature of the BEP44 payload. + description: A base64URL-encoded signature of the BEP44 payload. seq: type: integer description: A sequence number for the request, recommended to be a unix timestamp in seconds. v: type: string - descrption: A base64URL-encoded bencoded DNS packet containing the DID Document. + description: A base64URL-encoded bencoded DNS packet containing the DID Document. retention_proof: type: string description: A retention proof calculated according to the spec-defined retention proof algorithm. @@ -137,7 +137,7 @@ paths: application/json: schema: type: string - /did/{id}: + /dids/{id}: get: tags: - DID @@ -169,7 +169,7 @@ paths: application/json: schema: type: string - /did/types: + /dids/types: get: tags: - DID @@ -177,7 +177,7 @@ paths: description: Retrieve a list of supported indexing types, according to the spec-defined type list. responses: "200": - description: A list of types support, alongisde their human-readable description. + description: A list of types support, alongside their human-readable description. content: application/json: schema: @@ -196,7 +196,7 @@ paths: application/json: schema: type: string - /did/types/{id}: + /dids/types/{id}: get: tags: - DID @@ -216,7 +216,7 @@ paths: type: integer - name: limit in: query - description: Specifies the maximum number of type records to retrieve. Deafult is 100. + description: Specifies the maximum number of type records to retrieve. Default is 100. schema: type: integer responses: From 02343933ea3fdf6e5bf48aa1ac4e8adaeb09e120 Mon Sep 17 00:00:00 2001 From: gabe Date: Tue, 12 Dec 2023 12:55:48 -0800 Subject: [PATCH 03/12] line wrapping and typos --- impl/pkg/server/gateway.go | 10 ++- spec/api.yaml | 7 +- spec/spec.md | 178 +++++++++++++++++++++++++++---------- 3 files changed, 142 insertions(+), 53 deletions(-) diff --git a/impl/pkg/server/gateway.go b/impl/pkg/server/gateway.go index 5da7d0d1..be7ee442 100644 --- a/impl/pkg/server/gateway.go +++ b/impl/pkg/server/gateway.go @@ -14,6 +14,14 @@ func NewGatewayRouter(service *service.GatewayService) (*GatewayRouter, error) { return &GatewayRouter{service: service}, nil } +type PublishDIDRequest struct { + DID string `json:"did" validate:"required"` + Sig string `json:"sig" validate:"required"` + Seq int `json:"seq" validate:"required"` + V string `json:"v" validate:"required"` + RetentionProof int `json:"retention_proof" validate:"required"` +} + func (r *GatewayRouter) PublishDID(c *gin.Context) { } @@ -31,5 +39,5 @@ func (r *GatewayRouter) GetDIDsForType(c *gin.Context) { } func (r *GatewayRouter) GetDifficulty(c *gin.Context) { - + } diff --git a/spec/api.yaml b/spec/api.yaml index 286cebe0..0c09053b 100644 --- a/spec/api.yaml +++ b/spec/api.yaml @@ -83,10 +83,10 @@ paths: application/json: schema: type: string - /dids: + /dids/{id}: put: tags: - - DID + - DID summary: Publish a DID description: Register or Update a DID in the DHT requestBody: @@ -111,7 +111,7 @@ paths: retention_proof: type: string description: A retention proof calculated according to the spec-defined retention proof algorithm. - required: [did, sig, seq, v] + required: [ did, sig, seq, v ] responses: "202": description: Accepted. The server has accepted the request as valid and will publish it to the @@ -137,7 +137,6 @@ paths: application/json: schema: type: string - /dids/{id}: get: tags: - DID diff --git a/spec/spec.md b/spec/spec.md index ab7e2297..d7c002d4 100644 --- a/spec/spec.md +++ b/spec/spec.md @@ -8,7 +8,8 @@ The DID DHT Method Specification 1.0 **Registry:** [https://did-dht.com/registry](https://did-dht.com/registry) **Draft Created:** October 20, 2023 -**Latest Update:** December 6, 2023 + +**Latest Update:** December 12, 2023 **Editors:** ~ [Gabe Cohen](https://github.com/decentralgabe) @@ -93,10 +94,14 @@ between the Domain Name System and peer-to-peer overlay networks, enabling self- sovereign, publicly addressable domains." [[def:Mainline DHT, DHT, Mainline, Mainline Node]] -~ [Mainline DHT](https://en.wikipedia.org/wiki/Mainline_DHT) is the name given to the DHT used by the BitTorrent protocol. It is a distributed system for storing and finding data on a peer-to-peer network. It is based on [Kademlia](https://en.wikipedia.org/wiki/Kademlia) and is primarily used to store and retrieve _torrent_ metadata. It has between 16 and 28 million concurrent users. +~ [Mainline DHT](https://en.wikipedia.org/wiki/Mainline_DHT) is the name given to the DHT used by the BitTorrent +protocol. It is a distributed system for storing and finding data on a peer-to-peer network. It is based on +[Kademlia](https://en.wikipedia.org/wiki/Kademlia) and is primarily used to store and retrieve _torrent_ metadata. +It has between 16 and 28 million concurrent users. [[def:Gateway, Gateways, Nodes, DID DHT Node, Bitcoin-anchored Gateway]] -~ A node that acts as a gateway to the DID DHT. The gateway may offer a set of APIs to interact with the DID DHT, such as features providing guaranteed retention, historical resolution, and other features. +~ A node that acts as a gateway to the DID DHT. The gateway may offer a set of APIs to interact with the DID DHT, +such as features providing guaranteed retention, historical resolution, and other features. [[def:Registered Gateway, Registered Gateways]] ~ A gateway that has chosen to make itself discoverable via a [[ref:Gateway Registry]] such as [our own registry](registry/index.html#gateways). @@ -105,13 +110,15 @@ sovereign, publicly addressable domains." ~ A system used to make [[ref:Gateways]], more specifically, [[ref:Registered Gateways]] discoverable. [[def:Client, Clients]] -~ A client is a piece of software that is responsible for generating a `did:dht` and submitting it to a [[ref:Mainline]] node or [[ref:Gateway]]. +~ A client is a piece of software that is responsible for generating a `did:dht` and submitting it to a [[ref:Mainline]] + node or [[ref:Gateway]]. [[def:Retained DID Set, Retained Set, Retention Set]] ~ The set of DIDs that a [[ref:Gateway]] is retaining and thus is responsible for republishing. [[def:Retention Proof, Retention Proofs]] -~ A proof of work that is performed by the [[ref:DID]] controller to prove that they are still in control of the DID. Nodes use this proof to determine how long they should retain a DID. +~ A proof of work that is performed by the [[ref:DID]] controller to prove that they are still in control of the DID. +Nodes use this proof to determine how long they should retain a DID. ## DID DHT Method Specification @@ -158,7 +165,8 @@ attributes. * All records ****MUST**** end in `_did.` or `_did.TLD.` if a TLD is associated with the record. ::: note -It might look like repeating `_did` is an overhead, but is compressed away using [DNS packet compression](https://courses.cs.duke.edu/fall16/compsci356/DNS/DNS-primer.pdf) techniques. +It might look like repeating `_did` is an overhead, but is compressed away using +[DNS packet compression](https://courses.cs.duke.edu/fall16/compsci356/DNS/DNS-primer.pdf) techniques. ::: * The DNS packet ****MUST**** set the _Authoritative Answer_ flag, since this is always an _Authoritative_ packet. @@ -179,7 +187,7 @@ The complete identifier is stored in the resource data field (e.g. `id=abcd,t=0, contains a list of IDs of the keys and service endpoints used in different sections of the [[ref:DID Document]]. * Verification Methods, Verification Relationships, and Services are separated by a `;`, while -values within each property are separatred by a `,`. +values within each property are separated by a `,`. An example is as follows: @@ -334,7 +342,8 @@ To create a `did:dht`, the process is as follows: To read a `did:dht`, the process is as follows: -1. Take the suffix of the DID, that is, the _[[ref:z-base-32]] encoded identifier key_, and pass it to a [[ref:Pkarr]] relay or a [[ref:Gateway]]. +1. Take the suffix of the DID, that is, the _[[ref:z-base-32]] encoded identifier key_, and pass it to a [[ref:Pkarr]] + relay or a [[ref:Gateway]]. 2. Decode the resulting [[ref:BEP44]] response's `v` value using [[ref:bencode]]. 3. Reverse the DNS [property mapping](#property-mapping) process and re-construct a conformant [[ref:DID Document]]. @@ -397,16 +406,20 @@ it is essential to delve deeper, employing tools like verifiable credentials and ## Interoperability With Other DID Methods -As an **OPTIONAL** extension, some existing DID methods can leverage `did:dht` to broaden their feature set. This enhancement is most useful for DID -methods that operated based on a single key, compatable with an [[ref:Ed25519]] key format. By adopting this optional extension, users can maintain -their current DIDs without any changes. Additionally, they gain the ability to add extra information to their DIDs. This is achieved by either publishing -or retrieving data from [[ref:Mainline]]. +As an **OPTIONAL** extension, some existing DID methods can leverage `did:dht` to broaden their feature set. This +enhancement is most useful for DID methods that operated based on a single key, compatible with an [[ref:Ed25519]] key +format. By adopting this optional extension, users can maintain their current DIDs without any changes. Additionally, +they gain the ability to add extra information to their DIDs. This is achieved by either publishing or retrieving +data from [[ref:Mainline]]. Interoperable DID methods ****MUST**** be registered in [the corresponding registry](registry/index.html#interoperable-did-methods). ## Gateways -Gateways serve as specialized nodes within the network, providing a range of DID-centric functionalities that extend beyond the capabilities of a standard [[ref:Mainline DHT]] node. This section elaborates on these unique features, outlines the operational prerequisites for managing a gateway, and discusses various other facets, including the optional integration of these gateways into a registry system. +Gateways serve as specialized nodes within the network, providing a range of DID-centric functionalities that extend +beyond the capabilities of a standard [[ref:Mainline DHT]] node. This section elaborates on these unique features, +outlines the operational prerequisites for managing a gateway, and discusses various other facets, including the +optional integration of these gateways into a registry system. ::: note [[ref:Gateways]] may choose to support interoperable methods in addition to `did:dht` as outlined in the @@ -415,40 +428,66 @@ Gateways serve as specialized nodes within the network, providing a range of DID ### Discovering Gateways -As an **OPTIONAL** feature of the DID DHT Method, operators of a [[ref:Gateway]] have the opportunity to make it to a [[ref:Registered Gateway]]. A [[ref:Registered Gateway]] distinguishes itself by being discoverable through a [[ref:Gateway Registry]]. This feature allows for easy location through various internet-based discovery mechanisms. The primary purpose of [[ref:Registered Gateways]] is to simplify the process of finding [[ref:Gateways]], accessible to any entity utilizing a [[ref:Gateway Registry]] to locate registered [[ref:Nodes]]. The [[ref:Gateway Registries]] can vary in nature, encompassing a spectrum from centrally managed directories to diverse decentralized systems including databases, ledgers, or other structures. [[ref:Registered Gateways]] are exposed through the [Gateway Registry](registry/index.html#gateways). +As an **OPTIONAL** feature of the DID DHT Method, operators of a [[ref:Gateway]] have the opportunity to make it to +a [[ref:Registered Gateway]]. A [[ref:Registered Gateway]] distinguishes itself by being discoverable through a +[[ref:Gateway Registry]]. This feature allows for easy location through various internet-based discovery mechanisms. +The primary purpose of [[ref:Registered Gateways]] is to simplify the process of finding [[ref:Gateways]], accessible +to any entity utilizing a [[ref:Gateway Registry]] to locate registered [[ref:Nodes]]. The [[ref:Gateway Registries]] +can vary in nature, encompassing a spectrum from centrally managed directories to diverse decentralized systems +including databases, ledgers, or other structures. [[ref:Registered Gateways]] are exposed through the +[Gateway Registry](registry/index.html#gateways). ### Retained DID Set -A [[ref:Retained DID Set]] refers to the set of DIDs a [[ref:Gateway]] retains and republishes to the DHT. A [[ref:Gateway]] may choose to surface additional [APIs](#gateway-api) based on this set, such as providing a [type index](#type-indexing). +A [[ref:Retained DID Set]] refers to the set of DIDs a [[ref:Gateway]] retains and republishes to the DHT. A +[[ref:Gateway]] may choose to surface additional [APIs](#gateway-api) based on this set, such as providing a +[type index](#type-indexing). -To safeguard equitable access to the resources of [[ref:Gateways]], which are publicly accessible and potentially subject to [a high volume of requests](#rate-limiting), we suggest an ****OPTIONAL**** mechanism aimed at upholding fairness in the retention and republishing of record sets by [[ref:Gateways]]. This mechanism, referred to as a [[ref:Retention Proof]], requires clients to generate a proof value for write requests. This process guarantees that the amount of work done by a client is proportional to the duration of data retention and republishing a [[ref:Gateway]] performs. This mechanism enhances the overall reliability and effectiveness of [[ref:Gateways]] in managing requests. +To safeguard equitable access to the resources of [[ref:Gateways]], which are publicly accessible and potentially +subject to [a high volume of requests](#rate-limiting), we suggest an ****OPTIONAL**** mechanism aimed at upholding +fairness in the retention and republishing of record sets by [[ref:Gateways]]. This mechanism, referred to as a +[[ref:Retention Proof]], requires clients to generate a proof value for write requests. This process guarantees that +the amount of work done by a client is proportional to the duration of data retention and republishing a [[ref:Gateway]] + performs. This mechanism enhances the overall reliability and effectiveness of [[ref:Gateways]] in managing requests. #### Generating a Retention Proof -A [[ref:Retention Proof]] is a form of [Proof of Work](https://en.bitcoin.it/wiki/Proof_of_work) performed over a DID's identifier concatenated with the `retention` value of a given DID operation. The `retention` value is composed of a hash value specified [in the gateway registry](registry/index.html#gateways), and a random [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) using the [SHA-256 hashing algorithm](https://en.wikipedia.org/wiki/SHA-2). The resulting _Retention Proof Hash_ is used to determine the retention duration based on the number of leading zeros of the hash, referred to as the _difficulty_, which ****MUST**** be no less than 26 bits of the 256-bit hash value. The algorithm, in detail, is as follows: +A [[ref:Retention Proof]] is a form of [Proof of Work](https://en.bitcoin.it/wiki/Proof_of_work) performed over a DID +identifier concatenated with the `retention` value of a given DID operation. The `retention` value is composed of a +hash value specified [in the gateway registry](registry/index.html#gateways), and a random +[nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) using the [SHA-256 hashing algorithm](https://en.wikipedia.org/wiki/SHA-2). +The resulting _Retention Proof Hash_ is used to determine the retention duration based on the number of leading zeros +of the hash, referred to as the _difficulty_, which ****MUST**** be no less than 26 bits of the 256-bit hash value. +The algorithm, in detail, is as follows: 1. Obtain a did identifier and set it to `DID`. 2. Get the difficulty and recent hash from the server set to `DIFFICULTY` and `HASH`, respectively. 2. Generate a random 32-bit integer nonce value set to `NONCE`. 3. Compute the [SHA-256](https://en.wikipedia.org/wiki/SHA-2) hash over `ATTEMPT` where `ATTEMPT` = (`DID` + `HASH` + `NONCE`). 4. Inspect the result of `ATTEMPT`, and ensure it has >= `DIFFICULTY` bits of leading zeroes. - a. If so, `ATTEMPT` = `RENTION_PROOF`. + a. If so, `ATTEMPT` = `RETENTION_PROOF`. b. Else, regenerate `NONCE` and go to step 3. 5. Submit the `RETENTION_PROOF` to the [Gateway API](#register=or-update-a-did). #### Managing the Retained DID Set -[[ref:Nodes]] following the [[ref:Retention Set]] rules ****SHOULD**** sort DIDs they are retaining by the number of _leading 0s_ in their [[ref:Retention Proofs]] in descending order, followed by block hash's index number in descending order. When a [[ref:node]] needs to reduce its [[ref:retained set]] of DID entries, it ****SHOULD**** remove entries from the bottom of the list following this sort. +[[ref:Nodes]] following the [[ref:Retention Set]] rules ****SHOULD**** sort DIDs they are retaining by the number of +_leading 0s_ in their [[ref:Retention Proofs]] in descending order, followed by block hash's index number in +descending order. When a [[ref:node]] needs to reduce its [[ref:retained set]] of DID entries, it ****SHOULD**** +remove entries from the bottom of the list following this sort. #### Reporting on Retention Status -Nodes ****MUST**** include the approximate time until retention fall-off in the [DID Resoution Metadata](https://www.w3.org/TR/did-core/#did-resolution-metadata) of a resolved [[ref:DID Document]], to aid [[ref:clients]] in being able to assess whether resubmission is required. +Nodes ****MUST**** include the approximate time until retention fall-off in the +[DID Resolution Metadata](https://www.w3.org/TR/did-core/#did-resolution-metadata) of a resolved +[[ref:DID Document]], to aid [[ref:clients]] in being able to assess whether resubmission is required. ### Gateway API At a minimum, a gateway ****MUST**** support the [Relay API defined by Pkarr](https://github.com/Nuhvi/pkarr/blob/main/design/relays.md). -Expanding on this API, a Gateway ****MUST**** support the following API, which is also made available via an [OpenAPI document](#open-api-definition). +Expanding on this API, a Gateway ****MUST**** support the following API, which is also made available via an +[OpenAPI document](#open-api-definition). #### Get the Current Difficulty @@ -472,8 +511,8 @@ Difficulty is exposed as an **OPTIONAL** endpoint based on support of [retention #### Register or Update a DID - **Method:** `PUT` -- **Path:** `/did` -- **Request Body:** A JSON payload constructed as follows... +- **Path:** `/did/:id` + - `id` - **string** - **REQUIRED** - ID of the DID to publish. - `did` - **string** - **REQUIRED** - The DID to register or update. - `sig` - **string** - **REQUIRED** - A base64URL-encoded signature of the [[ref:BEP44]] payload. - `seq` - **integer** - **REQUIRED** - A sequence number for the request. This number ****MUST**** be unique for each DID operation, @@ -506,7 +545,7 @@ DID by its type. - `id` - **string** - **REQUIRED** - ID of the DID to resolve. - **Returns:** - `200` - Success. - - `did` - **object** - A JSON object representing the DID's Document. + - `did` - **object** - A JSON object representing the DID Document. - `types` - **array** - An array of [type strings](#type-indexing) for the DID. - `sequence_numbers` - **array** - An sorted array of seen sequence numbers, used with [historical resolution](#historical-resolution). - `404` - DID not found. @@ -552,8 +591,8 @@ packet, with its signature data, is required it is ****RECOMMENDED**** to use th [[ref:Nodes]] ****MAY**** choose to support historical resolution, which is to surface different version of the same [[ref:DID Document]], sorted by sequence number, according to the rules set out in the section on [conflict resolution](#conflict-resolution). -Upon [resolving a DID](#resolving-a-did), the Gateway will return the parameter `sequence_numbers` if there exists historical state for -a given [[ref:DID]]. The following API can be used with specific sequence numbers to fetch historical state: +Upon [resolving a DID](#resolving-a-did), the Gateway will return the parameter `sequence_numbers` if there exists +historical state for a given [[ref:DID]]. The following API can be used with specific sequence numbers to fetch historical state: - **Method:** `GET` - **Path:** `/did/:id?seq=:sequence_number` @@ -561,7 +600,7 @@ a given [[ref:DID]]. The following API can be used with specific sequence number - `seq` - **integer** - **OPTIONAL** - Sequence number of the DID to resolve - **Returns**: - `200` - Success. - - `did` - **object** - A JSON object representing the DID's Document. + - `did` - **object** - A JSON object representing the DID Document. - `types` - **array** - An array of [type strings](#type-indexing) for the DID. - `404` - DID not found for the given sequence number. @@ -626,70 +665,113 @@ returned. If no DIDs match the type, an empty array is returned. According to [[ref:BEP44]] [[ref:Nodes]] can leverage the `seq` sequence number to handle conflicts: -> Storing nodes receiving a put request where seq is lower than or equal to what's already stored on the node, ****MUST**** reject the request. If the sequence number is equal, and the value is also the same, the node ****SHOULD**** reset its timeout counter. +> Storing nodes receiving a put request where seq is lower than or equal to what's already stored on the node, +****MUST**** reject the request. If the sequence number is equal, and the value is also the same, the node +****SHOULD**** reset its timeout counter. -When the sequence number is equal, but the value is different, nodes need to decide which value to accept and which to reject. To make this determination nodes ****MUST**** compare the payloads lexicographically to determine a [lexicographical order](https://en.wikipedia.org/wiki/Lexicographic_order), and reject the payload with a **lower** lexicographical order. +When the sequence number is equal, but the value is different, nodes need to decide which value to accept and which +to reject. To make this determination nodes ****MUST**** compare the payloads lexicographically to determine a +[lexicographical order](https://en.wikipedia.org/wiki/Lexicographic_order), and reject the payload with a **lower** +lexicographical order. ### Size Constraints -[[ref:BEP44]] payload sizes are limited to 1000 bytes. Accordingly, we have defined [an efficient representation of a DID Document](#dids-as-a-dns-packet) and leverage DNS packet encoding to optimize our payload sizes. With this encoding format we recommend additional considerations to keep payload sizes minimal: +[[ref:BEP44]] payload sizes are limited to 1000 bytes. Accordingly, we have defined [an efficient representation of a +DID Document](#dids-as-a-dns-packet) and leverage DNS packet encoding to optimize our payload sizes. With this +encoding format we recommend additional considerations to keep payload sizes minimal: #### Representing Keys -Outside of the encoding of a cryptographic key itself, whose size cannot be further minimized, we ****RECOMMEND**** the following representations of keys and their identifiers with usage of `JsonWebKey`: +Outside of the encoding of a cryptographic key itself, whose size cannot be further minimized, we ****RECOMMEND**** +the following representations of keys and their identifiers with usage of `JsonWebKey`: * The [[ref:Identity Key]]'s identifier ****MUST**** always be `#0`. -* Key identifiers (`kid`s) ****MAY**** be omitted. If omitted, upon reconstruction of a DID Document, the JWK's key ID is set to its JWK Thumbprint [[spec:RFC7638]]. +* Key identifiers (`kid`s) ****MAY**** be omitted. If omitted, upon reconstruction of a DID Document, the JWK `kid` +is set to its JWK Thumbprint [[spec:RFC7638]]. #### Historical Key State -However, key rotation is a commonly recommended security practice, which could lead to having many historically necessary keys in a [[ref: DID Document]], increasing the size of the document. To address this concern and to distinguish between keys that are currently active and keys that are no longer used but were once considered valid users ****MAY**** make use of the [service property](https://www.w3.org/TR/did-core/#services) to store signed records of historical key state, saving space in the [[ref:DID Document]] itself. +However, key rotation is a commonly recommended security practice, which could lead to having many historically +necessary keys in a [[ref: DID Document]], increasing the size of the document. To address this concern and to +distinguish between keys that are currently active and keys that are no longer used but were once considered valid +users ****MAY**** make use of the [service property](https://www.w3.org/TR/did-core/#services) to store signed records +of historical key state, saving space in the [[ref:DID Document]] itself. ### Republishing Data -[[ref:Mainline]] offers a limited duration (approximately 2 hours) for retaining records in the DHT. To ensure the verifiability of data signed by a [[ref:DID]], consistent republishing of [[ref:DID Document]] records is crucial. To address this, it is ****RECOMMENDED**** to use [[ref:Gateways]] equipped with [[ref:Retention Proofs]] support. +[[ref:Mainline]] offers a limited duration (approximately 2 hours) for retaining records in the DHT. To ensure the +verifiability of data signed by a [[ref:DID]], consistent republishing of [[ref:DID Document]] records is crucial. To +address this, it is ****RECOMMENDED**** to use [[ref:Gateways]] equipped with [[ref:Retention Proofs]] support. ### Rate Limiting -To reduce the risk of [Denial of Service Attacks](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks), spam, and other unwanted traffic, it is ****RECOMMENDED**** that [[ref:Gateways]] require [[ref:Retention Proofs]]. The use of [[ref:Retention Proofs]] can act as an attack prevention measure, as it would be costly to scale retention proof calculations. [[ref:Nodes]] ****MAY**** choose to explore other rate limiting techniques, such as IP-limiting, or an access-token based approach. +To reduce the risk of [Denial of Service Attacks](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks), +spam, and other unwanted traffic, it is ****RECOMMENDED**** that [[ref:Gateways]] require [[ref:Retention Proofs]]. The +use of [[ref:Retention Proofs]] can act as an attack prevention measure, as it would be costly to scale retention proof +calculations. [[ref:Nodes]] ****MAY**** choose to explore other rate limiting techniques, such as IP-limiting, or an +access-token based approach. ## Security and Privacy Considerations -When implementing and using the `did:dht` method, there are several security and privacy considerations to be aware of to ensure expected and legitimate behavior. +When implementing and using the `did:dht` method, there are several security and privacy considerations to be aware of +to ensure expected and legitimate behavior. ### Data Conflicts -Malicious actors may try to force [[ref:Nodes]] into uncertain states by manipulating the sequence number associated with a record set. There are three such cases to be aware of: +Malicious actors may try to force [[ref:Nodes]] into uncertain states by manipulating the sequence number associated +with a record set. There are three such cases to be aware of: -- **Low Sequence Number** - If a [[ref:Node]] has yet to see sequence numbers for a given record it ****MUST**** query its peers to see if they have encountered the record. If a peer is found who has encountered the record, the record with the latest sequence number must be selected. If the node has encountered greater sequence numbers before, the node ****MAY**** reject the record set. If the node supports [historical resolution](#historical-resolution) it ****MAY**** choose to accept the request and insert the record into its historical ordered state. +- **Low Sequence Number** - If a [[ref:Node]] has yet to see sequence numbers for a given record it ****MUST**** query +its peers to see if they have encountered the record. If a peer is found who has encountered the record, the record +with the latest sequence number must be selected. If the node has encountered greater sequence numbers before, the +node ****MAY**** reject the record set. If the node supports [historical resolution](#historical-resolution) it +****MAY**** choose to accept the request and insert the record into its historical ordered state. -- **Conflicting Sequence Number** - When a malicious actor publishes _valid but conflicting_ records to two different [[ref:Mainline Nodes]] or [[ref:Gateways]]. Implementers are encouraged to follow the guidance outlined in [conflict resolution](#conflict-resolution). +- **Conflicting Sequence Number** - When a malicious actor publishes _valid but conflicting_ records to two different +[[ref:Mainline Nodes]] or [[ref:Gateways]]. Implementers are encouraged to follow the guidance outlined in [conflict +resolution](#conflict-resolution). -- **High Sequence Number** - Since sequence numbers ****MUST**** be second representations of [Unix time](https://en.wikipedia.org/wiki/Unix_time), it is ****RECOMMENDED**** that nodes reject sequence numbers that represent timestamps greater than **2 hours** into the future. +- **High Sequence Number** - Since sequence numbers ****MUST**** be second representations of [Unix +time](https://en.wikipedia.org/wiki/Unix_time), it is ****RECOMMENDED**** that nodes reject sequence numbers that +represent timestamps greater than **2 hours** into the future. ### Data Availability -Given the nature of decentralized distributed systems, there are no firm guarantees that all [[ref:Nodes]] have access to the same state. It is ****RECOMMENDED**** to publish and read from multiple [[ref:Gateways]] to reduce such risks. As an **optional** enhancement [[ref:Gateways]] ****MAY**** choose to share state amongst themselves via mechanisms such as a [gossip protocol](https://en.wikipedia.org/wiki/Gossip_protocol). +Given the nature of decentralized distributed systems, there are no firm guarantees that all [[ref:Nodes]] have access +to the same state. It is ****RECOMMENDED**** to publish and read from multiple [[ref:Gateways]] to reduce such risks. +As an **optional** enhancement [[ref:Gateways]] ****MAY**** choose to share state amongst themselves via mechanisms +such as a [gossip protocol](https://en.wikipedia.org/wiki/Gossip_protocol). ### Data Authenticity -To enter into the DHT using [[ref:BEP44]] your records must be signed by an [[ref:Ed25519]] private key. When retrieving records either through a [[ref:Mainline Node]] or a [[ref:Gateway]] is it ****RECOMMENDED**** that one verifies the cryptographic integrity of the record themselves instead of trusting a node to have done the validation. Nodes that do not return a signature value ****MUST NOT**** be trusted. +To enter into the DHT using [[ref:BEP44]] your records must be signed by an [[ref:Ed25519]] private key. When retrieving +records either through a [[ref:Mainline Node]] or a [[ref:Gateway]] is it ****RECOMMENDED**** that one verifies the +cryptographic integrity of the record themselves instead of trusting a node to have done the validation. Nodes that do +not return a signature value ****MUST NOT**** be trusted. ### Key Compromise -Since the `did:dht` uses a single, un-rotatable root key, there is a risk of root key compromise. Such a compromise may be tough to detect without external assurances of identity. Implementers are encouraged to be aware of this possibility and devise strategies that support entities transitioning to new [[ref:DIDs]] over time. +Since the `did:dht` uses a single, un-rotatable root key, there is a risk of root key compromise. Such a compromise +may be tough to detect without external assurances of identity. Implementers are encouraged to be aware of this +possibility and devise strategies that support entities transitioning to new [[ref:DIDs]] over time. ### Public Data -[[ref:Mainline]] is a public network. As such, there is risk in storing private, sensitive, or personally identifying information (PII) on such a network. Storing such sensitive information on the network or in the contents of a `did:dht` document is strongly discouraged. +[[ref:Mainline]] is a public network. As such, there is risk in storing private, sensitive, or personally identifying +information (PII) on such a network. Storing such sensitive information on the network or in the contents of a `did:dht` +document is strongly discouraged. ### Data Retention -It is ****RECOMMENDED**** that [[ref:Gateways]] implement measures supporting the "[Right to be Forgotten](https://en.wikipedia.org/wiki/Right_to_be_forgotten)," enabling precise control over the data retention duration. +It is ****RECOMMENDED**** that [[ref:Gateways]] implement measures supporting the "[Right to be +Forgotten](https://en.wikipedia.org/wiki/Right_to_be_forgotten)," enabling precise control over the data retention duration. ### Cryptographic Risk -The security of data within the [[ref:Mainline DHT]] which relies on mutable records using [[ref:Ed25519]] keys—is intrinsically tied to the strength of these keys and their underlying algorithms, as outlined in [[spec:RFC8032]]. Should vulnerabilities be discovered in [[ref:Ed25519]] or if advancements in quantum computing compromise its cryptographic foundations, the [[ref:Mainline]] method could become obsolete. +The security of data within the [[ref:Mainline DHT]] which relies on mutable records using [[ref:Ed25519]] keys—is +intrinsically tied to the strength of these keys and their underlying algorithms, as outlined in [[spec:RFC8032]]. +Should vulnerabilities be discovered in [[ref:Ed25519]] or if advancements in quantum computing compromise its +cryptographic foundations, the [[ref:Mainline]] method could become obsolete. ## Appendix @@ -869,7 +951,7 @@ format. [Bittorrent.org](https://www.bittorrent.org/). Z. O'Whielacronx; November 2002. [[def:VC-JOSE-COSE]] -~ [Securing Verifiable Credentials using JOSE and COSE](https://www.w3.org/TR/vc-jose-cose/). O. Steele, M. Jones, M. Prorock, G. Cohen; 04 -December 2023. [W3C](https://www.w3.org/). +~ [Securing Verifiable Credentials using JOSE and COSE](https://www.w3.org/TR/vc-jose-cose/). O. Steele, M. Jones, +M. Prorock, G. Cohen; 04 December 2023. [W3C](https://www.w3.org/). [[spec]] \ No newline at end of file From 913ce3de3e0cd58cdc4b9c0395794c10058bb1f0 Mon Sep 17 00:00:00 2001 From: gabe Date: Tue, 12 Dec 2023 13:13:13 -0800 Subject: [PATCH 04/12] update spec --- impl/pkg/server/errors.go | 13 +++++++ impl/pkg/server/gateway.go | 52 ++++++++++++++++++++++++-- impl/pkg/service/gateway.go | 7 +++- spec/api.yaml | 74 ++++++++++++++++++++++--------------- spec/spec.md | 6 ++- 5 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 impl/pkg/server/errors.go diff --git a/impl/pkg/server/errors.go b/impl/pkg/server/errors.go new file mode 100644 index 00000000..be7a3b63 --- /dev/null +++ b/impl/pkg/server/errors.go @@ -0,0 +1,13 @@ +package server + +type InvalidSignatureError struct{} + +func (e *InvalidSignatureError) Error() string { + return "invalid signature" +} + +type HigherSequenceNumberError struct{} + +func (e *HigherSequenceNumberError) Error() string { + return "DID already exists with a higher sequence number" +} diff --git a/impl/pkg/server/gateway.go b/impl/pkg/server/gateway.go index be7ee442..77d51784 100644 --- a/impl/pkg/server/gateway.go +++ b/impl/pkg/server/gateway.go @@ -1,7 +1,10 @@ package server import ( + "net/http" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" "github.com/TBD54566975/did-dht-method/pkg/service" ) @@ -15,19 +18,62 @@ func NewGatewayRouter(service *service.GatewayService) (*GatewayRouter, error) { } type PublishDIDRequest struct { - DID string `json:"did" validate:"required"` Sig string `json:"sig" validate:"required"` Seq int `json:"seq" validate:"required"` V string `json:"v" validate:"required"` - RetentionProof int `json:"retention_proof" validate:"required"` + RetentionProof int `json:"retention_proof,omitempty"` +} + +func (p PublishDIDRequest) toServiceRequest(did string) service.PublishDIDRequest { + return service.PublishDIDRequest{ + DID: did, + Sig: p.Sig, + Seq: p.Seq, + V: p.V, + RetentionProof: p.RetentionProof, + } } func (r *GatewayRouter) PublishDID(c *gin.Context) { + id := GetParam(c, IDParam) + if id == nil || *id == "" { + LoggingRespondErrMsg(c, "missing id param", http.StatusBadRequest) + return + } + + var req PublishDIDRequest + if err := Decode(c.Request, &req); err != nil { + LoggingRespondErrWithMsg(c, err, "failed to decode request", http.StatusBadRequest) + return + } + // three possible errors + // 1. invalid signature + // 2. did already exists with a higher sequence number + // 3. internal service error + if err := r.service.PublishDID(req.toServiceRequest(*id)); err != nil { + if errors.Is(err, &InvalidSignatureError{}) { + Respond(c, nil, http.StatusUnauthorized) + return + } + + if errors.Is(err, &HigherSequenceNumberError{}) { + Respond(c, nil, http.StatusConflict) + return + } + + LoggingRespondErrWithMsg(c, err, "failed to publish did", http.StatusInternalServerError) + } + + Respond(c, nil, http.StatusAccepted) } func (r *GatewayRouter) GetDID(c *gin.Context) { - + id := GetParam(c, IDParam) + if id == nil || *id == "" { + LoggingRespondErrMsg(c, "missing id param", http.StatusBadRequest) + return + } } func (r *GatewayRouter) GetTypes(c *gin.Context) { diff --git a/impl/pkg/service/gateway.go b/impl/pkg/service/gateway.go index 2214ac36..0f2f5653 100644 --- a/impl/pkg/service/gateway.go +++ b/impl/pkg/service/gateway.go @@ -31,9 +31,14 @@ func NewGatewayService(cfg *config.Config, db *storage.Storage, pkarrService *Pk } type PublishDIDRequest struct { + DID string `json:"did" validate:"required"` + Sig string `json:"sig" validate:"required"` + Seq int `json:"seq" validate:"required"` + V string `json:"v" validate:"required"` + RetentionProof int `json:"retention_proof,omitempty"` } -func (s *GatewayService) PublishDID(req *PublishDIDRequest) error { +func (s *GatewayService) PublishDID(req PublishDIDRequest) error { return nil } diff --git a/spec/api.yaml b/spec/api.yaml index 0c09053b..aff08515 100644 --- a/spec/api.yaml +++ b/spec/api.yaml @@ -4,22 +4,22 @@ info: description: "The [DID DHT API](https://did-dht.com)" license: name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0.html + url: https://www.apache.org/licenses/LICENSE-2.0.html version: v0.1 paths: /{id}: get: tags: - - Pkarr Relay + - Pkarr Relay summary: Get Pkarr records from the DHT description: Get a Pkarr record set from the DHT parameters: - - name: id - in: path - description: ID to get - required: true - schema: - type: string + - name: id + in: path + description: ID to get + required: true + schema: + type: string responses: "200": description: "64 bytes sig, 8 bytes u64 big-endian seq, 0-1000 bytes of v." @@ -49,16 +49,16 @@ paths: type: string put: tags: - - Pkarr Relay + - Pkarr Relay summary: Put a Pkarr record set into the DHT description: Put a Pkarr record set into the DHT parameters: - - name: id - in: path - description: ID to put - required: true - schema: - type: string + - name: id + in: path + description: ID to put + required: true + schema: + type: string requestBody: description: "64 bytes sig, 8 bytes u64 big-endian seq, 0-1000 bytes of v." content: @@ -89,6 +89,13 @@ paths: - DID summary: Publish a DID description: Register or Update a DID in the DHT + parameters: + - name: id + in: path + description: DID to publish. + required: true + schema: + type: string requestBody: description: A deconstructed Pkarr request object content: @@ -96,9 +103,6 @@ paths: schema: type: object properties: - did: - type: string - description: The DID to register or update. sig: type: string description: A base64URL-encoded signature of the BEP44 payload. @@ -120,7 +124,7 @@ paths: schema: type: string "400": - description: Invalid request body. + description: Invalid request. content: application/json: schema: @@ -139,10 +143,10 @@ paths: type: string get: tags: - - DID + - DID summary: Resolve a DID description: Resolve a DID from the DHT first, with a fallback to local storage. - parameters: + parameters: - name: id in: path description: DID to resolve. @@ -155,13 +159,19 @@ paths: required: false schema: type: integer - responses: + responses: "200": description: The resolved DID Document. content: application/json: schema: type: object + "400": + description: Invalid request. + content: + application/json: + schema: + type: string "404": description: DID could not be resolved. content: @@ -171,7 +181,7 @@ paths: /dids/types: get: tags: - - DID + - DID summary: Retrieve a list of supported types for indexing. description: Retrieve a list of supported indexing types, according to the spec-defined type list. responses: @@ -188,17 +198,17 @@ paths: type: integer description: type: string - required: [type, description] + required: [ type, description ] "404": description: Type indexing not supported by this gateway. content: application/json: schema: - type: string + type: string /dids/types/{id}: get: tags: - - DID + - DID summary: Retrieve a list of DIDs indexed under a given type. description: Retrieve a list of DIDs indexed under a given type, according to the spec-defined type index. parameters: @@ -226,11 +236,17 @@ paths: schema: type: array items: - type: string + type: string + "404": + description: Type not found. + content: + application/json: + schema: + type: string /difficulty: get: tags: - - DID + - DID summary: Get information about the current difficulty. description: Get information needed to calculate a retention proof for DID PUT operations. responses: @@ -245,7 +261,7 @@ paths: type: string difficulty: type: integer - required: [hash, difficulty] + required: [ hash, difficulty ] "404": description: Retention proofs not supported by this gateway. content: diff --git a/spec/spec.md b/spec/spec.md index d7c002d4..d03485ae 100644 --- a/spec/spec.md +++ b/spec/spec.md @@ -499,7 +499,7 @@ Difficulty is exposed as an **OPTIONAL** endpoint based on support of [retention - `200` - Success. - `hash` - **string** - The current hash. - `difficulty` - **integer** - The current difficulty. - - `404` - Not found. Difficulty not supported by this gateway. + - `404` - Retention proofs not supported by this gateway. ```json { @@ -521,7 +521,7 @@ Difficulty is exposed as an **OPTIONAL** endpoint based on support of [retention - `retention_proof` - **string** – **OPTIONAL** - A retention proof calculated according to the [retention proof algorithm](#generating-a-retention-proof). - **Returns:** - `202` - Accepted. The server has accepted the request as valid and will publish to the DHT. - - `400` - Invalid request body. + - `400` - Invalid request. - `401` - Invalid signature. - `409` - DID already exists with a higher sequence number. DID may be accepted if the [[ref:Gateway]] supports [historical resolution](#historical-resolution). @@ -548,6 +548,7 @@ DID by its type. - `did` - **object** - A JSON object representing the DID Document. - `types` - **array** - An array of [type strings](#type-indexing) for the DID. - `sequence_numbers` - **array** - An sorted array of seen sequence numbers, used with [historical resolution](#historical-resolution). + - `400` - Invalid request. - `404` - DID not found. ```json @@ -602,6 +603,7 @@ historical state for a given [[ref:DID]]. The following API can be used with spe - `200` - Success. - `did` - **object** - A JSON object representing the DID Document. - `types` - **array** - An array of [type strings](#type-indexing) for the DID. + - `400` - Invalid request. - `404` - DID not found for the given sequence number. #### Deactivating a DID From 3e92bfcf90a2911b3fd4ae94ca274ef7414936ab Mon Sep 17 00:00:00 2001 From: gabe Date: Tue, 12 Dec 2023 15:50:49 -0800 Subject: [PATCH 05/12] gateway dfn --- impl/pkg/server/errors.go | 6 ++ impl/pkg/server/gateway.go | 127 ++++++++++++++++++++++++++++++++++++ impl/pkg/service/gateway.go | 22 +++++-- 3 files changed, 151 insertions(+), 4 deletions(-) diff --git a/impl/pkg/server/errors.go b/impl/pkg/server/errors.go index be7a3b63..f1ad306f 100644 --- a/impl/pkg/server/errors.go +++ b/impl/pkg/server/errors.go @@ -11,3 +11,9 @@ type HigherSequenceNumberError struct{} func (e *HigherSequenceNumberError) Error() string { return "DID already exists with a higher sequence number" } + +type TypeNotFoundError struct{} + +func (e *TypeNotFoundError) Error() string { + return "type not found" +} diff --git a/impl/pkg/server/gateway.go b/impl/pkg/server/gateway.go index 77d51784..d9e28129 100644 --- a/impl/pkg/server/gateway.go +++ b/impl/pkg/server/gateway.go @@ -3,6 +3,7 @@ package server import ( "net/http" + "github.com/TBD54566975/ssi-sdk/did" "github.com/gin-gonic/gin" "github.com/pkg/errors" @@ -17,6 +18,7 @@ func NewGatewayRouter(service *service.GatewayService) (*GatewayRouter, error) { return &GatewayRouter{service: service}, nil } +// PublishDIDRequest represents a request to publish a DID type PublishDIDRequest struct { Sig string `json:"sig" validate:"required"` Seq int `json:"seq" validate:"required"` @@ -34,6 +36,19 @@ func (p PublishDIDRequest) toServiceRequest(did string) service.PublishDIDReques } } +// PublishDID godoc +// @Summary Publish a DID document +// @Description Publish a DID document to the DHT +// @Tags DID +// @Accept json +// @Param id path string true "ID of the record to get" +// @Success 202 {object} PublishDIDRequest +// @Failure 400 {string} string "Invalid request body" +// @Failure 401 {string} string "Invalid signature" +// @Failure 409 {string} string "DID already exists with a higher sequence number" +// @Failure 500 {string} string "Internal server error" +// @Router /dids/{id} [put] +// TODO(gabe) support historical document storage https://github.com/TBD54566975/did-dht-method/issues/16 func (r *GatewayRouter) PublishDID(c *gin.Context) { id := GetParam(c, IDParam) if id == nil || *id == "" { @@ -68,22 +83,134 @@ func (r *GatewayRouter) PublishDID(c *gin.Context) { Respond(c, nil, http.StatusAccepted) } +// GetDIDResponse represents a response containing a DID document, types, and sequence numbers. +type GetDIDResponse struct { + DID did.Document `json:"did" validate:"required"` + Types []int `json:"types,omitempty"` + SequenceNumbers []int `json:"sequence_numbers,omitempty"` +} + +// GetDID godoc +// @Summary Get a DID document +// @Description Get a DID document +// @Tags DID +// @Accept json +// @Param id path string true "ID of the record to get" +// @Success 200 {object} GetDIDResponse +// @Failure 400 {string} string "Invalid request" +// @Failure 404 {string} string "DID not found" +// @Failure 500 {string} string "Internal server error" +// @Router /dids/{id} [get] +// TODO(gabe) support historical queries https://github.com/TBD54566975/did-dht-method/issues/16 func (r *GatewayRouter) GetDID(c *gin.Context) { id := GetParam(c, IDParam) if id == nil || *id == "" { LoggingRespondErrMsg(c, "missing id param", http.StatusBadRequest) return } + + resp, err := r.service.GetDID(*id) + if err != nil { + LoggingRespondErrWithMsg(c, err, "failed to get did", http.StatusInternalServerError) + return + } + + if resp == nil { + LoggingRespondErrMsg(c, "did not found", http.StatusNotFound) + return + } + + Respond(c, GetDIDResponse(*resp), http.StatusOK) } +// GetTypesResponse represents a response containing a list of supported types and their names. +type GetTypesResponse struct { + Types []service.TypeMapping `json:"types,omitempty"` +} + +// GetTypes godoc +// @Summary Get a list of supported types +// @Description Get a list of supported types +// @Tags DID +// @Accept json +// @Success 200 {object} GetTypesResponse +// @Failure 404 {string} string "Type indexing is not supported by this gateway" +// @Router /dids/types [get] func (r *GatewayRouter) GetTypes(c *gin.Context) { + resp, err := r.service.GetTypes() + if err != nil { + LoggingRespondErrWithMsg(c, err, "failed to get types", http.StatusInternalServerError) + return + } + + if resp == nil { + LoggingRespondErrMsg(c, "types not supported", http.StatusNotFound) + return + } + + Respond(c, GetTypesResponse(*resp), http.StatusOK) +} +// GetDIDsForTypeResponse represents a response containing a list of DIDs for a given type. +type GetDIDsForTypeResponse struct { + DIDs []string `json:"dids,omitempty"` } +// GetDIDsForType godoc +// @Summary Get a list of DIDs for a given type +// @Description Get a list of DIDs for a given type +// @Tags DID +// @Accept json +// @Success 200 {object} GetDIDsForTypeResponse +// @Failure 404 {string} string "Type not found" +// @Failure 500 {string} string "Internal server error" +// @Router /dids/types/{id} [get] func (r *GatewayRouter) GetDIDsForType(c *gin.Context) { + id := GetParam(c, IDParam) + if id == nil || *id == "" { + LoggingRespondErrMsg(c, "missing id param", http.StatusBadRequest) + return + } + + resp, err := r.service.GetDIDsForType(service.GetDIDsForTypeRequest{Type: *id}) + if err != nil { + if errors.Is(err, &TypeNotFoundError{}) { + LoggingRespondErrMsg(c, "type not found", http.StatusNotFound) + return + } + + LoggingRespondErrWithMsg(c, err, "failed to get dids for type", http.StatusInternalServerError) + return + } + Respond(c, GetDIDsForTypeResponse(*resp), http.StatusOK) } +type GetDifficultyResponse struct { + Hash string `json:"hash" validate:"required"` + Difficulty int `json:"difficulty" validate:"required"` +} + +// GetDifficulty godoc +// @Summary Get the current difficulty for the gateway's retention proof feature +// @Description Get the current difficulty for the gateway's retention proof feature +// @Tags DID +// @Accept json +// @Success 200 {object} int +// @Failure 404 {string} string "Retention proofs are not supported by this gateway" +// @Failure 500 {string} string "Internal server error" +// @Router /difficulty [get] func (r *GatewayRouter) GetDifficulty(c *gin.Context) { + resp, err := r.service.GetDifficulty() + if err != nil { + LoggingRespondErrWithMsg(c, err, "failed to get difficulty", http.StatusInternalServerError) + return + } + + if resp == nil { + LoggingRespondErrMsg(c, "retention proofs not supported", http.StatusNotFound) + return + } + Respond(c, GetDifficultyResponse(*resp), http.StatusOK) } diff --git a/impl/pkg/service/gateway.go b/impl/pkg/service/gateway.go index 0f2f5653..d2d3a455 100644 --- a/impl/pkg/service/gateway.go +++ b/impl/pkg/service/gateway.go @@ -1,6 +1,7 @@ package service import ( + "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/util" "github.com/TBD54566975/did-dht-method/config" @@ -43,6 +44,9 @@ func (s *GatewayService) PublishDID(req PublishDIDRequest) error { } type GetDIDResponse struct { + DID did.Document `json:"did" validate:"required"` + Types []int `json:"types,omitempty"` + SequenceNumbers []int `json:"sequence_numbers,omitempty"` } func (s *GatewayService) GetDID(id string) (*GetDIDResponse, error) { @@ -50,6 +54,12 @@ func (s *GatewayService) GetDID(id string) (*GetDIDResponse, error) { } type GetTypesResponse struct { + Types []TypeMapping `json:"types,omitempty"` +} + +type TypeMapping struct { + TypeIndex int `json:"type_index" validate:"required"` + Type string `json:"type" validate:"required"` } func (s *GatewayService) GetTypes() (*GetTypesResponse, error) { @@ -57,18 +67,22 @@ func (s *GatewayService) GetTypes() (*GetTypesResponse, error) { } type GetDIDsForTypeRequest struct { + Type string `json:"type" validate:"required"` } type GetDIDsForTypeResponse struct { + DIDs []string `json:"dids,omitempty"` } -func (s *GatewayService) GetDIDsForType(req *GetDIDsForTypeRequest) (*GetDIDsForTypeResponse, error) { +func (s *GatewayService) GetDIDsForType(req GetDIDsForTypeRequest) (*GetDIDsForTypeResponse, error) { return nil, nil } -type GetDIDsForTypesResponse struct { +type GetDifficultyResponse struct { + Hash string `json:"hash" validate:"required"` + Difficulty int `json:"difficulty" validate:"required"` } -func (s *GatewayService) GetDifficulty() { - +func (s *GatewayService) GetDifficulty() (*GetDifficultyResponse, error) { + return nil, nil } From 8116ddafba8b75b8b12e1ebb025f9516657f36aa Mon Sep 17 00:00:00 2001 From: gabe Date: Tue, 12 Dec 2023 16:29:50 -0800 Subject: [PATCH 06/12] tmp --- impl/pkg/server/gateway.go | 7 +++-- impl/pkg/server/server.go | 2 +- impl/pkg/service/gateway.go | 62 +++++++++++++++++++++++++++++++++++-- impl/pkg/service/pkarr.go | 4 +-- 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/impl/pkg/server/gateway.go b/impl/pkg/server/gateway.go index d9e28129..ba14721c 100644 --- a/impl/pkg/server/gateway.go +++ b/impl/pkg/server/gateway.go @@ -21,9 +21,9 @@ func NewGatewayRouter(service *service.GatewayService) (*GatewayRouter, error) { // PublishDIDRequest represents a request to publish a DID type PublishDIDRequest struct { Sig string `json:"sig" validate:"required"` - Seq int `json:"seq" validate:"required"` + Seq int64 `json:"seq" validate:"required"` V string `json:"v" validate:"required"` - RetentionProof int `json:"retention_proof,omitempty"` + RetentionProof string `json:"retention_proof,omitempty"` } func (p PublishDIDRequest) toServiceRequest(did string) service.PublishDIDRequest { @@ -66,7 +66,7 @@ func (r *GatewayRouter) PublishDID(c *gin.Context) { // 1. invalid signature // 2. did already exists with a higher sequence number // 3. internal service error - if err := r.service.PublishDID(req.toServiceRequest(*id)); err != nil { + if err := r.service.PublishDID(c, req.toServiceRequest(*id)); err != nil { if errors.Is(err, &InvalidSignatureError{}) { Respond(c, nil, http.StatusUnauthorized) return @@ -186,6 +186,7 @@ func (r *GatewayRouter) GetDIDsForType(c *gin.Context) { Respond(c, GetDIDsForTypeResponse(*resp), http.StatusOK) } +// GetDifficultyResponse represents a response containing the current difficulty for the gateway's retention proof feature. type GetDifficultyResponse struct { Hash string `json:"hash" validate:"required"` Difficulty int `json:"difficulty" validate:"required"` diff --git a/impl/pkg/server/server.go b/impl/pkg/server/server.go index d8acd6b0..5d54a7dd 100644 --- a/impl/pkg/server/server.go +++ b/impl/pkg/server/server.go @@ -154,7 +154,7 @@ func GatewayAPI(rg *gin.RouterGroup, service *service.GatewayService) error { rg.GET("/difficulty", gatewayRouter.GetDifficulty) didsAPI := rg.Group("/dids") - didsAPI.PUT("", gatewayRouter.PublishDID) + didsAPI.PUT("/:id", gatewayRouter.PublishDID) didsAPI.GET("/:id", gatewayRouter.GetDID) didsAPI.GET("/types", gatewayRouter.GetDIDsForType) didsAPI.GET("/types/:id", gatewayRouter.GetDIDsForType) diff --git a/impl/pkg/service/gateway.go b/impl/pkg/service/gateway.go index d2d3a455..73406b89 100644 --- a/impl/pkg/service/gateway.go +++ b/impl/pkg/service/gateway.go @@ -1,10 +1,18 @@ package service import ( + "context" + "crypto/ed25519" + "encoding/base64" + "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/util" + "github.com/pkg/errors" + "github.com/tv42/zbase32" "github.com/TBD54566975/did-dht-method/config" + didint "github.com/TBD54566975/did-dht-method/internal/did" + "github.com/TBD54566975/did-dht-method/pkg/server" "github.com/TBD54566975/did-dht-method/pkg/storage" ) @@ -34,12 +42,60 @@ func NewGatewayService(cfg *config.Config, db *storage.Storage, pkarrService *Pk type PublishDIDRequest struct { DID string `json:"did" validate:"required"` Sig string `json:"sig" validate:"required"` - Seq int `json:"seq" validate:"required"` + Seq int64 `json:"seq" validate:"required"` V string `json:"v" validate:"required"` - RetentionProof int `json:"retention_proof,omitempty"` + RetentionProof string `json:"retention_proof,omitempty"` +} + +func (p PublishDIDRequest) toPkarrRequest(suffix string) (*PublishPkarrRequest, error) { + keyBytes, err := zbase32.DecodeString(suffix) + if err != nil { + return nil, err + } + if len(keyBytes) != ed25519.PublicKeySize { + return nil, errors.New("invalid key length") + } + encoding := base64.RawURLEncoding + sigBytes, err := encoding.DecodeString(p.Sig) + if err != nil { + return nil, err + } + if len(sigBytes) != ed25519.SignatureSize { + return nil, &server.InvalidSignatureError{} + } + vBytes, err := encoding.DecodeString(p.V) + if err != nil { + return nil, err + } + if len(vBytes) > 1000 { + return nil, errors.New("v exceeds 1000 bytes") + } + return &PublishPkarrRequest{ + V: vBytes, + K: [32]byte(keyBytes), + Sig: [64]byte(sigBytes), + Seq: p.Seq, + }, nil } -func (s *GatewayService) PublishDID(req PublishDIDRequest) error { +func (s *GatewayService) PublishDID(ctx context.Context, req PublishDIDRequest) error { + suffix, err := didint.DHT(req.DID).Suffix() + if err != nil { + return err + } + pkarrRequest, err := req.toPkarrRequest(suffix) + if err != nil { + return err + } + + // unpack as a DID Document and store metadata + + // publish to the network + // TODO(gabe): check for conflicts with existing record sequence numbers https://github.com/TBD54566975/did-dht-method/issues/16 + if err = s.pkarr.PublishPkarr(ctx, suffix, *pkarrRequest); err != nil { + return err + } + return nil } diff --git a/impl/pkg/service/pkarr.go b/impl/pkg/service/pkarr.go index 8eeb69f2..c8770467 100644 --- a/impl/pkg/service/pkarr.go +++ b/impl/pkg/service/pkarr.go @@ -3,7 +3,6 @@ package service import ( "context" "encoding/base64" - "errors" "time" "github.com/goccy/go-json" @@ -17,6 +16,7 @@ import ( "github.com/TBD54566975/did-dht-method/config" dhtint "github.com/TBD54566975/did-dht-method/internal/dht" "github.com/TBD54566975/did-dht-method/pkg/dht" + "github.com/TBD54566975/did-dht-method/pkg/server" "github.com/TBD54566975/did-dht-method/pkg/storage" ) @@ -87,7 +87,7 @@ func (p PublishPkarrRequest) isValid() error { return err } if !bep44.Verify(p.K[:], nil, p.Seq, bv, p.Sig[:]) { - return errors.New("signature is invalid") + return &server.InvalidSignatureError{} } return nil } From 9490bcd933d4a2ddd8b799ac74c71a994a5ec3ce Mon Sep 17 00:00:00 2001 From: gabe Date: Mon, 18 Dec 2023 16:08:06 -0800 Subject: [PATCH 07/12] read write tests --- impl/{pkg/server => internal/util}/errors.go | 2 +- impl/pkg/server/gateway.go | 7 +- impl/pkg/service/gateway.go | 26 ++- impl/pkg/service/pkarr.go | 5 +- impl/pkg/storage/gateway.go | 166 +++++++++++++++++++ impl/pkg/storage/gateway_test.go | 153 +++++++++++++++++ 6 files changed, 350 insertions(+), 9 deletions(-) rename impl/{pkg/server => internal/util}/errors.go (96%) create mode 100644 impl/pkg/storage/gateway.go create mode 100644 impl/pkg/storage/gateway_test.go diff --git a/impl/pkg/server/errors.go b/impl/internal/util/errors.go similarity index 96% rename from impl/pkg/server/errors.go rename to impl/internal/util/errors.go index f1ad306f..bf06acb5 100644 --- a/impl/pkg/server/errors.go +++ b/impl/internal/util/errors.go @@ -1,4 +1,4 @@ -package server +package util type InvalidSignatureError struct{} diff --git a/impl/pkg/server/gateway.go b/impl/pkg/server/gateway.go index ba14721c..e30defa1 100644 --- a/impl/pkg/server/gateway.go +++ b/impl/pkg/server/gateway.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/pkg/errors" + "github.com/TBD54566975/did-dht-method/internal/util" "github.com/TBD54566975/did-dht-method/pkg/service" ) @@ -67,12 +68,12 @@ func (r *GatewayRouter) PublishDID(c *gin.Context) { // 2. did already exists with a higher sequence number // 3. internal service error if err := r.service.PublishDID(c, req.toServiceRequest(*id)); err != nil { - if errors.Is(err, &InvalidSignatureError{}) { + if errors.Is(err, &util.InvalidSignatureError{}) { Respond(c, nil, http.StatusUnauthorized) return } - if errors.Is(err, &HigherSequenceNumberError{}) { + if errors.Is(err, &util.HigherSequenceNumberError{}) { Respond(c, nil, http.StatusConflict) return } @@ -174,7 +175,7 @@ func (r *GatewayRouter) GetDIDsForType(c *gin.Context) { resp, err := r.service.GetDIDsForType(service.GetDIDsForTypeRequest{Type: *id}) if err != nil { - if errors.Is(err, &TypeNotFoundError{}) { + if errors.Is(err, &util.TypeNotFoundError{}) { LoggingRespondErrMsg(c, "type not found", http.StatusNotFound) return } diff --git a/impl/pkg/service/gateway.go b/impl/pkg/service/gateway.go index 73406b89..f27d5ddd 100644 --- a/impl/pkg/service/gateway.go +++ b/impl/pkg/service/gateway.go @@ -7,12 +7,13 @@ import ( "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/util" + "github.com/miekg/dns" "github.com/pkg/errors" "github.com/tv42/zbase32" "github.com/TBD54566975/did-dht-method/config" didint "github.com/TBD54566975/did-dht-method/internal/did" - "github.com/TBD54566975/did-dht-method/pkg/server" + intutil "github.com/TBD54566975/did-dht-method/internal/util" "github.com/TBD54566975/did-dht-method/pkg/storage" ) @@ -61,7 +62,7 @@ func (p PublishDIDRequest) toPkarrRequest(suffix string) (*PublishPkarrRequest, return nil, err } if len(sigBytes) != ed25519.SignatureSize { - return nil, &server.InvalidSignatureError{} + return nil, &intutil.InvalidSignatureError{} } vBytes, err := encoding.DecodeString(p.V) if err != nil { @@ -79,7 +80,8 @@ func (p PublishDIDRequest) toPkarrRequest(suffix string) (*PublishPkarrRequest, } func (s *GatewayService) PublishDID(ctx context.Context, req PublishDIDRequest) error { - suffix, err := didint.DHT(req.DID).Suffix() + id := didint.DHT(req.DID) + suffix, err := id.Suffix() if err != nil { return err } @@ -88,7 +90,25 @@ func (s *GatewayService) PublishDID(ctx context.Context, req PublishDIDRequest) return err } + // TODO(gabe): retention proof support https://github.com/TBD54566975/did-dht-method/issues/73 + // unpack as a DID Document and store metadata + msg := new(dns.Msg) + if err = msg.Unpack(pkarrRequest.V); err != nil { + return errors.Wrap(err, "failed to unpack records") + } + doc, types, err := id.FromDNSPacket(msg) + if err != nil { + return errors.Wrap(err, "failed to parse DID document from DNS packet") + } + if err = s.db.WriteDID(storage.GatewayRecord{ + Document: *doc, + Types: types, + SequenceNumber: req.Seq, + RetentionProof: req.RetentionProof, + }); err != nil { + return errors.Wrap(err, "failed to write DID document to db") + } // publish to the network // TODO(gabe): check for conflicts with existing record sequence numbers https://github.com/TBD54566975/did-dht-method/issues/16 diff --git a/impl/pkg/service/pkarr.go b/impl/pkg/service/pkarr.go index c8770467..c3208640 100644 --- a/impl/pkg/service/pkarr.go +++ b/impl/pkg/service/pkarr.go @@ -15,8 +15,8 @@ import ( "github.com/TBD54566975/did-dht-method/config" dhtint "github.com/TBD54566975/did-dht-method/internal/dht" + util2 "github.com/TBD54566975/did-dht-method/internal/util" "github.com/TBD54566975/did-dht-method/pkg/dht" - "github.com/TBD54566975/did-dht-method/pkg/server" "github.com/TBD54566975/did-dht-method/pkg/storage" ) @@ -87,7 +87,7 @@ func (p PublishPkarrRequest) isValid() error { return err } if !bep44.Verify(p.K[:], nil, p.Seq, bv, p.Sig[:]) { - return &server.InvalidSignatureError{} + return &util2.InvalidSignatureError{} } return nil } @@ -229,6 +229,7 @@ func (s *PkarrService) addRecordToCache(id string, resp GetPkarrResponse) error } // TODO(gabe) make this more efficient. create a publish schedule based on each individual record, not all records +// TODO(gabe) consider a get before put to avoid writing outdated records https://github.com/TBD54566975/did-dht-method/issues/12 func (s *PkarrService) republish() { allRecords, err := s.db.ListRecords() if err != nil { diff --git a/impl/pkg/storage/gateway.go b/impl/pkg/storage/gateway.go new file mode 100644 index 00000000..71e67b6c --- /dev/null +++ b/impl/pkg/storage/gateway.go @@ -0,0 +1,166 @@ +package storage + +import ( + "strconv" + + "github.com/TBD54566975/ssi-sdk/did" + "github.com/goccy/go-json" + + didint "github.com/TBD54566975/did-dht-method/internal/did" +) + +const ( + gatewayNamespace = "dids" + typesNamespace = "types" +) + +type GatewayRecord struct { + // TODO(gabe) when historical document storage is supported, this should be a list of documents + Document did.Document `json:"document" validate:"required"` + Types []didint.TypeIndex `json:"types,omitempty"` + SequenceNumber int64 `json:"sequence_number" validate:"required"` + RetentionProof string `json:"retention_proof,omitempty"` +} + +type TypeRecord struct { + Types []string `json:"dids,omitempty"` +} + +// WriteDID writes a DID to the storage and adds it to the type index(es) it is associated with +func (s *Storage) WriteDID(record GatewayRecord) error { + // note current types for the DID to make sure we update the appropriate indexes + gotDID, err := s.ReadDID(record.Document.ID) + var currTypes []didint.TypeIndex + if err == nil && gotDID != nil { + currTypes = gotDID.Types + } + recordBytes, err := json.Marshal(record) + if err != nil { + return err + } + if err = s.Write(gatewayNamespace, record.Document.ID, recordBytes); err != nil { + return err + } + return s.UpdateTypeIndexesForDID(record.Document.ID, currTypes, record.Types) +} + +// ReadDID reads a DID from the storage by ID +func (s *Storage) ReadDID(id string) (*GatewayRecord, error) { + recordBytes, err := s.Read(gatewayNamespace, id) + if err != nil { + return nil, err + } + if len(recordBytes) == 0 { + return nil, nil + } + var record GatewayRecord + if err = json.Unmarshal(recordBytes, &record); err != nil { + return nil, err + } + return &record, nil +} + +// UpdateTypeIndexesForDID is an orchestration method that updates the type indexes for a DID +// It checks the existing type indexes for the DID and adds/removes the DID from the appropriate type indexes +func (s *Storage) UpdateTypeIndexesForDID(id string, currTypes, newTypes []didint.TypeIndex) error { + currTypeMap := make(map[didint.TypeIndex]bool) + for _, currType := range currTypes { + currTypeMap[currType] = true + } + newTypeMap := make(map[didint.TypeIndex]bool) + for _, newType := range newTypes { + newTypeMap[newType] = true + } + + // remove the DID from any type indexes it is no longer associated with + for _, currType := range currTypes { + if _, ok := newTypeMap[currType]; !ok { + if err := s.RemoveDIDFromTypeIndex(id, currType); err != nil { + return err + } + } + } + + // add the DID to any type indexes it is now associated with + for _, newType := range newTypes { + if _, ok := currTypeMap[newType]; !ok { + if err := s.AddDIDToTypeIndex(id, newType); err != nil { + return err + } + } + } + + return nil +} + +// AddDIDToTypeIndex adds a DID to a type index by appending it to the list of DIDs for that type index +// If the type index does not exist, it is created and the DID is added to it +func (s *Storage) AddDIDToTypeIndex(id string, typeIndex didint.TypeIndex) error { + t := strconv.Itoa(int(typeIndex)) + recordBytes, err := s.Read(typesNamespace, t) + if err != nil { + return err + } + if len(recordBytes) == 0 { + record := TypeRecord{Types: []string{id}} + recordBytes, err = json.Marshal(record) + if err != nil { + return err + } + return s.Write(typesNamespace, t, recordBytes) + } + var record TypeRecord + if err = json.Unmarshal(recordBytes, &record); err != nil { + return err + } + record.Types = append(record.Types, id) + recordBytes, err = json.Marshal(record) + if err != nil { + return err + } + return s.Write(typesNamespace, t, recordBytes) +} + +// RemoveDIDFromTypeIndex removes a DID from a type index by removing it from the list of DIDs for that type index +func (s *Storage) RemoveDIDFromTypeIndex(id string, typeIndex didint.TypeIndex) error { + t := strconv.Itoa(int(typeIndex)) + recordBytes, err := s.Read(typesNamespace, t) + if err != nil { + return err + } + if len(recordBytes) == 0 { + return nil + } + var record TypeRecord + if err = json.Unmarshal(recordBytes, &record); err != nil { + return err + } + for i, didID := range record.Types { + if didID == id { + record.Types = append(record.Types[:i], record.Types[i+1:]...) + break + } + } + recordBytes, err = json.Marshal(record) + if err != nil { + return err + } + return s.Write(typesNamespace, t, recordBytes) +} + +// ListDIDsForType returns a list of DIDs for a given type index +func (s *Storage) ListDIDsForType(typeIndex didint.TypeIndex) ([]string, error) { + t := strconv.Itoa(int(typeIndex)) + recordBytes, err := s.Read(typesNamespace, t) + if err != nil { + return nil, err + } + if len(recordBytes) == 0 { + return nil, nil + } + var record TypeRecord + if err = json.Unmarshal(recordBytes, &record); err != nil { + return nil, err + } + return record.Types, nil +} diff --git a/impl/pkg/storage/gateway_test.go b/impl/pkg/storage/gateway_test.go new file mode 100644 index 00000000..30b044fe --- /dev/null +++ b/impl/pkg/storage/gateway_test.go @@ -0,0 +1,153 @@ +package storage + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/TBD54566975/did-dht-method/internal/did" +) + +func TestGatewayStorage(t *testing.T) { + db := setupBoltDB(t) + defer db.Close() + require.NotEmpty(t, db) + + t.Run("Read and Write DID", func(t *testing.T) { + // create a did doc to store + _, doc, err := did.GenerateDIDDHT(did.CreateDIDDHTOpts{}) + require.NoError(t, err) + require.NotEmpty(t, doc) + + // create record + record := GatewayRecord{ + Document: *doc, + Types: []did.TypeIndex{1, 2, 3}, + SequenceNumber: 1, + } + + err = db.WriteDID(record) + assert.NoError(t, err) + + // get it back + readRecord, err := db.ReadDID(record.Document.ID) + assert.NoError(t, err) + assert.Equal(t, record, *readRecord) + }) + + t.Run("Update a DID and its type indexes", func(t *testing.T) { + // create a did doc to store + _, doc, err := did.GenerateDIDDHT(did.CreateDIDDHTOpts{}) + require.NoError(t, err) + require.NotEmpty(t, doc) + + // create record + record := GatewayRecord{ + Document: *doc, + Types: []did.TypeIndex{1, 2, 3}, + SequenceNumber: 1, + } + + err = db.WriteDID(record) + assert.NoError(t, err) + + // get types + types, err := db.ListDIDsForType(1) + assert.NoError(t, err) + assert.Equal(t, []string{record.Document.ID}, types) + + types, err = db.ListDIDsForType(2) + assert.NoError(t, err) + assert.Equal(t, []string{record.Document.ID}, types) + + types, err = db.ListDIDsForType(3) + assert.NoError(t, err) + assert.Equal(t, []string{record.Document.ID}, types) + + // update record + record.Types = []did.TypeIndex{4, 5, 6} + record.SequenceNumber = 2 + err = db.WriteDID(record) + assert.NoError(t, err) + + // get it back + readRecord, err := db.ReadDID(record.Document.ID) + assert.NoError(t, err) + assert.Equal(t, record, *readRecord) + + // get types + types, err = db.ListDIDsForType(1) + assert.NoError(t, err) + assert.Empty(t, types) + + types, err = db.ListDIDsForType(2) + assert.NoError(t, err) + assert.Empty(t, types) + + types, err = db.ListDIDsForType(3) + assert.NoError(t, err) + assert.Empty(t, types) + + types, err = db.ListDIDsForType(4) + assert.NoError(t, err) + assert.Equal(t, []string{record.Document.ID}, types) + + types, err = db.ListDIDsForType(5) + assert.NoError(t, err) + assert.Equal(t, []string{record.Document.ID}, types) + + types, err = db.ListDIDsForType(6) + assert.NoError(t, err) + assert.Equal(t, []string{record.Document.ID}, types) + }) + + t.Run("Multiple DIDs with Types", func(t *testing.T) { + // create a did doc to store + _, doc, err := did.GenerateDIDDHT(did.CreateDIDDHTOpts{}) + require.NoError(t, err) + require.NotEmpty(t, doc) + + // create record + record := GatewayRecord{ + Document: *doc, + Types: []did.TypeIndex{1, 2, 3}, + SequenceNumber: 1, + } + + err = db.WriteDID(record) + assert.NoError(t, err) + + // create a did doc to store + _, doc2, err := did.GenerateDIDDHT(did.CreateDIDDHTOpts{}) + require.NoError(t, err) + require.NotEmpty(t, doc) + + // create record + record2 := GatewayRecord{ + Document: *doc2, + Types: []did.TypeIndex{2, 3, 4}, + SequenceNumber: 1, + } + + err = db.WriteDID(record2) + assert.NoError(t, err) + + // get types + types, err := db.ListDIDsForType(1) + assert.NoError(t, err) + assert.Equal(t, []string{record.Document.ID}, types) + + types, err = db.ListDIDsForType(2) + assert.NoError(t, err) + assert.Equal(t, []string{record.Document.ID, record2.Document.ID}, types) + + types, err = db.ListDIDsForType(3) + assert.NoError(t, err) + assert.Equal(t, []string{record.Document.ID, record2.Document.ID}, types) + + types, err = db.ListDIDsForType(4) + assert.NoError(t, err) + assert.Equal(t, []string{record2.Document.ID}, types) + }) +} From fc73c24cece7867d5132707823fd12ec017dbace Mon Sep 17 00:00:00 2001 From: gabe Date: Mon, 18 Dec 2023 16:34:29 -0800 Subject: [PATCH 08/12] basic api without retention proofs or historical resolution done --- impl/internal/did/did.go | 1 + impl/pkg/server/gateway.go | 27 +++++---- impl/pkg/service/gateway.go | 113 +++++++++++++++++++++++++++++++----- impl/pkg/service/pkarr.go | 4 +- spec/api.yaml | 6 ++ spec/spec.md | 1 + 6 files changed, 124 insertions(+), 28 deletions(-) diff --git a/impl/internal/did/did.go b/impl/internal/did/did.go index 0fc9e5cb..a834fae6 100644 --- a/impl/internal/did/did.go +++ b/impl/internal/did/did.go @@ -26,6 +26,7 @@ const ( DHTMethod did.Method = "dht" JSONWebKeyType cryptosuite.LDKeyType = "JsonWebKey" + Discoverable TypeIndex = 0 Organization TypeIndex = 1 GovernmentOrganization TypeIndex = 2 Corporation TypeIndex = 3 diff --git a/impl/pkg/server/gateway.go b/impl/pkg/server/gateway.go index e30defa1..8a5daee5 100644 --- a/impl/pkg/server/gateway.go +++ b/impl/pkg/server/gateway.go @@ -2,11 +2,13 @@ package server import ( "net/http" + "strconv" "github.com/TBD54566975/ssi-sdk/did" "github.com/gin-gonic/gin" "github.com/pkg/errors" + didint "github.com/TBD54566975/did-dht-method/internal/did" "github.com/TBD54566975/did-dht-method/internal/util" "github.com/TBD54566975/did-dht-method/pkg/service" ) @@ -86,9 +88,9 @@ func (r *GatewayRouter) PublishDID(c *gin.Context) { // GetDIDResponse represents a response containing a DID document, types, and sequence numbers. type GetDIDResponse struct { - DID did.Document `json:"did" validate:"required"` - Types []int `json:"types,omitempty"` - SequenceNumbers []int `json:"sequence_numbers,omitempty"` + DID did.Document `json:"did" validate:"required"` + Types []didint.TypeIndex `json:"types,omitempty"` + SequenceNumbers []int `json:"sequence_numbers,omitempty"` } // GetDID godoc @@ -138,18 +140,13 @@ type GetTypesResponse struct { // @Failure 404 {string} string "Type indexing is not supported by this gateway" // @Router /dids/types [get] func (r *GatewayRouter) GetTypes(c *gin.Context) { - resp, err := r.service.GetTypes() - if err != nil { - LoggingRespondErrWithMsg(c, err, "failed to get types", http.StatusInternalServerError) - return - } - - if resp == nil { + resp := r.service.GetTypes() + if len(resp.Types) == 0 { LoggingRespondErrMsg(c, "types not supported", http.StatusNotFound) return } - Respond(c, GetTypesResponse(*resp), http.StatusOK) + Respond(c, GetTypesResponse{Types: resp.Types}, http.StatusOK) } // GetDIDsForTypeResponse represents a response containing a list of DIDs for a given type. @@ -163,6 +160,7 @@ type GetDIDsForTypeResponse struct { // @Tags DID // @Accept json // @Success 200 {object} GetDIDsForTypeResponse +// @Failure 400 {string} string "Invalid request" // @Failure 404 {string} string "Type not found" // @Failure 500 {string} string "Internal server error" // @Router /dids/types/{id} [get] @@ -172,8 +170,13 @@ func (r *GatewayRouter) GetDIDsForType(c *gin.Context) { LoggingRespondErrMsg(c, "missing id param", http.StatusBadRequest) return } + typeIndex, err := strconv.Atoi(*id) + if err != nil { + LoggingRespondErrWithMsg(c, err, "failed to convert type index to int", http.StatusBadRequest) + return + } - resp, err := r.service.GetDIDsForType(service.GetDIDsForTypeRequest{Type: *id}) + resp, err := r.service.ListDIDsForType(service.ListDIDsForTypeRequest{Type: didint.TypeIndex(typeIndex)}) if err != nil { if errors.Is(err, &util.TypeNotFoundError{}) { LoggingRespondErrMsg(c, "type not found", http.StatusNotFound) diff --git a/impl/pkg/service/gateway.go b/impl/pkg/service/gateway.go index f27d5ddd..c9764b98 100644 --- a/impl/pkg/service/gateway.go +++ b/impl/pkg/service/gateway.go @@ -101,6 +101,15 @@ func (s *GatewayService) PublishDID(ctx context.Context, req PublishDIDRequest) if err != nil { return errors.Wrap(err, "failed to parse DID document from DNS packet") } + + // check to see if the DID already exists with a higher sequence number + gotDID, err := s.db.ReadDID(req.DID) + if err == nil && gotDID != nil { + if gotDID.SequenceNumber > req.Seq { + return &intutil.HigherSequenceNumberError{} + } + } + if err = s.db.WriteDID(storage.GatewayRecord{ Document: *doc, Types: types, @@ -120,13 +129,26 @@ func (s *GatewayService) PublishDID(ctx context.Context, req PublishDIDRequest) } type GetDIDResponse struct { - DID did.Document `json:"did" validate:"required"` - Types []int `json:"types,omitempty"` - SequenceNumbers []int `json:"sequence_numbers,omitempty"` + DID did.Document `json:"did" validate:"required"` + Types []didint.TypeIndex `json:"types,omitempty"` + SequenceNumbers []int `json:"sequence_numbers,omitempty"` } func (s *GatewayService) GetDID(id string) (*GetDIDResponse, error) { - return nil, nil + gotDID, err := s.db.ReadDID(id) + if err != nil { + return nil, err + } + + if gotDID == nil { + return nil, nil + } + + return &GetDIDResponse{ + DID: gotDID.Document, + Types: gotDID.Types, + SequenceNumbers: []int{int(gotDID.SequenceNumber)}, + }, nil } type GetTypesResponse struct { @@ -134,24 +156,39 @@ type GetTypesResponse struct { } type TypeMapping struct { - TypeIndex int `json:"type_index" validate:"required"` - Type string `json:"type" validate:"required"` + TypeIndex didint.TypeIndex `json:"type_index" validate:"required"` + Type string `json:"type" validate:"required"` } -func (s *GatewayService) GetTypes() (*GetTypesResponse, error) { - return nil, nil +// GetTypes returns a list of supported types and their names. +// As defined by the spec's registry https://did-dht.com/registry/index.html#indexed-types +func (s *GatewayService) GetTypes() GetTypesResponse { + return GetTypesResponse{ + Types: knownTypes, + } } -type GetDIDsForTypeRequest struct { - Type string `json:"type" validate:"required"` +type ListDIDsForTypeRequest struct { + Type didint.TypeIndex `json:"type" validate:"required"` } -type GetDIDsForTypeResponse struct { +type ListDIDsForTypeResponse struct { DIDs []string `json:"dids,omitempty"` } -func (s *GatewayService) GetDIDsForType(req GetDIDsForTypeRequest) (*GetDIDsForTypeResponse, error) { - return nil, nil +// ListDIDsForType returns a list of DIDs for a given type. +func (s *GatewayService) ListDIDsForType(req ListDIDsForTypeRequest) (*ListDIDsForTypeResponse, error) { + if !isKnownType(req.Type) { + return nil, &intutil.TypeNotFoundError{} + } + dids, err := s.db.ListDIDsForType(req.Type) + if err != nil { + return nil, err + } + if len(dids) == 0 { + return nil, nil + } + return &ListDIDsForTypeResponse{DIDs: dids}, nil } type GetDifficultyResponse struct { @@ -159,6 +196,54 @@ type GetDifficultyResponse struct { Difficulty int `json:"difficulty" validate:"required"` } +// GetDifficulty returns the current difficulty for the gateway's retention proof feature. +// TODO(gabe): retention proof support https://github.com/TBD54566975/did-dht-method/issues/73 func (s *GatewayService) GetDifficulty() (*GetDifficultyResponse, error) { - return nil, nil + return nil, errors.New("not yet implemented") } + +func isKnownType(t didint.TypeIndex) bool { + for _, knownType := range knownTypes { + if knownType.TypeIndex == t { + return true + } + } + return false +} + +var ( + knownTypes = []TypeMapping{ + { + TypeIndex: didint.Discoverable, + Type: "Discoverable", + }, + { + TypeIndex: didint.Organization, + Type: "Organization", + }, + { + TypeIndex: didint.GovernmentOrganization, + Type: "Government Organization", + }, + { + TypeIndex: didint.Corporation, + Type: "Corporation", + }, + { + TypeIndex: didint.LocalBusiness, + Type: "Local Business", + }, + { + TypeIndex: didint.SoftwarePackage, + Type: "Software Package", + }, + { + TypeIndex: didint.WebApplication, + Type: "Web Application", + }, + { + TypeIndex: didint.FinancialInstitution, + Type: "Financial Institution", + }, + } +) diff --git a/impl/pkg/service/pkarr.go b/impl/pkg/service/pkarr.go index c3208640..6dcb0aab 100644 --- a/impl/pkg/service/pkarr.go +++ b/impl/pkg/service/pkarr.go @@ -15,7 +15,7 @@ import ( "github.com/TBD54566975/did-dht-method/config" dhtint "github.com/TBD54566975/did-dht-method/internal/dht" - util2 "github.com/TBD54566975/did-dht-method/internal/util" + intutil "github.com/TBD54566975/did-dht-method/internal/util" "github.com/TBD54566975/did-dht-method/pkg/dht" "github.com/TBD54566975/did-dht-method/pkg/storage" ) @@ -87,7 +87,7 @@ func (p PublishPkarrRequest) isValid() error { return err } if !bep44.Verify(p.K[:], nil, p.Seq, bv, p.Sig[:]) { - return &util2.InvalidSignatureError{} + return &intutil.InvalidSignatureError{} } return nil } diff --git a/spec/api.yaml b/spec/api.yaml index aff08515..f16fc41b 100644 --- a/spec/api.yaml +++ b/spec/api.yaml @@ -237,6 +237,12 @@ paths: type: array items: type: string + "400": + description: Invalid request. + content: + application/json: + schema: + type: string "404": description: Type not found. content: diff --git a/spec/spec.md b/spec/spec.md index d03485ae..506337a3 100644 --- a/spec/spec.md +++ b/spec/spec.md @@ -649,6 +649,7 @@ stop republishing the DHT. If the DNS Packets contains a `_typ._did.` record, th - **Returns:** - `200` - Success. - **array** - An array of DID Identifiers matching the associated type. + - `400` - Invalid request. - `404` - Type not found. ```json From c06bbe57ff6579d72f29522e16f7d6032ef082c8 Mon Sep 17 00:00:00 2001 From: gabe Date: Mon, 18 Dec 2023 16:36:16 -0800 Subject: [PATCH 09/12] update spec --- impl/docs/swagger.yaml | 311 +++++++++++++++++++++++++++++++++++++ impl/pkg/server/gateway.go | 90 +++++------ 2 files changed, 356 insertions(+), 45 deletions(-) diff --git a/impl/docs/swagger.yaml b/impl/docs/swagger.yaml index 13c0f29c..419169e3 100644 --- a/impl/docs/swagger.yaml +++ b/impl/docs/swagger.yaml @@ -1,10 +1,193 @@ definitions: + did.Document: + properties: + '@context': {} + alsoKnownAs: + type: string + assertionMethod: + items: {} + type: array + authentication: + items: {} + type: array + capabilityDelegation: + items: {} + type: array + capabilityInvocation: + items: {} + type: array + controller: + type: string + id: + description: |- + As per https://www.w3.org/TR/did-core/#did-subject intermediate representations of DID Documents do not + require an ID property. The provided test vectors demonstrate IRs. As such, the property is optional. + type: string + keyAgreement: + items: {} + type: array + service: + items: + $ref: '#/definitions/did.Service' + type: array + verificationMethod: + items: + $ref: '#/definitions/github_com_TBD54566975_ssi-sdk_did.VerificationMethod' + type: array + type: object + did.Service: + properties: + accept: + items: + type: string + type: array + id: + type: string + routingKeys: + items: + type: string + type: array + serviceEndpoint: + description: |- + A string, map, or set composed of one or more strings and/or maps + All string values must be valid URIs + type: + type: string + required: + - id + - serviceEndpoint + - type + type: object + github_com_TBD54566975_did-dht-method_internal_did.TypeIndex: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + type: integer + x-enum-varnames: + - Discoverable + - Organization + - GovernmentOrganization + - Corporation + - LocalBusiness + - SoftwarePackage + - WebApplication + - FinancialInstitution + github_com_TBD54566975_did-dht-method_pkg_service.TypeMapping: + properties: + type: + type: string + type_index: + $ref: '#/definitions/github_com_TBD54566975_did-dht-method_internal_did.TypeIndex' + required: + - type + - type_index + type: object + github_com_TBD54566975_ssi-sdk_did.VerificationMethod: + properties: + blockchainAccountId: + description: for PKH DIDs - https://github.com/w3c-ccg/did-pkh/blob/90b28ad3c18d63822a8aab3c752302aa64fc9382/did-pkh-method-draft.md + type: string + controller: + type: string + id: + type: string + publicKeyBase58: + type: string + publicKeyJwk: + allOf: + - $ref: '#/definitions/jwx.PublicKeyJWK' + description: must conform to https://datatracker.ietf.org/doc/html/rfc7517 + publicKeyMultibase: + description: https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03 + type: string + type: + type: string + required: + - controller + - id + - type + type: object + jwx.PublicKeyJWK: + properties: + alg: + type: string + crv: + type: string + e: + type: string + key_ops: + type: string + kid: + type: string + kty: + type: string + "n": + type: string + use: + type: string + x: + type: string + "y": + type: string + required: + - kty + type: object + pkg_server.GetDIDResponse: + properties: + did: + $ref: '#/definitions/did.Document' + sequence_numbers: + items: + type: integer + type: array + types: + items: + $ref: '#/definitions/github_com_TBD54566975_did-dht-method_internal_did.TypeIndex' + type: array + required: + - did + type: object + pkg_server.GetDIDsForTypeResponse: + properties: + dids: + items: + type: string + type: array + type: object pkg_server.GetHealthCheckResponse: properties: status: description: Status is always equal to `OK`. type: string type: object + pkg_server.GetTypesResponse: + properties: + types: + items: + $ref: '#/definitions/github_com_TBD54566975_did-dht-method_pkg_service.TypeMapping' + type: array + type: object + pkg_server.PublishDIDRequest: + properties: + retention_proof: + type: string + seq: + type: integer + sig: + type: string + v: + type: string + required: + - seq + - sig + - v + type: object externalDocs: description: OpenAPI url: https://swagger.io/resources/open-api/ @@ -87,6 +270,134 @@ paths: summary: PutRecord a Pkarr record into the DHT tags: - Pkarr + /dids/{id}: + get: + consumes: + - application/json + description: Get a DID document + parameters: + - description: ID of the record to get + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/pkg_server.GetDIDResponse' + "400": + description: Invalid request + schema: + type: string + "404": + description: DID not found + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Get a DID document + tags: + - DID + put: + consumes: + - application/json + description: Publish a DID document to the DHT + parameters: + - description: ID of the record to get + in: path + name: id + required: true + type: string + responses: + "202": + description: Accepted + schema: + $ref: '#/definitions/pkg_server.PublishDIDRequest' + "400": + description: Invalid request body + schema: + type: string + "401": + description: Invalid signature + schema: + type: string + "409": + description: DID already exists with a higher sequence number + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Publish a DID document + tags: + - DID + /dids/types: + get: + consumes: + - application/json + description: Get a list of supported types + responses: + "200": + description: OK + schema: + $ref: '#/definitions/pkg_server.GetTypesResponse' + "404": + description: Type indexing is not supported by this gateway + schema: + type: string + summary: Get a list of supported types + tags: + - DID + /dids/types/{id}: + get: + consumes: + - application/json + description: Get a list of DIDs for a given type + responses: + "200": + description: OK + schema: + $ref: '#/definitions/pkg_server.GetDIDsForTypeResponse' + "400": + description: Invalid request + schema: + type: string + "404": + description: Type not found + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Get a list of DIDs for a given type + tags: + - DID + /difficulty: + get: + consumes: + - application/json + description: Get the current difficulty for the gateway's retention proof feature + responses: + "200": + description: OK + schema: + type: integer + "404": + description: Retention proofs are not supported by this gateway + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Get the current difficulty for the gateway's retention proof feature + tags: + - DID /health: get: consumes: diff --git a/impl/pkg/server/gateway.go b/impl/pkg/server/gateway.go index 8a5daee5..9b9dbdc9 100644 --- a/impl/pkg/server/gateway.go +++ b/impl/pkg/server/gateway.go @@ -40,17 +40,17 @@ func (p PublishDIDRequest) toServiceRequest(did string) service.PublishDIDReques } // PublishDID godoc -// @Summary Publish a DID document -// @Description Publish a DID document to the DHT -// @Tags DID -// @Accept json -// @Param id path string true "ID of the record to get" -// @Success 202 {object} PublishDIDRequest -// @Failure 400 {string} string "Invalid request body" -// @Failure 401 {string} string "Invalid signature" -// @Failure 409 {string} string "DID already exists with a higher sequence number" -// @Failure 500 {string} string "Internal server error" -// @Router /dids/{id} [put] +// @Summary Publish a DID document +// @Description Publish a DID document to the DHT +// @Tags DID +// @Accept json +// @Param id path string true "ID of the record to get" +// @Success 202 {object} PublishDIDRequest +// @Failure 400 {string} string "Invalid request body" +// @Failure 401 {string} string "Invalid signature" +// @Failure 409 {string} string "DID already exists with a higher sequence number" +// @Failure 500 {string} string "Internal server error" +// @Router /dids/{id} [put] // TODO(gabe) support historical document storage https://github.com/TBD54566975/did-dht-method/issues/16 func (r *GatewayRouter) PublishDID(c *gin.Context) { id := GetParam(c, IDParam) @@ -94,16 +94,16 @@ type GetDIDResponse struct { } // GetDID godoc -// @Summary Get a DID document -// @Description Get a DID document -// @Tags DID -// @Accept json -// @Param id path string true "ID of the record to get" -// @Success 200 {object} GetDIDResponse -// @Failure 400 {string} string "Invalid request" -// @Failure 404 {string} string "DID not found" -// @Failure 500 {string} string "Internal server error" -// @Router /dids/{id} [get] +// @Summary Get a DID document +// @Description Get a DID document +// @Tags DID +// @Accept json +// @Param id path string true "ID of the record to get" +// @Success 200 {object} GetDIDResponse +// @Failure 400 {string} string "Invalid request" +// @Failure 404 {string} string "DID not found" +// @Failure 500 {string} string "Internal server error" +// @Router /dids/{id} [get] // TODO(gabe) support historical queries https://github.com/TBD54566975/did-dht-method/issues/16 func (r *GatewayRouter) GetDID(c *gin.Context) { id := GetParam(c, IDParam) @@ -132,13 +132,13 @@ type GetTypesResponse struct { } // GetTypes godoc -// @Summary Get a list of supported types -// @Description Get a list of supported types -// @Tags DID -// @Accept json -// @Success 200 {object} GetTypesResponse -// @Failure 404 {string} string "Type indexing is not supported by this gateway" -// @Router /dids/types [get] +// @Summary Get a list of supported types +// @Description Get a list of supported types +// @Tags DID +// @Accept json +// @Success 200 {object} GetTypesResponse +// @Failure 404 {string} string "Type indexing is not supported by this gateway" +// @Router /dids/types [get] func (r *GatewayRouter) GetTypes(c *gin.Context) { resp := r.service.GetTypes() if len(resp.Types) == 0 { @@ -155,15 +155,15 @@ type GetDIDsForTypeResponse struct { } // GetDIDsForType godoc -// @Summary Get a list of DIDs for a given type -// @Description Get a list of DIDs for a given type -// @Tags DID -// @Accept json -// @Success 200 {object} GetDIDsForTypeResponse -// @Failure 400 {string} string "Invalid request" -// @Failure 404 {string} string "Type not found" -// @Failure 500 {string} string "Internal server error" -// @Router /dids/types/{id} [get] +// @Summary Get a list of DIDs for a given type +// @Description Get a list of DIDs for a given type +// @Tags DID +// @Accept json +// @Success 200 {object} GetDIDsForTypeResponse +// @Failure 400 {string} string "Invalid request" +// @Failure 404 {string} string "Type not found" +// @Failure 500 {string} string "Internal server error" +// @Router /dids/types/{id} [get] func (r *GatewayRouter) GetDIDsForType(c *gin.Context) { id := GetParam(c, IDParam) if id == nil || *id == "" { @@ -197,14 +197,14 @@ type GetDifficultyResponse struct { } // GetDifficulty godoc -// @Summary Get the current difficulty for the gateway's retention proof feature -// @Description Get the current difficulty for the gateway's retention proof feature -// @Tags DID -// @Accept json -// @Success 200 {object} int -// @Failure 404 {string} string "Retention proofs are not supported by this gateway" -// @Failure 500 {string} string "Internal server error" -// @Router /difficulty [get] +// @Summary Get the current difficulty for the gateway's retention proof feature +// @Description Get the current difficulty for the gateway's retention proof feature +// @Tags DID +// @Accept json +// @Success 200 {object} int +// @Failure 404 {string} string "Retention proofs are not supported by this gateway" +// @Failure 500 {string} string "Internal server error" +// @Router /difficulty [get] func (r *GatewayRouter) GetDifficulty(c *gin.Context) { resp, err := r.service.GetDifficulty() if err != nil { From da72a80171d601c451e0f5449fe57b64298c44ac Mon Sep 17 00:00:00 2001 From: gabe Date: Tue, 19 Dec 2023 11:01:26 -0800 Subject: [PATCH 10/12] update status codes --- impl/docs/swagger.yaml | 18 +++++++++++++----- impl/pkg/server/gateway.go | 17 +++++++++++++---- spec/api.yaml | 16 ++++++++++++++-- spec/spec.md | 4 +++- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/impl/docs/swagger.yaml b/impl/docs/swagger.yaml index 419169e3..1eec7fc6 100644 --- a/impl/docs/swagger.yaml +++ b/impl/docs/swagger.yaml @@ -298,6 +298,10 @@ paths: description: Internal server error schema: type: string + "501": + description: Historical resolution not supported by this gateway + schema: + type: string summary: Get a DID document tags: - DID @@ -345,7 +349,7 @@ paths: description: OK schema: $ref: '#/definitions/pkg_server.GetTypesResponse' - "404": + "501": description: Type indexing is not supported by this gateway schema: type: string @@ -374,6 +378,10 @@ paths: description: Internal server error schema: type: string + "501": + description: Type indexing is not supported by this gateway + schema: + type: string summary: Get a list of DIDs for a given type tags: - DID @@ -387,14 +395,14 @@ paths: description: OK schema: type: integer - "404": - description: Retention proofs are not supported by this gateway - schema: - type: string "500": description: Internal server error schema: type: string + "501": + description: Retention proofs are not supported by this gateway + schema: + type: string summary: Get the current difficulty for the gateway's retention proof feature tags: - DID diff --git a/impl/pkg/server/gateway.go b/impl/pkg/server/gateway.go index 9b9dbdc9..a408ed2e 100644 --- a/impl/pkg/server/gateway.go +++ b/impl/pkg/server/gateway.go @@ -40,6 +40,7 @@ func (p PublishDIDRequest) toServiceRequest(did string) service.PublishDIDReques } // PublishDID godoc +// // @Summary Publish a DID document // @Description Publish a DID document to the DHT // @Tags DID @@ -51,6 +52,7 @@ func (p PublishDIDRequest) toServiceRequest(did string) service.PublishDIDReques // @Failure 409 {string} string "DID already exists with a higher sequence number" // @Failure 500 {string} string "Internal server error" // @Router /dids/{id} [put] +// // TODO(gabe) support historical document storage https://github.com/TBD54566975/did-dht-method/issues/16 func (r *GatewayRouter) PublishDID(c *gin.Context) { id := GetParam(c, IDParam) @@ -94,6 +96,7 @@ type GetDIDResponse struct { } // GetDID godoc +// // @Summary Get a DID document // @Description Get a DID document // @Tags DID @@ -103,7 +106,9 @@ type GetDIDResponse struct { // @Failure 400 {string} string "Invalid request" // @Failure 404 {string} string "DID not found" // @Failure 500 {string} string "Internal server error" +// @Failure 501 {string} string "Historical resolution not supported by this gateway" // @Router /dids/{id} [get] +// // TODO(gabe) support historical queries https://github.com/TBD54566975/did-dht-method/issues/16 func (r *GatewayRouter) GetDID(c *gin.Context) { id := GetParam(c, IDParam) @@ -132,17 +137,18 @@ type GetTypesResponse struct { } // GetTypes godoc +// // @Summary Get a list of supported types // @Description Get a list of supported types // @Tags DID // @Accept json // @Success 200 {object} GetTypesResponse -// @Failure 404 {string} string "Type indexing is not supported by this gateway" +// @Failure 501 {string} string "Type indexing is not supported by this gateway" // @Router /dids/types [get] func (r *GatewayRouter) GetTypes(c *gin.Context) { resp := r.service.GetTypes() if len(resp.Types) == 0 { - LoggingRespondErrMsg(c, "types not supported", http.StatusNotFound) + LoggingRespondErrMsg(c, "types not supported", http.StatusNotImplemented) return } @@ -155,6 +161,7 @@ type GetDIDsForTypeResponse struct { } // GetDIDsForType godoc +// // @Summary Get a list of DIDs for a given type // @Description Get a list of DIDs for a given type // @Tags DID @@ -163,6 +170,7 @@ type GetDIDsForTypeResponse struct { // @Failure 400 {string} string "Invalid request" // @Failure 404 {string} string "Type not found" // @Failure 500 {string} string "Internal server error" +// @Failure 501 {string} string "Type indexing is not supported by this gateway" // @Router /dids/types/{id} [get] func (r *GatewayRouter) GetDIDsForType(c *gin.Context) { id := GetParam(c, IDParam) @@ -197,13 +205,14 @@ type GetDifficultyResponse struct { } // GetDifficulty godoc +// // @Summary Get the current difficulty for the gateway's retention proof feature // @Description Get the current difficulty for the gateway's retention proof feature // @Tags DID // @Accept json // @Success 200 {object} int -// @Failure 404 {string} string "Retention proofs are not supported by this gateway" // @Failure 500 {string} string "Internal server error" +// @Failure 501 {string} string "Retention proofs are not supported by this gateway" // @Router /difficulty [get] func (r *GatewayRouter) GetDifficulty(c *gin.Context) { resp, err := r.service.GetDifficulty() @@ -213,7 +222,7 @@ func (r *GatewayRouter) GetDifficulty(c *gin.Context) { } if resp == nil { - LoggingRespondErrMsg(c, "retention proofs not supported", http.StatusNotFound) + LoggingRespondErrMsg(c, "retention proofs not supported", http.StatusNotImplemented) return } diff --git a/spec/api.yaml b/spec/api.yaml index f16fc41b..003118b8 100644 --- a/spec/api.yaml +++ b/spec/api.yaml @@ -178,6 +178,12 @@ paths: application/json: schema: type: string + "501": + description: Historical resolution not supported by this gateway. + content: + application/json: + schema: + type: string /dids/types: get: tags: @@ -199,7 +205,7 @@ paths: description: type: string required: [ type, description ] - "404": + "501": description: Type indexing not supported by this gateway. content: application/json: @@ -249,6 +255,12 @@ paths: application/json: schema: type: string + "501": + description: Type indexing not supported by this gateway. + content: + application/json: + schema: + type: string /difficulty: get: tags: @@ -268,7 +280,7 @@ paths: difficulty: type: integer required: [ hash, difficulty ] - "404": + "501": description: Retention proofs not supported by this gateway. content: application/json: diff --git a/spec/spec.md b/spec/spec.md index 506337a3..e3a901c8 100644 --- a/spec/spec.md +++ b/spec/spec.md @@ -499,7 +499,7 @@ Difficulty is exposed as an **OPTIONAL** endpoint based on support of [retention - `200` - Success. - `hash` - **string** - The current hash. - `difficulty` - **integer** - The current difficulty. - - `404` - Retention proofs not supported by this gateway. + - `501` - Retention proofs not supported by this gateway. ```json { @@ -605,6 +605,7 @@ historical state for a given [[ref:DID]]. The following API can be used with spe - `types` - **array** - An array of [type strings](#type-indexing) for the DID. - `400` - Invalid request. - `404` - DID not found for the given sequence number. + - `501` - Historical resolution not supported by this gateway. #### Deactivating a DID @@ -651,6 +652,7 @@ stop republishing the DHT. If the DNS Packets contains a `_typ._did.` record, th - **array** - An array of DID Identifiers matching the associated type. - `400` - Invalid request. - `404` - Type not found. + - `501` - Types not supported by this gateway. ```json [ From 7e72e5432844e2945cf0544ebf2b069cf4ce1691 Mon Sep 17 00:00:00 2001 From: gabe Date: Tue, 19 Dec 2023 13:40:18 -0800 Subject: [PATCH 11/12] fix tests --- impl/pkg/service/pkarr_test.go | 2 +- impl/pkg/storage/gateway_test.go | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/impl/pkg/service/pkarr_test.go b/impl/pkg/service/pkarr_test.go index 21f31b91..9b4372a2 100644 --- a/impl/pkg/service/pkarr_test.go +++ b/impl/pkg/service/pkarr_test.go @@ -63,7 +63,7 @@ func TestPKARRService(t *testing.T) { Seq: putMsg.Seq, }) assert.Error(t, err) - assert.Contains(t, err.Error(), "signature is invalid") + assert.Contains(t, err.Error(), "invalid signature") }) t.Run("test put and get record", func(t *testing.T) { diff --git a/impl/pkg/storage/gateway_test.go b/impl/pkg/storage/gateway_test.go index 30b044fe..d0d411e8 100644 --- a/impl/pkg/storage/gateway_test.go +++ b/impl/pkg/storage/gateway_test.go @@ -10,11 +10,11 @@ import ( ) func TestGatewayStorage(t *testing.T) { - db := setupBoltDB(t) - defer db.Close() - require.NotEmpty(t, db) - t.Run("Read and Write DID", func(t *testing.T) { + db := setupBoltDB(t) + defer db.Close() + require.NotEmpty(t, db) + // create a did doc to store _, doc, err := did.GenerateDIDDHT(did.CreateDIDDHTOpts{}) require.NoError(t, err) @@ -37,6 +37,10 @@ func TestGatewayStorage(t *testing.T) { }) t.Run("Update a DID and its type indexes", func(t *testing.T) { + db := setupBoltDB(t) + defer db.Close() + require.NotEmpty(t, db) + // create a did doc to store _, doc, err := did.GenerateDIDDHT(did.CreateDIDDHTOpts{}) require.NoError(t, err) @@ -103,6 +107,10 @@ func TestGatewayStorage(t *testing.T) { }) t.Run("Multiple DIDs with Types", func(t *testing.T) { + db := setupBoltDB(t) + defer db.Close() + require.NotEmpty(t, db) + // create a did doc to store _, doc, err := did.GenerateDIDDHT(did.CreateDIDDHTOpts{}) require.NoError(t, err) From b56db30f8b851bd1e8f3417b24186eaa5bfa9a5d Mon Sep 17 00:00:00 2001 From: gabe Date: Thu, 4 Jan 2024 11:21:45 -0800 Subject: [PATCH 12/12] tmp --- impl/internal/did/did.go | 19 ++++++++++++++++++ impl/internal/did/did_test.go | 14 +++++++++++++ impl/pkg/service/gateway_test.go | 34 ++++++++++++++++++++++++++++++++ impl/pkg/service/pkarr_test.go | 6 +++--- 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 impl/pkg/service/gateway_test.go diff --git a/impl/internal/did/did.go b/impl/internal/did/did.go index a834fae6..8dcbc4e1 100644 --- a/impl/internal/did/did.go +++ b/impl/internal/did/did.go @@ -4,6 +4,7 @@ import ( "crypto/ed25519" "encoding/base64" "fmt" + "math" "strconv" "strings" @@ -74,6 +75,24 @@ type VerificationMethod struct { Purposes []did.PublicKeyPurpose `json:"purposes"` } +func GenerateVanityDIDDHT(prefix string, opts CreateDIDDHTOpts) (ed25519.PrivateKey, *did.Document, error) { + // generate the identity key + for i := 0; i < math.MaxInt32; i++ { + pubKey, privKey, err := crypto.GenerateEd25519Key() + if err != nil { + return nil, nil, err + } + + id := GetDIDDHTIdentifier(pubKey) + + if strings.HasPrefix(id, prefix) { + doc, err := CreateDIDDHTDID(pubKey, opts) + return privKey, doc, err + } + } + return nil, nil, fmt.Errorf("failed to generate vanity did:dht identifier with prefix %s", prefix) +} + // GenerateDIDDHT generates a did:dht identifier given a set of options func GenerateDIDDHT(opts CreateDIDDHTOpts) (ed25519.PrivateKey, *did.Document, error) { // generate the identity key diff --git a/impl/internal/did/did_test.go b/impl/internal/did/did_test.go index 378a04ee..9c1ee606 100644 --- a/impl/internal/did/did_test.go +++ b/impl/internal/did/did_test.go @@ -3,6 +3,7 @@ package did import ( "crypto/ed25519" "testing" + "time" "github.com/goccy/go-json" @@ -309,3 +310,16 @@ func TestVectors(t *testing.T) { } }) } + +func TestVanityDID(t *testing.T) { + now := time.Now() + pk, doc, err := GenerateVanityDIDDHT("gabe", CreateDIDDHTOpts{}) + require.NoError(t, err) + require.NotEmpty(t, pk) + require.NotEmpty(t, doc) + + b, _ := json.Marshal(doc) + println(string(b)) + + println(time.Since(now).String()) +} diff --git a/impl/pkg/service/gateway_test.go b/impl/pkg/service/gateway_test.go new file mode 100644 index 00000000..5dd6fe71 --- /dev/null +++ b/impl/pkg/service/gateway_test.go @@ -0,0 +1,34 @@ +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/TBD54566975/did-dht-method/config" + "github.com/TBD54566975/did-dht-method/pkg/storage" +) + +func TestGatewayService(t *testing.T) { + svc := newGatewayService(t) + require.NotEmpty(t, svc) + + t.Run("Publish and Get a DID", func(t *testing.T) { + + }) +} + +func newGatewayService(t *testing.T) GatewayService { + defaultConfig := config.GetDefaultConfig() + db, err := storage.NewStorage(defaultConfig.ServerConfig.DBFile) + require.NoError(t, err) + require.NotEmpty(t, db) + pkarrService, err := NewPkarrService(&defaultConfig, db) + require.NoError(t, err) + require.NotEmpty(t, pkarrService) + + gatewayService, err := NewGatewayService(&defaultConfig, db, pkarrService) + require.NoError(t, err) + require.NotEmpty(t, gatewayService) + return *gatewayService +} diff --git a/impl/pkg/service/pkarr_test.go b/impl/pkg/service/pkarr_test.go index 9b4372a2..3f67a853 100644 --- a/impl/pkg/service/pkarr_test.go +++ b/impl/pkg/service/pkarr_test.go @@ -13,8 +13,8 @@ import ( "github.com/TBD54566975/did-dht-method/pkg/storage" ) -func TestPKARRService(t *testing.T) { - svc := newPKARRService(t) +func TestPkarrService(t *testing.T) { + svc := newPkarrService(t) require.NotEmpty(t, svc) t.Run("test put bad record", func(t *testing.T) { @@ -100,7 +100,7 @@ func TestPKARRService(t *testing.T) { }) } -func newPKARRService(t *testing.T) PkarrService { +func newPkarrService(t *testing.T) PkarrService { defaultConfig := config.GetDefaultConfig() db, err := storage.NewStorage(defaultConfig.ServerConfig.DBFile) require.NoError(t, err)