From e7d184d231cea00d56b5efcd8a0d43b9f864f034 Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Fri, 7 Jul 2023 16:52:25 -0400 Subject: [PATCH] `jwsPublicKeys` as an option when creating ION DIDs. (#577) * Allow `jwsPublicKeys` as an option when creating ION DIDs. * Removed the non-signed public key logic. --- doc/swagger.yaml | 26 ++++++++++- integration/didion_integration_test.go | 42 +++++++++-------- integration/testdata/did-ion-input.json | 20 +------- pkg/server/router/did.go | 4 +- pkg/service/did/ion.go | 49 ++++++++++++++++++-- pkg/service/did/ion_test.go | 61 +++++++++++++++++++++---- 6 files changed, 149 insertions(+), 53 deletions(-) diff --git a/doc/swagger.yaml b/doc/swagger.yaml index 9726b76b6..ce3445d37 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -563,6 +563,25 @@ definitions: description: Whether this credential is currently suspended. type: boolean type: object + github_com_tbd54566975_ssi-service_pkg_service_did.CreateIONDIDOptions: + properties: + jwsPublicKeys: + description: |- + List of JSON Web Signatures serialized using compact serialization. The payload must be a JSON object that + represents a publicKey object. Such object must follow the schema described in step 3 of + https://identity.foundation/sidetree/spec/#add-public-keys. The payload must be signed + with the private key associated with the `publicKeyJwk` that will be added in the DID document. + The input will be parsed and verified, and the payload will be used to add public keys to the DID document in the + same way in which the `add-public-keys` patch action adds keys (see https://identity.foundation/sidetree/spec/#add-public-keys). + items: + type: string + type: array + serviceEndpoints: + description: Services to add to the DID document that will be created. + items: + $ref: '#/definitions/github_com_TBD54566975_ssi-sdk_did.Service' + type: array + type: object github_com_tbd54566975_ssi-service_pkg_service_framework.Status: properties: message: @@ -2464,7 +2483,12 @@ paths: name: request required: true schema: - $ref: '#/definitions/pkg_server_router.CreateDIDByMethodRequest' + allOf: + - $ref: '#/definitions/pkg_server_router.CreateDIDByMethodRequest' + - properties: + options: + $ref: '#/definitions/github_com_tbd54566975_ssi-service_pkg_service_did.CreateIONDIDOptions' + type: object produces: - application/json responses: diff --git a/integration/didion_integration_test.go b/integration/didion_integration_test.go index a70d4ac32..11778f8b2 100644 --- a/integration/didion_integration_test.go +++ b/integration/didion_integration_test.go @@ -44,26 +44,28 @@ func TestCreateIssuerDIDIONIntegration(t *testing.T) { assert.NotEmpty(t, verificationMethodID) SetValue(didIONContext, "verificationMethodID", verificationMethodID) - verificationMethod1ID, err := getJSONElement(didIONOutput, "$.did.verificationMethod[1].id") - assert.NoError(t, err) - assert.Equal(t, "#externalVerificationMethodId", verificationMethod1ID) - - verificationMethod1KID, err := getJSONElement(didIONOutput, "$.did.verificationMethod[1].publicKeyJwk.kid") - assert.NoError(t, err) - assert.Equal(t, "myExternalPublicKID", verificationMethod1KID) - - keyAgreementKID, err := getJSONElement(didIONOutput, "$.did.keyAgreement[0]") - assert.NoError(t, err) - assert.Equal(t, "#externalVerificationMethodId", keyAgreementKID) - - capabilityInvocationKID, err := getJSONElement(didIONOutput, "$.did.capabilityInvocation[0]") - assert.NoError(t, err) - assert.Equal(t, "#externalVerificationMethodId", capabilityInvocationKID) - - capabilityDelegationKID, err := getJSONElement(didIONOutput, "$.did.capabilityDelegation[0]") - assert.NoError(t, err) - assert.Equal(t, "#externalVerificationMethodId", capabilityDelegationKID) - + // The jwsPublicKeys entry represents the following: + //{ + // "id": "test-id", + // "type": "JsonWebKey2020", + // "publicKeyJwk": { + // "kty": "OKP", + // "crv": "Ed25519", + // "x": "ghzv2q5WwYO3Y6ZH-MQJvBkd45zbTSyJ6gU1q3yk01E", + // "alg": "EdDSA", + // "kid": "test-kid" + // }, + // "purposes": [ + // "authentication" + // ] + //} + verificationMethod2ID, err := getJSONElement(didIONOutput, "$.did.verificationMethod[1].id") + assert.NoError(t, err) + assert.Equal(t, "#test-id", verificationMethod2ID) + + verificationMethod2KID, err := getJSONElement(didIONOutput, "$.did.verificationMethod[1].publicKeyJwk.kid") + assert.NoError(t, err) + assert.Equal(t, "test-kid", verificationMethod2KID) } func TestCreateAliceDIDKeyForDIDIONIntegration(t *testing.T) { diff --git a/integration/testdata/did-ion-input.json b/integration/testdata/did-ion-input.json index 52e9edd2a..642af1b6e 100644 --- a/integration/testdata/did-ion-input.json +++ b/integration/testdata/did-ion-input.json @@ -2,24 +2,8 @@ "keyType":"Ed25519", "options": { "serviceEndpoints": [], - "publicKeys": [ - { - "id": "externalVerificationMethodId", - "type": "JsonWebKey2020", - "publicKeyJwk": { - "kty": "OKP", - "crv": "Ed25519", - "x": "CV-aGlld3nVdgnhoZK0D36Wk-9aIMlZjZOK2XhPMnkQ", - "kid": "myExternalPublicKID" - }, - "purposes": [ - "authentication", - "assertionMethod", - "capabilityInvocation", - "capabilityDelegation", - "keyAgreement" - ] - } + "jwsPublicKeys": [ + "eyJhbGciOiJFZERTQSJ9.eyJpZCI6InRlc3QtaWQiLCJ0eXBlIjoiSnNvbldlYktleTIwMjAiLCJwdWJsaWNLZXlKd2siOnsia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsIngiOiJnaHp2MnE1V3dZTzNZNlpILU1RSnZCa2Q0NXpiVFN5SjZnVTFxM3lrMDFFIiwiYWxnIjoiRWREU0EiLCJraWQiOiJ0ZXN0LWtpZCJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdfQ.Q27mR09J2Uq8zdgEQ-oM5clsSJOEso_XMxbTeD_o3s33rWAy3yVwgm1ZfziRPnLUsEO4hsGsZZtr3FEFwl91Bw" ] } } \ No newline at end of file diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go index 7b68c91c0..d07386ce9 100644 --- a/pkg/server/router/did.go +++ b/pkg/server/router/did.go @@ -82,8 +82,8 @@ type CreateDIDByMethodResponse struct { // @Tags DecentralizedIdentityAPI // @Accept json // @Produce json -// @Param method path string true "Method" -// @Param request body CreateDIDByMethodRequest true "request body" +// @Param method path string true "Method" +// @Param request body CreateDIDByMethodRequest{options=did.CreateIONDIDOptions} true "request body" // @Success 201 {object} CreateDIDByMethodResponse // @Failure 400 {string} string "Bad request" // @Failure 500 {string} string "Internal server error" diff --git a/pkg/service/did/ion.go b/pkg/service/did/ion.go index 766f3c800..be8afc316 100644 --- a/pkg/service/did/ion.go +++ b/pkg/service/did/ion.go @@ -11,7 +11,9 @@ import ( "github.com/TBD54566975/ssi-sdk/did/ion" "github.com/TBD54566975/ssi-sdk/did/resolution" "github.com/TBD54566975/ssi-sdk/util" + "github.com/goccy/go-json" "github.com/google/uuid" + "github.com/lestrrat-go/jwx/v2/jws" "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -52,8 +54,16 @@ type ionHandler struct { var _ MethodHandler = (*ionHandler)(nil) type CreateIONDIDOptions struct { - ServiceEndpoints []did.Service `json:"serviceEndpoints"` - PublicKeys []ion.PublicKey `json:"publicKeys"` + // Services to add to the DID document that will be created. + ServiceEndpoints []did.Service `json:"serviceEndpoints"` + + // List of JSON Web Signatures serialized using compact serialization. The payload must be a JSON object that + // represents a publicKey object. Such object must follow the schema described in step 3 of + // https://identity.foundation/sidetree/spec/#add-public-keys. The payload must be signed + // with the private key associated with the `publicKeyJwk` that will be added in the DID document. + // The input will be parsed and verified, and the payload will be used to add public keys to the DID document in the + // same way in which the `add-public-keys` patch action adds keys (see https://identity.foundation/sidetree/spec/#add-public-keys). + JWSPublicKeys []string `json:"jwsPublicKeys"` } func (c CreateIONDIDOptions) Method() did.Method { @@ -88,6 +98,7 @@ func (h *ionHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (* // process options var opts CreateIONDIDOptions var ok bool + var publicKeysFromJWS []ion.PublicKey if request.Options != nil { opts, ok = request.Options.(CreateIONDIDOptions) if !ok || request.Options.Method() != did.IONMethod { @@ -96,6 +107,38 @@ func (h *ionHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (* if err := util.IsValidStruct(opts); err != nil { return nil, errors.Wrap(err, "processing options") } + + publicKeysFromJWS = make([]ion.PublicKey, 0, len(opts.JWSPublicKeys)) + for _, jwsString := range opts.JWSPublicKeys { + m, err := jws.ParseString(jwsString) + if err != nil { + return nil, errors.Wrapf(err, "parsing JWS string <%s>", jwsString) + } + + headers, err := jwx.GetJWSHeaders([]byte(jwsString)) + if err != nil { + return nil, errors.Wrapf(err, "getting JWS headers from <%s>", jwsString) + } + + var publicKey ion.PublicKey + if err := json.Unmarshal(m.Payload(), &publicKey); err != nil { + return nil, errors.Wrap(err, "unmarshalling payload") + } + if err := util.IsValidStruct(publicKey); err != nil { + return nil, errors.Wrap(err, "invalid publicKey in payload") + } + + goPublicKey, err := publicKey.PublicKeyJWK.ToPublicKey() + if err != nil { + return nil, errors.Wrap(err, "converting JWK to go crypto public key") + } + + if _, err := jws.Verify([]byte(jwsString), jws.WithKey(headers.Algorithm(), goPublicKey)); err != nil { + return nil, errors.Wrapf(err, "verifying JWS for <%s>", jwsString) + } + + publicKeysFromJWS = append(publicKeysFromJWS, publicKey) + } } // create a key for the docs @@ -117,7 +160,7 @@ func (h *ionHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (* Purposes: []ion.PublicKeyPurpose{ion.Authentication, ion.AssertionMethod}, }, } - pubKeys = append(pubKeys, opts.PublicKeys...) + pubKeys = append(pubKeys, publicKeysFromJWS...) // generate the did document's initial state doc := ion.Document{PublicKeys: pubKeys, Services: opts.ServiceEndpoints} diff --git a/pkg/service/did/ion_test.go b/pkg/service/did/ion_test.go index f4a781e6e..ff4e5df62 100644 --- a/pkg/service/did/ion_test.go +++ b/pkg/service/did/ion_test.go @@ -7,7 +7,9 @@ import ( "testing" "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/did/ion" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,7 +26,7 @@ var BasicDIDResolution []byte func TestIONHandler(t *testing.T) { for _, test := range testutil.TestDatabases { t.Run(test.Name, func(t *testing.T) { - t.Run("Test Create ION Handler", func(tt *testing.T) { + t.Run("Create ION Handler", func(tt *testing.T) { handler, err := NewIONHandler("", nil, nil) assert.Error(tt, err) assert.Empty(tt, handler) @@ -56,7 +58,7 @@ func TestIONHandler(t *testing.T) { assert.Equal(tt, handler.GetMethod(), did.IONMethod) }) - t.Run("Test Create DID", func(tt *testing.T) { + t.Run("Create DID", func(tt *testing.T) { // create a handler s := test.ServiceStorage(t) keystoreService := testKeyStoreService(tt, s) @@ -72,16 +74,57 @@ func TestIONHandler(t *testing.T) { BodyString(string(BasicDIDResolution)) defer gock.Off() + publicKey, privKey, err := crypto.GenerateEd25519Key() + require.NoError(tt, err) + signer, err := jwx.NewJWXSigner("test-id", "test-kid", privKey) + require.NoError(tt, err) + jwxJWK, err := jwx.PublicKeyToPublicKeyJWK("test-kid", publicKey) + require.NoError(tt, err) + ionPublicKey := ion.PublicKey{ + ID: "test-id", + Type: "JsonWebKey2020", + PublicKeyJWK: *jwxJWK, + Purposes: []ion.PublicKeyPurpose{ion.Authentication}, + } + ionPublicKeyData, err := json.Marshal(ionPublicKey) + require.NoError(tt, err) + jwsPublicKey, err := signer.SignJWS(ionPublicKeyData) + require.NoError(tt, err) + // create a did - created, err := handler.CreateDID(context.Background(), CreateDIDRequest{ + createDIDRequest := CreateDIDRequest{ Method: did.IONMethod, KeyType: crypto.Ed25519, + Options: CreateIONDIDOptions{ + ServiceEndpoints: []did.Service{}, + JWSPublicKeys: []string{string(jwsPublicKey)}, + }, + } + + tt.Run("good input returns no error", func(t *testing.T) { + created, err := handler.CreateDID(context.Background(), createDIDRequest) + assert.NoError(tt, err) + assert.NotEmpty(tt, created) + }) + + tt.Run("signing with another key returns error", func(ttt *testing.T) { + a := createDIDRequest + _, privKey, err := crypto.GenerateEd25519Key() + require.NoError(ttt, err) + signer2, err := jwx.NewJWXSigner("test-id", "test-kid", privKey) + require.NoError(ttt, err) + signedWithOtherKey, err := signer2.SignJWS(ionPublicKeyData) + require.NoError(ttt, err) + a.Options.(CreateIONDIDOptions).JWSPublicKeys[0] = string(signedWithOtherKey) + + _, err = handler.CreateDID(context.Background(), createDIDRequest) + + assert.Error(ttt, err) + assert.ErrorContains(ttt, err, "verifying JWS for") }) - assert.NoError(tt, err) - assert.NotEmpty(tt, created) }) - t.Run("Test Create DID", func(tt *testing.T) { + t.Run("Get a Created DID", func(tt *testing.T) { gock.New("https://ion.tbddev.org"). Post("/operations"). Reply(200). @@ -114,7 +157,7 @@ func TestIONHandler(t *testing.T) { assert.NotEmpty(tt, gotDID) }) - t.Run("Test Get DID from storage", func(tt *testing.T) { + t.Run("Get DID from storage", func(tt *testing.T) { // create a handler s := test.ServiceStorage(t) keystoreService := testKeyStoreService(tt, s) @@ -153,7 +196,7 @@ func TestIONHandler(t *testing.T) { assert.Equal(tt, created.DID.ID, gotDID.DID.ID) }) - t.Run("Test Get DIDs from storage", func(tt *testing.T) { + t.Run("Get DIDs from storage", func(tt *testing.T) { // create a handler s := test.ServiceStorage(t) keystoreService := testKeyStoreService(tt, s) @@ -211,7 +254,7 @@ func TestIONHandler(t *testing.T) { assert.Len(tt, gotDeletedDIDs.DIDs, 1) }) - t.Run("Test Get DID from resolver", func(tt *testing.T) { + t.Run("Get DID from resolver", func(tt *testing.T) { // create a handler s := test.ServiceStorage(t) keystoreService := testKeyStoreService(tt, s)