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