diff --git a/cid/README.md b/cid/README.md new file mode 100644 index 0000000..0281701 --- /dev/null +++ b/cid/README.md @@ -0,0 +1,4 @@ +# Controlled Identifier Documents + +This directory contains an implementation of the [Controlled Identifier Document 1.0](https://w3c.github.io/cid/) +specification. \ No newline at end of file diff --git a/controller/controller.go b/cid/cid.go similarity index 62% rename from controller/controller.go rename to cid/cid.go index 0cd5402..150d3ed 100644 --- a/controller/controller.go +++ b/cid/cid.go @@ -1,4 +1,4 @@ -package controller +package cid import ( "github.com/lestrrat-go/jwx/v2/jwk" @@ -25,14 +25,14 @@ type Document struct { } type VerificationMethod struct { - ID string `json:"id" validate:"required"` - Type string `json:"type" validate:"required"` - Controller string `json:"controller" validate:"required"` - Revoked string `json:"revoked,omitempty"` - PublicKeyJWK jwk.Key `json:"publicKeyJwk,omitempty"` - SecretKeyJWK jwk.Key `json:"secretKeyJwk,omitempty"` - PublicKeyMultibase string `json:"publicKeyMultibase,omitempty"` - SecretKeyMultibase string `json:"secretKeyMultibase,omitempty"` + ID string `json:"id" validate:"required"` + Type string `json:"type" validate:"required"` + Controller util.SingleOrArray[string] `json:"controller" validate:"required"` + Revoked string `json:"revoked,omitempty"` + PublicKeyJWK jwk.Key `json:"publicKeyJwk,omitempty"` + SecretKeyJWK jwk.Key `json:"secretKeyJwk,omitempty"` + PublicKeyMultibase string `json:"publicKeyMultibase,omitempty"` + SecretKeyMultibase string `json:"secretKeyMultibase,omitempty"` } type VerificationMethodMap struct { diff --git a/controller/README.md b/controller/README.md deleted file mode 100644 index 8d35cd2..0000000 --- a/controller/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Controller Documents - -This directory contains an implementation of the [Controller Documents 1.0](https://www.w3.org/TR/controller-document/) -specification. \ No newline at end of file diff --git a/credential/testdata/vp-enveloped-vc-example-1.json b/credential/testdata/vp-enveloped-vc-example-1.json index 255fdf5..7aa7187 100644 --- a/credential/testdata/vp-enveloped-vc-example-1.json +++ b/credential/testdata/vp-enveloped-vc-example-1.json @@ -7,6 +7,6 @@ "verifiableCredential": [{ "@context": "https://www.w3.org/ns/credentials/v2", "type": "EnvelopedVerifiableCredential", - "id": "data:application/vc-ld+jwt,eyJraWQiOiJFeEhrQk1XOWZtYmt2VjI2Nm1ScHVQMnNVWV9OX0VXSU4xbGFwVXpPOHJvIiwiYWxnIjoiRVMzODQifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjIiXSwiaWQiOiJodHRwOi8vdW5pdmVyc2l0eS5leGFtcGxlL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRXhhbXBsZUFsdW1uaUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly91bml2ZXJzaXR5LmV4YW1wbGUvaXNzdWVycy81NjUwNDkiLCJ2YWxpZEZyb20iOiIyMDEwLTAxLTAxVDE5OjIzOjI0WiIsImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL2V4YW1wbGUub3JnL2V4YW1wbGVzL2RlZ3JlZS5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19.d2k4O3FytQJf83kLh-HsXuPvh6yeOlhJELVo5TF71gu7elslQyOf2ZItAXrtbXF4Kz9WivNdztOayz4VUQ0Mwa8yCDZkP9B2pH-9S_tcAFxeoeJ6Z4XnFuL_DOfkR1fP" + "id": "data:application/vc+jwt,eyJraWQiOiJFeEhrQk1XOWZtYmt2VjI2Nm1ScHVQMnNVWV9OX0VXSU4xbGFwVXpPOHJvIiwiYWxnIjoiRVMzODQifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjIiXSwiaWQiOiJodHRwOi8vdW5pdmVyc2l0eS5leGFtcGxlL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRXhhbXBsZUFsdW1uaUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly91bml2ZXJzaXR5LmV4YW1wbGUvaXNzdWVycy81NjUwNDkiLCJ2YWxpZEZyb20iOiIyMDEwLTAxLTAxVDE5OjIzOjI0WiIsImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL2V4YW1wbGUub3JnL2V4YW1wbGVzL2RlZ3JlZS5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19.d2k4O3FytQJf83kLh-HsXuPvh6yeOlhJELVo5TF71gu7elslQyOf2ZItAXrtbXF4Kz9WivNdztOayz4VUQ0Mwa8yCDZkP9B2pH-9S_tcAFxeoeJ6Z4XnFuL_DOfkR1fP" }] } \ No newline at end of file diff --git a/credential/testdata/vp-enveloped-vp-example-1.json b/credential/testdata/vp-enveloped-vp-example-1.json index db65f48..a9b2129 100644 --- a/credential/testdata/vp-enveloped-vp-example-1.json +++ b/credential/testdata/vp-enveloped-vp-example-1.json @@ -4,5 +4,5 @@ "https://www.w3.org/ns/credentials/examples/v2" ], "type": "EnvelopedVerifiablePresentation", - "id": "data:application/vp-ld+jwt,eyJraWQiOiJFeEhrQk1XOWZtYmt2VjI2Nm1ScHVQMnNVWV9OX0VXSU4xbGFwVXpPOHJvIiwiYWxnIjoiRVMzODQifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjIiXSwiaWQiOiJodHRwOi8vdW5pdmVyc2l0eS5leGFtcGxlL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRXhhbXBsZUFsdW1uaUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly91bml2ZXJzaXR5LmV4YW1wbGUvaXNzdWVycy81NjUwNDkiLCJ2YWxpZEZyb20iOiIyMDEwLTAxLTAxVDE5OjIzOjI0WiIsImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL2V4YW1wbGUub3JnL2V4YW1wbGVzL2RlZ3JlZS5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19.d2k4O3FytQJf83kLh-HsXuPvh6yeOlhJELVo5TF71gu7elslQyOf2ZItAXrtbXF4Kz9WivNdztOayz4VUQ0Mwa8yCDZkP9B2pH-9S_tcAFxeoeJ6Z4XnFuL_DOfkR1fP" + "id": "data:application/vp+jwt,eyJraWQiOiJFeEhrQk1XOWZtYmt2VjI2Nm1ScHVQMnNVWV9OX0VXSU4xbGFwVXpPOHJvIiwiYWxnIjoiRVMzODQifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjIiXSwiaWQiOiJodHRwOi8vdW5pdmVyc2l0eS5leGFtcGxlL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRXhhbXBsZUFsdW1uaUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly91bml2ZXJzaXR5LmV4YW1wbGUvaXNzdWVycy81NjUwNDkiLCJ2YWxpZEZyb20iOiIyMDEwLTAxLTAxVDE5OjIzOjI0WiIsImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL2V4YW1wbGUub3JnL2V4YW1wbGVzL2RlZ3JlZS5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19.d2k4O3FytQJf83kLh-HsXuPvh6yeOlhJELVo5TF71gu7elslQyOf2ZItAXrtbXF4Kz9WivNdztOayz4VUQ0Mwa8yCDZkP9B2pH-9S_tcAFxeoeJ6Z4XnFuL_DOfkR1fP" } \ No newline at end of file diff --git a/crypto_test.go b/crypto_test.go new file mode 100644 index 0000000..56e46de --- /dev/null +++ b/crypto_test.go @@ -0,0 +1,134 @@ +package vc_jose_cose_go + +import ( + "fmt" + "github.com/TBD54566975/vc-jose-cose-go/cid" + "github.com/TBD54566975/vc-jose-cose-go/util" + "github.com/goccy/go-json" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/stretchr/testify/require" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestGenerateKeys is used to generate sample cid document verification methods +func TestGenerateKeys(t *testing.T) { + t.Skip("skipping test as it is not needed except for local testing") + + tests := []struct { + name string + curve jwa.EllipticCurveAlgorithm + kid string + }{ + {"EC P-256", jwa.P256, "key-1"}, + {"EC P-384", jwa.P384, "key-2"}, + {"EC P-521", jwa.P521, "key-3"}, + {"OKP EdDSA", jwa.Ed25519, "key-4"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, err := util.GenerateJWK(tt.curve) + require.NoError(t, err) + assert.NotNil(t, key) + + pubKey, err := key.PublicKey() + require.NoError(t, err) + assert.NotNil(t, pubKey) + + id := "https://example.issuer/6c427e8392ab4057b93356fbb9022ecb" + vm := cid.VerificationMethod{ + ID: fmt.Sprintf("%s#%s", id, tt.kid), + Type: cid.TypeJSONWebKey, + Controller: util.SingleOrArray[string]{id}, + PublicKeyJWK: pubKey, + SecretKeyJWK: key, + } + + vmJSONBytes, err := json.Marshal(vm) + require.NoError(t, err) + t.Logf("\n%s\n", string(vmJSONBytes)) + }) + } +} + +func TestKeyToBytes(t *testing.T) { + for _, keyType := range util.GetSupportedKeyTypes() { + t.Run(string(keyType), func(t *testing.T) { + pub, priv, err := util.GenerateKeyByKeyType(keyType) + + assert.NoError(t, err) + assert.NotEmpty(t, pub) + assert.NotEmpty(t, priv) + + pubKeyBytes, err := util.PubKeyToBytes(pub) + assert.NoError(t, err) + assert.NotEmpty(t, pubKeyBytes) + + reconstructedPub, err := util.BytesToPubKey(pubKeyBytes, keyType) + assert.NoError(t, err) + assert.NotEmpty(t, reconstructedPub) + assert.EqualValues(t, pub, reconstructedPub) + + privKeyBytes, err := util.PrivKeyToBytes(priv) + assert.NoError(t, err) + assert.NotEmpty(t, privKeyBytes) + + reconstructedPriv, err := util.BytesToPrivKey(privKeyBytes, keyType) + assert.NoError(t, err) + assert.NotEmpty(t, reconstructedPriv) + assert.EqualValues(t, priv, reconstructedPriv) + + kt, err := util.GetKeyTypeFromPrivateKey(priv) + assert.NoError(t, err) + assert.Equal(t, keyType, kt) + }) + } + + for _, keyType := range util.GetSupportedKeyTypes() { + t.Run(string(keyType)+" with pointers", func(t *testing.T) { + pub, priv, err := util.GenerateKeyByKeyType(keyType) + + assert.NoError(t, err) + assert.NotEmpty(t, pub) + assert.NotEmpty(t, priv) + + pubKeyBytes, err := util.PubKeyToBytes(&pub) + assert.NoError(t, err) + assert.NotEmpty(t, pubKeyBytes) + + reconstructedPub, err := util.BytesToPubKey(pubKeyBytes, keyType) + assert.NoError(t, err) + assert.NotEmpty(t, reconstructedPub) + assert.EqualValues(t, pub, reconstructedPub) + + privKeyBytes, err := util.PrivKeyToBytes(&priv) + assert.NoError(t, err) + assert.NotEmpty(t, privKeyBytes) + + reconstructedPriv, err := util.BytesToPrivKey(privKeyBytes, keyType) + assert.NoError(t, err) + assert.NotEmpty(t, reconstructedPriv) + assert.EqualValues(t, priv, reconstructedPriv) + + kt, err := util.GetKeyTypeFromPrivateKey(&priv) + assert.NoError(t, err) + assert.Equal(t, keyType, kt) + }) + } +} + +func TestSECP256k1Conversions(t *testing.T) { + pk, sk, err := util.GenerateSECP256k1Key() + assert.NoError(t, err) + + ecdsaPK := pk.ToECDSA() + ecdsaSK := sk.ToECDSA() + + gotPK := util.SECP256k1ECDSAPubKeyToSECP256k1(*ecdsaPK) + gotSK := util.SECP256k1ECDSASPrivKeyToSECP256k1(*ecdsaSK) + + assert.Equal(t, pk, gotPK) + assert.Equal(t, sk, gotSK) +} diff --git a/jose/jose.go b/jose/jose.go index 937fdad..835b37f 100644 --- a/jose/jose.go +++ b/jose/jose.go @@ -114,18 +114,18 @@ func VerifyVerifiableCredential(jwt string, key jwk.Key) (*credential.Verifiable } // SignVerifiablePresentation dynamically signs a VerifiablePresentation based on the key type. -func SignVerifiablePresentation(vp credential.VerifiablePresentation, key jwk.Key) (string, error) { +func SignVerifiablePresentation(vp credential.VerifiablePresentation, key jwk.Key) (*string, error) { if vp.IsEmpty() { - return "", errors.New("VerifiablePresentation is empty") + return nil, errors.New("VerifiablePresentation is empty") } if key == nil { - return "", errors.New("key is required") + return nil, errors.New("key is required") } if key.KeyID() == "" { - return "", errors.New("key ID is required") + return nil, errors.New("key ID is required") } if key.Algorithm().String() == "" { - return "", errors.New("key algorithm is required") + return nil, errors.New("key algorithm is required") } var alg jwa.SignatureAlgorithm @@ -134,7 +134,7 @@ func SignVerifiablePresentation(vp credential.VerifiablePresentation, key jwk.Ke case jwa.EC: crv, ok := key.Get("crv") if !ok || crv == nil { - return "", fmt.Errorf("invalid or missing 'crv' parameter") + return nil, fmt.Errorf("invalid or missing 'crv' parameter") } crvAlg := crv.(jwa.EllipticCurveAlgorithm) switch crvAlg { @@ -145,22 +145,22 @@ func SignVerifiablePresentation(vp credential.VerifiablePresentation, key jwk.Ke case jwa.P521: alg = jwa.ES512 default: - return "", fmt.Errorf("unsupported curve: %s", crvAlg.String()) + return nil, fmt.Errorf("unsupported curve: %s", crvAlg.String()) } case jwa.OKP: alg = jwa.EdDSA default: - return "", fmt.Errorf("unsupported key type: %s", kty) + return nil, fmt.Errorf("unsupported key type: %s", kty) } // Convert the VerifiablePresentation to a map for manipulation vpMap := make(map[string]any) vpBytes, err := json.Marshal(vp) if err != nil { - return "", err + return nil, err } if err = json.Unmarshal(vpBytes, &vpMap); err != nil { - return "", err + return nil, err } // Add standard claims @@ -179,7 +179,7 @@ func SignVerifiablePresentation(vp credential.VerifiablePresentation, key jwk.Ke // Marshal the claims to JSON payload, err := json.Marshal(vpMap) if err != nil { - return "", err + return nil, err } // Add protected header values @@ -192,17 +192,18 @@ func SignVerifiablePresentation(vp credential.VerifiablePresentation, key jwk.Ke } for k, v := range headers { if err = jwsHeaders.Set(k, v); err != nil { - return "", err + return nil, err } } // Sign the payload signed, err := jws.Sign(payload, jws.WithKey(alg, key, jws.WithProtectedHeaders(jwsHeaders))) if err != nil { - return "", err + return nil, err } - return string(signed), nil + result := string(signed) + return &result, nil } // VerifyVerifiablePresentation verifies a VerifiablePresentation JWT using the provided key. diff --git a/jose/jose_test.go b/jose/jose_test.go index 2066f60..7cc8e03 100644 --- a/jose/jose_test.go +++ b/jose/jose_test.go @@ -79,7 +79,7 @@ func Test_Sign_Verify_VerifiablePresentation(t *testing.T) { assert.NotEmpty(t, jwt) // Verify the VP - verifiedVP, err := VerifyVerifiablePresentation(jwt, key) + verifiedVP, err := VerifyVerifiablePresentation(*jwt, key) require.NoError(t, err) assert.Equal(t, vp.ID, verifiedVP.ID) assert.Equal(t, vp.Holder.ID(), verifiedVP.Holder.ID()) diff --git a/util/crypto_test.go b/util/crypto_test.go deleted file mode 100644 index cd774f4..0000000 --- a/util/crypto_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package util - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestKeyToBytes(t *testing.T) { - for _, keyType := range GetSupportedKeyTypes() { - t.Run(string(keyType), func(t *testing.T) { - pub, priv, err := GenerateKeyByKeyType(keyType) - - assert.NoError(t, err) - assert.NotEmpty(t, pub) - assert.NotEmpty(t, priv) - - pubKeyBytes, err := PubKeyToBytes(pub) - assert.NoError(t, err) - assert.NotEmpty(t, pubKeyBytes) - - reconstructedPub, err := BytesToPubKey(pubKeyBytes, keyType) - assert.NoError(t, err) - assert.NotEmpty(t, reconstructedPub) - assert.EqualValues(t, pub, reconstructedPub) - - privKeyBytes, err := PrivKeyToBytes(priv) - assert.NoError(t, err) - assert.NotEmpty(t, privKeyBytes) - - reconstructedPriv, err := BytesToPrivKey(privKeyBytes, keyType) - assert.NoError(t, err) - assert.NotEmpty(t, reconstructedPriv) - assert.EqualValues(t, priv, reconstructedPriv) - - kt, err := GetKeyTypeFromPrivateKey(priv) - assert.NoError(t, err) - assert.Equal(t, keyType, kt) - }) - } - - for _, keyType := range GetSupportedKeyTypes() { - t.Run(string(keyType)+" with pointers", func(t *testing.T) { - pub, priv, err := GenerateKeyByKeyType(keyType) - - assert.NoError(t, err) - assert.NotEmpty(t, pub) - assert.NotEmpty(t, priv) - - pubKeyBytes, err := PubKeyToBytes(&pub) - assert.NoError(t, err) - assert.NotEmpty(t, pubKeyBytes) - - reconstructedPub, err := BytesToPubKey(pubKeyBytes, keyType) - assert.NoError(t, err) - assert.NotEmpty(t, reconstructedPub) - assert.EqualValues(t, pub, reconstructedPub) - - privKeyBytes, err := PrivKeyToBytes(&priv) - assert.NoError(t, err) - assert.NotEmpty(t, privKeyBytes) - - reconstructedPriv, err := BytesToPrivKey(privKeyBytes, keyType) - assert.NoError(t, err) - assert.NotEmpty(t, reconstructedPriv) - assert.EqualValues(t, priv, reconstructedPriv) - - kt, err := GetKeyTypeFromPrivateKey(&priv) - assert.NoError(t, err) - assert.Equal(t, keyType, kt) - }) - } -} - -func TestSECP256k1Conversions(t *testing.T) { - pk, sk, err := GenerateSECP256k1Key() - assert.NoError(t, err) - - ecdsaPK := pk.ToECDSA() - ecdsaSK := sk.ToECDSA() - - gotPK := SECP256k1ECDSAPubKeyToSECP256k1(*ecdsaPK) - gotSK := SECP256k1ECDSASPrivKeyToSECP256k1(*ecdsaSK) - - assert.Equal(t, pk, gotPK) - assert.Equal(t, sk, gotSK) -}