Repository: TBD54566975/ssi-service
Pull request #191 (2022-11-30 20:13:46)

adding did:web
# Overview
This implements the creation of did:web's into the ssi-service

# Description

Input:
PUT
http://localhost:8080/v1/dids/web
{
  "keyType":"Ed25519",
  "didWebId":"did:web:tbd.website"
}

Output:
```
{
    "did": {
        "@context": "https://www.w3.org/ns/did/v1",
        "id": "did:web:tbd.website",
        "verificationMethod": [
            {
                "id": "did:web:tbd.website#owner",
                "type": "Ed25519VerificationKey2018",
                "controller": "did:web:tbd.website",
                "publicKeyBase58": "4HyjANoMdhpp952YPb4wALydQRQ7BmsXHmBdwq4YSyiR"
            }
        ],
        "authentication": [
            [
                "did:web:tbd.website#owner"
            ]
        ],
        "assertionMethod": [
            [
                "did:web:tbd.website#owner"
            ]
        ]
    },
    "privateKeyBase58": "3mG6UhpXKZxeg7BwQZNpgEYiJUMkeDYATrHoLAbQDsiqeQEYwXxer7Stw6gVGdHwjPMxAmxUosrhnxPAWLBjcoqB",
    "keyType": "Ed25519"
}
```



codecov-commenter: # [Codecov](https://codecov.io/gh/TBD54566975/ssi-service/pull/191?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#191](https://codecov.io/gh/TBD54566975/ssi-service/pull/191?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (1de03ca) into [main](https://codecov.io/gh/TBD54566975/ssi-service/commit/c25553b07ef1aa798a6f002f125755626b699ce2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (c25553b) will **not change** coverage.
> The diff coverage is `0.00%`.

```diff
@@           Coverage Diff           @@
##             main     #191   +/-   ##
=======================================
  Coverage   24.98%   24.98%           
=======================================
  Files          21       21           
  Lines        1781     1781           
=======================================
  Hits          445      445           
  Misses       1264     1264           
  Partials       72       72           
```


