diff --git a/design/design.go b/design/design.go index eafcfefa6ba78fd0b8be5cf2a3583c822ae377bf..cb8477f5eb610cabf6a2496b4f4025f73d08a509 100644 --- a/design/design.go +++ b/design/design.go @@ -58,6 +58,18 @@ var _ = Service("signer", func() { }) }) + Method("JwkPublicKey", func() { + Description("JwkPublicKey returns public key by name and namespace.") + Payload(JwkPublicKeyRequest) + Result(Any, "Public key encoded as JSON Web Key.") + HTTP(func() { + GET("/v1/jwk/{namespace}/{key}") + Response(StatusOK) + Response(StatusNotFound) + Response(StatusInternalServerError) + }) + }) + Method("CredentialProof", func() { Description("CredentialProof adds a proof to a given Verifiable Credential.") Payload(CredentialProofRequest) diff --git a/design/types.go b/design/types.go index bbcc86a7dbe8c4d04e1462577c1191d6fe1e8c4a..dd8ee49915247d8ef34c4cba261dac1a71d3c386 100644 --- a/design/types.go +++ b/design/types.go @@ -159,6 +159,16 @@ var DIDVerificationMethod = Type("DIDVerificationMethod", func() { Required("id", "type", "controller", "publicKeyJwk") }) +var JwkPublicKeyRequest = Type("JwkPublicKeyRequest", func() { + Field(1, "namespace", String, "Key namespace.", func() { + Example("transit") + }) + Field(2, "key", String, "Key name.", func() { + Example("my-ecdsa-key1") + }) + Required("namespace", "key") +}) + var SignRequest = Type("SignRequest", func() { Field(1, "namespace", String, "Key namespace to be used for signing.") Field(2, "key", String, "Key to be used for signing.") diff --git a/internal/service/signer/service.go b/internal/service/signer/service.go index 685ecd112e4d870f4f58e8601753780d316307c4..889dcfb14827cf31775ed84425bba33e6f95d32c 100644 --- a/internal/service/signer/service.go +++ b/internal/service/signer/service.go @@ -195,6 +195,33 @@ func (s *Service) VerificationMethods(ctx context.Context, req *signer.Verificat return res, nil } +// JwkPublicKey returns public key by name and namespace. +func (s *Service) JwkPublicKey(ctx context.Context, req *signer.JwkPublicKeyRequest) (any, error) { + logger := s.logger.With( + zap.String("operation", "jwkPublicKey"), + zap.String("namespace", req.Namespace), + zap.String("key", req.Key), + ) + + key, err := s.vault.Key(ctx, req.Namespace, req.Key) + if err != nil { + logger.Error("error getting key", zap.Error(err)) + return nil, err + } + + pubKey, err := s.jwkFromKey(key) + if err != nil { + logger.Error("error converting public key to jwk", + zap.String("key", key.Name), + zap.String("keyType", key.Type), + zap.Error(err), + ) + return nil, fmt.Errorf("error converting public key to jwk: %v", err) + } + + return pubKey, nil +} + // CredentialProof adds a proof to a given Verifiable Credential. func (s *Service) CredentialProof(ctx context.Context, req *signer.CredentialProofRequest) (interface{}, error) { logger := s.logger.With( diff --git a/internal/service/signer/service_test.go b/internal/service/signer/service_test.go index 70c919ad404ace0ed2545cf1811e80539e1aae7a..e1b0c4b498c8aca27dce7489bba1780dc8a203b9 100644 --- a/internal/service/signer/service_test.go +++ b/internal/service/signer/service_test.go @@ -7,7 +7,9 @@ import ( "encoding/base64" "encoding/json" "fmt" + "net" "net/http" + "os" "testing" "time" @@ -26,10 +28,26 @@ import ( var docLoader *ld.CachingDocumentLoader -func init() { +func TestMain(m *testing.M) { + c := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 5 * time.Second, + }).DialContext, + MaxIdleConns: 1, + MaxIdleConnsPerHost: 1, + TLSHandshakeTimeout: 5 * time.Second, + IdleConnTimeout: 60 * time.Second, + }, + Timeout: 10 * time.Second, + } + if docLoader == nil { - docLoader = ld.NewCachingDocumentLoader(ld.NewDefaultDocumentLoader(http.DefaultClient)) + docLoader = ld.NewCachingDocumentLoader(ld.NewDefaultDocumentLoader(c)) } + + os.Exit(m.Run()) } func TestService_Namespaces(t *testing.T) { @@ -262,6 +280,52 @@ func TestService_VerificationMethods(t *testing.T) { }) } +func TestService_JwkPublicKey(t *testing.T) { + t.Run("signer returns error when getting key", func(t *testing.T) { + vaultError := &signerfakes.FakeVault{ + KeyStub: func(ctx context.Context, namespace, key string) (*signer.VaultKey, error) { + return nil, errors.New(errors.NotFound, "key not found") + }, + } + + svc := signer.New(vaultError, []string{}, docLoader, zap.NewNop()) + result, err := svc.JwkPublicKey( + context.Background(), + &goasigner.JwkPublicKeyRequest{Namespace: "transit", Key: "key1"}, + ) + assert.Nil(t, result) + assert.Error(t, err) + e, ok := err.(*errors.Error) + assert.True(t, ok) + assert.Equal(t, errors.NotFound, e.Kind) + }) + + t.Run("signer returns ecdsa-p256 key successfully", func(t *testing.T) { + signerOK := &signerfakes.FakeVault{ + KeyStub: func(ctx context.Context, namespace, key string) (*signer.VaultKey, error) { + return &signer.VaultKey{ + Name: "key1", + Type: "ecdsa-p256", + PublicKey: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERTx/2cyYcGVSIRP/826S32BiZxSg\nnzyXgRYmKP8N2l26ec/MwCdsHIEyraX1ZYqwMUT4wO9fqFiGsRKyMBpPnQ==\n-----END PUBLIC KEY-----\n", + }, nil + }, + } + + svc := signer.New(signerOK, []string{"ecdsa-p256"}, docLoader, zap.NewNop()) + result, err := svc.JwkPublicKey( + context.Background(), + &goasigner.JwkPublicKeyRequest{Namespace: "transit", Key: "key1"}, + ) + assert.NotNil(t, result) + assert.NoError(t, err) + + pub, ok := result.(*jose.JSONWebKey) + assert.True(t, ok) + assert.NotNil(t, pub) + assert.IsType(t, (*ecdsa.PublicKey)(nil), pub.Key) + }) +} + func TestService_CredentialProof(t *testing.T) { tests := []struct { name string @@ -428,7 +492,7 @@ func TestService_CredentialProof(t *testing.T) { }) if err != nil { assert.Nil(t, res) - require.NotEmpty(t, test.errtext, "error is not expected, but got: %v ") + require.NotEmpty(t, test.errtext, "error is not expected, but got: %v ", err) assert.Contains(t, err.Error(), test.errtext) if e, ok := err.(*errors.Error); ok { assert.Equal(t, test.errkind, e.Kind) @@ -451,7 +515,7 @@ func TestService_CredentialProof(t *testing.T) { } }) - time.Sleep(500 * time.Millisecond) + time.Sleep(1 * time.Second) } } @@ -652,7 +716,7 @@ func TestService_PresentationProof(t *testing.T) { assert.NotEmpty(t, vp.Proofs[0]["jws"]) }) - time.Sleep(500 * time.Millisecond) + time.Sleep(1 * time.Second) } } @@ -815,6 +879,8 @@ func TestService_CreateCredential(t *testing.T) { assert.Equal(t, test.wantedCredentialSubject, vc.Subject) } }) + + time.Sleep(1 * time.Second) } }