From c4f5cb853dd6688e744b16a59c34be6036a6522d Mon Sep 17 00:00:00 2001
From: Lyuben Penkovski <penkovski@gmail.com>
Date: Wed, 22 Jun 2022 17:10:11 +0300
Subject: [PATCH] Return public key formmated as DID verification method

---
 design/design.go                        |  2 +-
 gen/http/openapi3.json                  |  2 +-
 gen/http/openapi3.yaml                  |  1 +
 go.mod                                  |  2 +-
 internal/clients/vault/client.go        |  4 +-
 internal/service/signer/service.go      | 65 +++++++++++++++++++++++--
 internal/service/signer/service_test.go | 25 ++++++----
 7 files changed, 83 insertions(+), 18 deletions(-)

diff --git a/design/design.go b/design/design.go
index cbba829..6b9e934 100644
--- a/design/design.go
+++ b/design/design.go
@@ -47,7 +47,7 @@ var _ = Service("signer", func() {
 	Method("GetKey", func() {
 		Description("GetKey returns key information from Vault or OCM.")
 		Payload(GetKeyRequest)
-		Result(Any)
+		Result(Any, "Public Key represented as DID Verification Method.")
 		HTTP(func() {
 			GET("/v1/keys/{key}")
 
diff --git a/gen/http/openapi3.json b/gen/http/openapi3.json
index 8252c78..59bb6e9 100644
--- a/gen/http/openapi3.json
+++ b/gen/http/openapi3.json
@@ -1 +1 @@
-{"openapi":"3.0.3","info":{"title":"Signer Service","description":"The signer service exposes HTTP API for creating and verifying digital signatures.","version":"1.0"},"servers":[{"url":"http://localhost:8085","description":"Signer Server"}],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response."}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response."}}}},"/v1/credential/proof":{"post":{"tags":["signer"],"summary":"CredentialProof signer","description":"CredentialProof adds a proof to a given Verifiable Credential.","operationId":"signer#CredentialProof","parameters":[{"name":"key","in":"query","description":"Key to use for the proof signature (optional).","allowEmptyValue":true,"schema":{"type":"string","description":"Key to use for the proof signature (optional).","example":"key1"},"example":"key1"}],"requestBody":{"description":"Verifiable Credential in JSON format.","required":true,"content":{"application/json":{"schema":{"type":"string","description":"Verifiable Credential in JSON format.","example":"RGVsZW5pdGkgaXBzYS4=","format":"binary"},"example":"RW5pbSBkZXNlcnVudC4="}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Itaque adipisci voluptas.","format":"binary"},"example":"Cupiditate et aliquid reiciendis pariatur."}}}}}},"/v1/keys/{key}":{"get":{"tags":["signer"],"summary":"GetKey signer","description":"GetKey returns key information from Vault or OCM.","operationId":"signer#GetKey","parameters":[{"name":"key","in":"path","description":"Name of requested key.","required":true,"schema":{"type":"string","description":"Name of requested key.","example":"key1"},"example":"key1"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Laudantium exercitationem quis sunt eos.","format":"binary"},"example":"Eum molestiae."}}}}}},"/v1/presentation/proof":{"post":{"tags":["signer"],"summary":"PresentationProof signer","description":"PresentationProof adds a proof to a given Verifiable Presentation.","operationId":"signer#PresentationProof","parameters":[{"name":"key","in":"query","description":"Key to use for the proof signature (optional).","allowEmptyValue":true,"schema":{"type":"string","description":"Key to use for the proof signature (optional).","example":"key1"},"example":"key1"}],"requestBody":{"description":"Verifiable Presentation in JSON format.","required":true,"content":{"application/json":{"schema":{"type":"string","description":"Verifiable Presentation in JSON format.","example":"QXNwZXJpb3JlcyBtb2xlc3RpYXMgcXVpLg==","format":"binary"},"example":"TW9sZXN0aWFlIHZlbGl0IG1haW9yZXMgZXQgcXVpYS4="}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Ipsa vel in repudiandae repellat.","format":"binary"},"example":"Voluptatem consectetur."}}}}}}},"components":{},"tags":[{"name":"health","description":"Health service provides health check endpoints."},{"name":"signer","description":"Sign service provides endpoints for making digital signatures and proofs for verifiable credentials and presentations."}]}
\ No newline at end of file
+{"openapi":"3.0.3","info":{"title":"Signer Service","description":"The signer service exposes HTTP API for creating and verifying digital signatures.","version":"1.0"},"servers":[{"url":"http://localhost:8085","description":"Signer Server"}],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response."}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response."}}}},"/v1/credential/proof":{"post":{"tags":["signer"],"summary":"CredentialProof signer","description":"CredentialProof adds a proof to a given Verifiable Credential.","operationId":"signer#CredentialProof","parameters":[{"name":"key","in":"query","description":"Key to use for the proof signature (optional).","allowEmptyValue":true,"schema":{"type":"string","description":"Key to use for the proof signature (optional).","example":"key1"},"example":"key1"}],"requestBody":{"description":"Verifiable Credential in JSON format.","required":true,"content":{"application/json":{"schema":{"type":"string","description":"Verifiable Credential in JSON format.","example":"RGVsZW5pdGkgaXBzYS4=","format":"binary"},"example":"RW5pbSBkZXNlcnVudC4="}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Itaque adipisci voluptas.","format":"binary"},"example":"Cupiditate et aliquid reiciendis pariatur."}}}}}},"/v1/keys/{key}":{"get":{"tags":["signer"],"summary":"GetKey signer","description":"GetKey returns key information from Vault or OCM.","operationId":"signer#GetKey","parameters":[{"name":"key","in":"path","description":"Name of requested key.","required":true,"schema":{"type":"string","description":"Name of requested key.","example":"key1"},"example":"key1"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","description":"Public Key represented as DID Verification Method.","example":"Laudantium exercitationem quis sunt eos.","format":"binary"},"example":"Eum molestiae."}}}}}},"/v1/presentation/proof":{"post":{"tags":["signer"],"summary":"PresentationProof signer","description":"PresentationProof adds a proof to a given Verifiable Presentation.","operationId":"signer#PresentationProof","parameters":[{"name":"key","in":"query","description":"Key to use for the proof signature (optional).","allowEmptyValue":true,"schema":{"type":"string","description":"Key to use for the proof signature (optional).","example":"key1"},"example":"key1"}],"requestBody":{"description":"Verifiable Presentation in JSON format.","required":true,"content":{"application/json":{"schema":{"type":"string","description":"Verifiable Presentation in JSON format.","example":"QXNwZXJpb3JlcyBtb2xlc3RpYXMgcXVpLg==","format":"binary"},"example":"TW9sZXN0aWFlIHZlbGl0IG1haW9yZXMgZXQgcXVpYS4="}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Ipsa vel in repudiandae repellat.","format":"binary"},"example":"Voluptatem consectetur."}}}}}}},"components":{},"tags":[{"name":"health","description":"Health service provides health check endpoints."},{"name":"signer","description":"Sign service provides endpoints for making digital signatures and proofs for verifiable credentials and presentations."}]}
\ No newline at end of file
diff --git a/gen/http/openapi3.yaml b/gen/http/openapi3.yaml
index a0b5711..d131c6a 100644
--- a/gen/http/openapi3.yaml
+++ b/gen/http/openapi3.yaml
@@ -115,6 +115,7 @@ paths:
                         application/json:
                             schema:
                                 type: string
+                                description: Public Key represented as DID Verification Method.
                                 example: Laudantium exercitationem quis sunt eos.
                                 format: binary
                             example: Eum molestiae.
diff --git a/go.mod b/go.mod
index 0fe2cd0..698ec0e 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/hyperledger/aries-framework-go v0.1.8
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/piprate/json-gold v0.4.1-0.20210813112359-33b90c4ca86c
+	github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693
 	github.com/stretchr/testify v1.7.0
 	go.uber.org/zap v1.21.0
 	goa.design/goa/v3 v3.7.6
@@ -52,7 +53,6 @@ require (
 	github.com/ryanuber/go-glob v1.0.0 // indirect
 	github.com/sergi/go-diff v1.2.0 // indirect
 	github.com/smartystreets/assertions v1.13.0 // indirect
-	github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 // indirect
 	github.com/teserakt-io/golang-ed25519 v0.0.0-20210104091850-3888c087a4c8 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
diff --git a/internal/clients/vault/client.go b/internal/clients/vault/client.go
index 62539bb..0c41523 100644
--- a/internal/clients/vault/client.go
+++ b/internal/clients/vault/client.go
@@ -113,7 +113,9 @@ func (c *Client) Sign(data []byte) ([]byte, error) {
 		return nil, fmt.Errorf("unexpected response: no signature")
 	}
 
-	// remove vault specific prefix from the signature, e.g. vault:v1:xxx -> xxx
+	// strip the vault specific prefix from the signature, e.g. vault:v1:xxx -> xxx
+	// because verifiers are not going to use our Vault for verification, but must
+	// verify the "pure" signature on their own.
 	var signature string
 	s := strings.Split(response.Data.Signature, ":")
 	if len(s) > 1 {
diff --git a/internal/service/signer/service.go b/internal/service/signer/service.go
index 0225380..b813939 100644
--- a/internal/service/signer/service.go
+++ b/internal/service/signer/service.go
@@ -2,6 +2,9 @@ package signer
 
 import (
 	"context"
+	"crypto/ed25519"
+	"crypto/x509"
+	"encoding/pem"
 	"fmt"
 	"net/http"
 
@@ -11,6 +14,7 @@ import (
 	"github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/jsonwebsignature2020"
 	"github.com/hyperledger/aries-framework-go/pkg/doc/verifiable"
 	"github.com/piprate/json-gold/ld"
+	"github.com/square/go-jose/v3"
 	"go.uber.org/zap"
 
 	"code.vereign.com/gaiax/tsa/golib/errors"
@@ -19,6 +23,12 @@ import (
 
 //go:generate counterfeiter . Vault
 
+type VerificationMethod struct {
+	ID           string           `json:"id"`
+	Type         string           `json:"type"`
+	PublicKeyJWK *jose.JSONWebKey `json:"publicKeyJWK"`
+}
+
 type VaultKey struct {
 	Name      string `json:"name"`
 	Type      string `json:"type"`
@@ -54,13 +64,34 @@ func New(vault Vault, issuer string, defaultKey string, supportedKeys []string,
 
 // GetKey returns a key from Vault or OCM.
 func (s *Service) GetKey(ctx context.Context, req *signer.GetKeyRequest) (interface{}, error) {
+	logger := s.logger.With(zap.String("operation", "getKey"))
+
 	key, err := s.vault.Key(ctx, req.Key)
 	if err != nil {
-		s.logger.Error("error getting key", zap.Error(err))
+		logger.Error("error getting key", zap.Error(err))
 		return nil, err
 	}
 
-	return key, nil
+	if !s.supportedKey(key.Type) {
+		logger.Error("unsupported key type", zap.String("key", req.Key), zap.String("keyType", key.Type))
+		return nil, fmt.Errorf("unsupported key type: %s", key.Type)
+	}
+
+	pubKey, err := s.jwkFromKey(key)
+	if err != nil {
+		logger.Error("error making JWK from Vault key",
+			zap.String("key", req.Key),
+			zap.String("keyType", key.Type),
+			zap.Error(err),
+		)
+		return nil, fmt.Errorf("error converting vault key to JWK")
+	}
+
+	return &VerificationMethod{
+		ID:           req.Key,
+		Type:         "JsonWebKey2020",
+		PublicKeyJWK: pubKey,
+	}, nil
 }
 
 // CredentialProof adds a proof to a given Verifiable Credential.
@@ -86,7 +117,7 @@ func (s *Service) CredentialProof(ctx context.Context, req *signer.CredentialPro
 
 	if !s.supportedKey(key.Type) {
 		logger.Error("unsupported key type", zap.String("key", keyname), zap.String("keyType", key.Type))
-		return nil, fmt.Errorf("unsupported key type: %q", key.Type)
+		return nil, fmt.Errorf("unsupported key type: %s", key.Type)
 	}
 
 	proofContext, err := s.proofContext(key.Name)
@@ -126,7 +157,7 @@ func (s *Service) PresentationProof(ctx context.Context, req *signer.Presentatio
 
 	if !s.supportedKey(key.Type) {
 		logger.Error("unsupported key type", zap.String("key", keyname), zap.String("keyType", key.Type))
-		return nil, fmt.Errorf("unsupported key type: %q", key.Type)
+		return nil, fmt.Errorf("unsupported key type: %s", key.Type)
 	}
 
 	proofContext, err := s.proofContext(key.Name)
@@ -177,3 +208,29 @@ func (s *Service) supportedKey(keyType string) bool {
 	}
 	return false
 }
+
+func (s *Service) jwkFromKey(key *VaultKey) (*jose.JSONWebKey, error) {
+	k := &jose.JSONWebKey{
+		KeyID: key.Name,
+	}
+
+	switch key.Type {
+	case "ed25519":
+		k.Key = ed25519.PublicKey(key.PublicKey)
+	case "ecdsa-p256", "ecdsa-p384", "ecdsa-p521", "rsa-2048":
+		block, _ := pem.Decode([]byte(key.PublicKey))
+		if block == nil {
+			return nil, fmt.Errorf("no public key found during PEM decode")
+		}
+
+		pub, err := x509.ParsePKIXPublicKey(block.Bytes)
+		if err != nil {
+			return nil, err
+		}
+		k.Key = pub
+	default:
+		return nil, fmt.Errorf("unsupported key type: %s", key.Type)
+	}
+
+	return k, nil
+}
diff --git a/internal/service/signer/service_test.go b/internal/service/signer/service_test.go
index 48fc867..09b31f3 100644
--- a/internal/service/signer/service_test.go
+++ b/internal/service/signer/service_test.go
@@ -2,6 +2,7 @@ package signer_test
 
 import (
 	"context"
+	"crypto/ecdsa"
 	"encoding/base64"
 	"net/http"
 	"testing"
@@ -34,26 +35,30 @@ func TestService_GetKey(t *testing.T) {
 		assert.Equal(t, errors.NotFound, e.Kind)
 	})
 
-	t.Run("signer returns key successfully", func(t *testing.T) {
+	t.Run("signer returns ecdsa-p256 key successfully", func(t *testing.T) {
 		signerOK := &signerfakes.FakeVault{
 			KeyStub: func(ctx context.Context, key string) (*signer.VaultKey, error) {
 				return &signer.VaultKey{
-					Name:      "keyname",
-					Type:      "ed25519",
-					PublicKey: "public key",
+					Name:      "key1",
+					Type:      "ecdsa-p256",
+					PublicKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERTx/2cyYcGVSIRP/826S32BiZxSg\nnzyXgRYmKP8N2l26ec/MwCdsHIEyraX1ZYqwMUT4wO9fqFiGsRKyMBpPnQ==\n-----END PUBLIC KEY-----\n",
 				}, nil
 			},
 		}
 
-		svc := signer.New(signerOK, "issuer", "default key", []string{}, http.DefaultClient, zap.NewNop())
+		svc := signer.New(signerOK, "issuer", "default key", []string{"ecdsa-p256"}, http.DefaultClient, zap.NewNop())
 		result, err := svc.GetKey(context.Background(), &goasigner.GetKeyRequest{Key: "key1"})
 		assert.NotNil(t, result)
 		assert.NoError(t, err)
-		assert.Equal(t, &signer.VaultKey{
-			Name:      "keyname",
-			Type:      "ed25519",
-			PublicKey: "public key",
-		}, result)
+
+		verMethod, ok := result.(*signer.VerificationMethod)
+		assert.True(t, ok)
+
+		assert.Equal(t, "key1", verMethod.ID)
+		assert.Equal(t, "JsonWebKey2020", verMethod.Type)
+		assert.NotNil(t, verMethod.PublicKeyJWK)
+		assert.NotNil(t, verMethod.PublicKeyJWK.Key)
+		assert.IsType(t, verMethod.PublicKeyJWK.Key, (*ecdsa.PublicKey)(nil))
 	})
 }
 
-- 
GitLab