| [Impacted Files](https://codecov.io/gh/TBD54566975/ssi-service/pull/191?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [config/config.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/191/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-Y29uZmlnL2NvbmZpZy5nbw==) | `11.57% <0.00%> (ø)` | |
| [pkg/server/router/did.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/191/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvZGlkLmdv) | `7.92% <0.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975)

nitro-neal: Yup I'll make new integ test and also plug up the resolution part too

Patch:
diff --git a/config/compose.toml b/config/compose.toml
index 735a3b5..c3826c4 100644
--- a/config/compose.toml
+++ b/config/compose.toml
@@ -30,7 +30,7 @@ password = "default-password"
 
 [services.did]
 name = "did"
-methods = ["key"]
+methods = ["key", "web"]
 resolution_methods = ["key", "web", "pkh", "peer"]
 
 [services.schema]
diff --git a/config/config.go b/config/config.go
index 666c824..2ea145d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -188,7 +188,7 @@ func LoadConfig(path string) (*SSIServiceConfig, error) {
 			},
 			DIDConfig: DIDServiceConfig{
 				BaseServiceConfig: &BaseServiceConfig{Name: "did"},
-				Methods:           []string{"key"},
+				Methods:           []string{"key", "web"},
 				ResolutionMethods: []string{"key", "peer", "web", "pkh"},
 			},
 			SchemaConfig: SchemaServiceConfig{
diff --git a/config/config.toml b/config/config.toml
index e9417d0..e1c7b1e 100644
--- a/config/config.toml
+++ b/config/config.toml
@@ -28,7 +28,7 @@ password = "default-password"
 
 [services.did]
 name = "did"
-methods = ["key"]
+methods = ["key", "web"]
 resolution_methods = ["key", "web", "pkh", "peer"]
 
 [services.schema]
diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go
index c223b08..152afd7 100644
--- a/pkg/server/router/did.go
+++ b/pkg/server/router/did.go
@@ -58,7 +58,8 @@ func (dr DIDRouter) GetDIDMethods(ctx context.Context, w http.ResponseWriter, _
 }
 
 type CreateDIDByMethodRequest struct {
-	KeyType crypto.KeyType `json:"keyType" validate:"required"`
+	KeyType  crypto.KeyType `json:"keyType" validate:"required"`
+	DIDWebID string         `json:"didWebId"`
 }
 
 type CreateDIDByMethodResponse struct {
@@ -102,7 +103,7 @@ func (dr DIDRouter) CreateDIDByMethod(ctx context.Context, w http.ResponseWriter
 	}
 
 	// TODO(gabe) check if the key type is supported for the method, to tell whether this is a bad req or internal error
-	createDIDRequest := did.CreateDIDRequest{Method: didsdk.Method(*method), KeyType: request.KeyType}
+	createDIDRequest := did.CreateDIDRequest{Method: didsdk.Method(*method), KeyType: request.KeyType, DIDWebID: request.DIDWebID}
 	createDIDResponse, err := dr.service.CreateDIDByMethod(createDIDRequest)
 	if err != nil {
 		errMsg := fmt.Sprintf("could not create DID for method<%s> with key type: %s", *method, request.KeyType)
diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go
index 6eb0635..10736e2 100644
--- a/pkg/service/did/model.go
+++ b/pkg/service/did/model.go
@@ -21,8 +21,9 @@ type ResolveDIDResponse struct {
 
 // CreateDIDRequest is the JSON-serializable request for creating a DID across DID methods
 type CreateDIDRequest struct {
-	Method  didsdk.Method  `json:"method" validate:"required"`
-	KeyType crypto.KeyType `validate:"required"`
+	Method   didsdk.Method  `json:"method" validate:"required"`
+	KeyType  crypto.KeyType `validate:"required"`
+	DIDWebID string         `json:"didWebId"`
 }
 
 // CreateDIDResponse is the JSON-serializable response for creating a DID
diff --git a/pkg/service/did/service.go b/pkg/service/did/service.go
index 2e7657f..011b0db 100644
--- a/pkg/service/did/service.go
+++ b/pkg/service/did/service.go
@@ -107,6 +107,8 @@ func (s *Service) instantiateHandlerForMethod(method didsdk.Method) error {
 	switch method {
 	case didsdk.KeyMethod:
 		s.handlers[method] = newKeyDIDHandler(s.storage, s.keyStore)
+	case didsdk.WebMethod:
+		s.handlers[method] = newWebDIDHandler(s.storage, s.keyStore)
 	default:
 		return util.LoggingNewErrorf("unsupported DID method: %s", method)
 	}
diff --git a/pkg/service/did/storage/bolt.go b/pkg/service/did/storage/bolt.go
index 70b2a78..454aa16 100644
--- a/pkg/service/did/storage/bolt.go
+++ b/pkg/service/did/storage/bolt.go
@@ -14,11 +14,13 @@ import (
 const (
 	namespace    = "did"
 	keyNamespace = "key"
+	webNamespace = "web"
 )
 
 var (
 	didMethodToNamespace = map[string]string{
 		"key": storage.MakeNamespace(namespace, keyNamespace),
+		"web": storage.MakeNamespace(namespace, webNamespace),
 	}
 )
 
diff --git a/pkg/service/did/web.go b/pkg/service/did/web.go
new file mode 100644
index 0000000..a7fb1a0
--- /dev/null
+++ b/pkg/service/did/web.go
@@ -0,0 +1,117 @@
+package did
+
+import (
+	"fmt"
+
+	"github.com/TBD54566975/ssi-sdk/crypto"
+	"github.com/TBD54566975/ssi-sdk/did"
+	"github.com/mr-tron/base58"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+
+	"github.com/tbd54566975/ssi-service/pkg/service/did/storage"
+	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
+)
+
+func newWebDIDHandler(s storage.Storage, ks *keystore.Service) MethodHandler {
+	return &webDIDHandler{storage: s, keyStore: ks}
+}
+
+type webDIDHandler struct {
+	storage  storage.Storage
+	keyStore *keystore.Service
+}
+
+func (h *webDIDHandler) CreateDID(request CreateDIDRequest) (*CreateDIDResponse, error) {
+	logrus.Debugf("creating DID: %+v", request)
+
+	if request.DIDWebID == "" {
+		return nil, errors.New("url is empty, cannot create did:web")
+	}
+
+	didWeb := did.DIDWeb(request.DIDWebID)
+
+	if !didWeb.IsValid() {
+		return nil, fmt.Errorf("did:web is not valid, could not resolve did:web DID: %s", didWeb)
+	}
+
+	pubKey, privKey, err := crypto.GenerateKeyByKeyType(request.KeyType)
+	if err != nil {
+		return nil, errors.Wrap(err, "could not generate key for did:web")
+	}
+
+	pubKeyBytes, err := crypto.PubKeyToBytes(pubKey)
+	if err != nil {
+		return nil, errors.Wrap(err, "could not convert public key to byte")
+	}
+
+	doc, err := didWeb.CreateDoc(request.KeyType, pubKeyBytes)
+	if err != nil {
+		return nil, errors.Wrap(err, "could not create did:web doc")
+	}
+
+	// store metadata in DID storage
+	id := didWeb.String()
+	storedDID := storage.StoredDID{
+		ID:  id,
+		DID: *doc,
+	}
+	if err = h.storage.StoreDID(storedDID); err != nil {
+		return nil, errors.Wrap(err, "could not store did:web value")
+	}
+
+	// convert to a serialized format for return to the client
+	privKeyBytes, err := crypto.PrivKeyToBytes(privKey)
+	if err != nil {
+		return nil, errors.Wrap(err, "could not encode private key as base58")
+	}
+	privKeyBase58 := base58.Encode(privKeyBytes)
+
+	// store private key in key storage
+	keyStoreRequest := keystore.StoreKeyRequest{
+		ID:               id,
+		Type:             request.KeyType,
+		Controller:       id,
+		PrivateKeyBase58: privKeyBase58,
+	}
+
+	if err = h.keyStore.StoreKey(keyStoreRequest); err != nil {
+		return nil, errors.Wrap(err, "could not store did:key private key")
+	}
+
+	return &CreateDIDResponse{
+		DID:              storedDID.DID,
+		PrivateKeyBase58: privKeyBase58,
+		KeyType:          request.KeyType,
+	}, nil
+}
+
+func (h *webDIDHandler) GetDID(request GetDIDRequest) (*GetDIDResponse, error) {
+
+	logrus.Debugf("getting DID: %+v", request)
+
+	id := request.ID
+	gotDID, err := h.storage.GetDID(id)
+	if err != nil {
+		return nil, fmt.Errorf("error getting DID: %s", id)
+	}
+	if gotDID == nil {
+		return nil, fmt.Errorf("did with id<%s> could not be found", id)
+	}
+	return &GetDIDResponse{DID: gotDID.DID}, nil
+}
+
+func (h *webDIDHandler) GetDIDs(method did.Method) (*GetDIDsResponse, error) {
+
+	logrus.Debugf("getting DIDs for method: %s", method)
+
+	gotDIDs, err := h.storage.GetDIDs(string(method))
+	if err != nil {
+		return nil, fmt.Errorf("error getting DIDs for method: %s", method)
+	}
+	dids := make([]did.DIDDocument, 0, len(gotDIDs))
+	for _, did := range gotDIDs {
+		dids = append(dids, did.DID)
+	}
+	return &GetDIDsResponse{DIDs: dids}, nil
+}


Test patch:
diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go
index 49bacef..124d156 100644
--- a/pkg/server/router/did_test.go
+++ b/pkg/server/router/did_test.go
@@ -103,4 +103,76 @@ func TestDIDRouter(t *testing.T) {
 		}
 		assert.Len(tt, knownDIDs, 0)
 	})
+
+	t.Run("DID Web Service Test", func(tt *testing.T) {
+		_ = os.Remove(storage.DBFile)
+
+		db, err := storage.NewBoltDB()
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, db)
+
+		keyStoreService := testKeyStoreService(tt, db)
+		methods := []string{didsdk.KeyMethod.String(), didsdk.WebMethod.String()}
+		serviceConfig := config.DIDServiceConfig{Methods: methods, ResolutionMethods: methods}
+		didService, err := did.NewDIDService(serviceConfig, db, keyStoreService)
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, didService)
+
+		// check type and status
+		assert.Equal(tt, framework.DID, didService.Type())
+		assert.Equal(tt, framework.StatusReady, didService.Status().Status)
+
+		// get unknown handler
+		_, err = didService.GetDIDByMethod(did.GetDIDRequest{Method: "bad"})
+		assert.Error(tt, err)
+		assert.Contains(tt, err.Error(), "could not get handler for method<bad>")
+
+		supported := didService.GetSupportedMethods()
+		assert.NotEmpty(tt, supported)
+		assert.Len(tt, supported.Methods, 2)
+		assert.Equal(tt, didsdk.KeyMethod, supported.Methods[0])
+		assert.Equal(tt, didsdk.WebMethod, supported.Methods[1])
+
+		// bad key type
+		_, err = didService.CreateDIDByMethod(did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: "bad", DIDWebID: "did:web:example.com"})
+		assert.Error(tt, err)
+		assert.Contains(tt, err.Error(), "could not generate key for did:web")
+
+		// good key type
+		createDIDResponse, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, DIDWebID: "did:web:example.com"})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, createDIDResponse)
+
+		// check the DID is a did:key
+		assert.Contains(tt, createDIDResponse.DID.ID, "did:web")
+
+		// get it back
+		getDIDResponse, err := didService.GetDIDByMethod(did.GetDIDRequest{Method: didsdk.WebMethod, ID: createDIDResponse.DID.ID})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, getDIDResponse)
+
+		// make sure it's the same value
+		assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID)
+
+		// create a second DID
+		createDIDResponse2, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, DIDWebID: "did:web:tbd.website"})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, createDIDResponse2)
+
+		// get all DIDs back
+		getDIDsResponse, err := didService.GetDIDsByMethod(did.GetDIDsRequest{Method: didsdk.WebMethod})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, getDIDsResponse)
+		assert.Len(tt, getDIDsResponse.DIDs, 2)
+
+		knownDIDs := map[string]bool{createDIDResponse.DID.ID: true, createDIDResponse2.DID.ID: true}
+		for _, did := range getDIDsResponse.DIDs {
+			if _, ok := knownDIDs[did.ID]; !ok {
+				tt.Error("got unknown DID")
+			} else {
+				delete(knownDIDs, did.ID)
+			}
+		}
+		assert.Len(tt, knownDIDs, 0)
+	})
 }