From 8f8ea12e5ec7dfd3590120b8b33923df1a99682b Mon Sep 17 00:00:00 2001 From: Gabe <7622243+decentralgabe@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:41:17 -0700 Subject: [PATCH] add COSE; update README (#7) * configs * more * add cose vc * cose tests * update docs * add all * remove tags * lints --- .golangci.yaml | 3 +- CONTRIBUTING.md | 3 - README.md | 151 +++++++++++- codecov.yaml | 14 ++ cose/cose.go | 240 +++++++++++++++++++ cose/cose_test.go | 88 +++++++ go.mod | 6 + go.sum | 6 + jose/jose.go | 37 ++- jose/jose_test.go | 4 +- magefile.go | 4 +- sd-jwt/sd_jwt.go => sdjwt/sdjwt.go | 63 ++++- sd-jwt/sd_jwt_test.go => sdjwt/sdjwt_test.go | 24 +- util/crypto.go | 4 +- 14 files changed, 607 insertions(+), 40 deletions(-) create mode 100644 codecov.yaml create mode 100644 cose/cose.go create mode 100644 cose/cose_test.go rename sd-jwt/sd_jwt.go => sdjwt/sdjwt.go (86%) rename sd-jwt/sd_jwt_test.go => sdjwt/sdjwt_test.go (94%) diff --git a/.golangci.yaml b/.golangci.yaml index 9d6cc1f..d3fb283 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,8 +1,6 @@ # See https://golangci-lint.run/usage/configuration/ for reference. run: concurrency: 16 - build-tags: - - jwx_es256k output: sort-results: true @@ -329,3 +327,4 @@ linters-settings: G101: pattern: "(/i)passwd|pass|password|pwd|secret|token|pw|apiKey|bearer" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4103e7..6c7c49e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,9 +13,6 @@ This guide is for you. ## Development Prerequisites -___***UPDATE TABLE OF PROJECT DEPS AND INSTALLATION NOTES***___ - - | Requirement | Tested Version | Installation Instructions | | ----------- | -------------- | ----------------------------------------------------- | | Go | 1.23.2 | [go.dev](https://go.dev/doc/tutorial/compile-install) | diff --git a/README.md b/README.md index a50a64e..dc2f2ce 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,159 @@ -# VC JOSE COSE in GO +[![godoc vc-jose-cose-go](https://img.shields.io/badge/godoc-vc--jose--cose--go-blue)](https://pkg.go.dev/github.com/TBD54566975/vc-jose-cose-go) +[![go version 1.23.2](https://img.shields.io/badge/go_version-1.23.2-brightgreen)](https://golang.org/) +[![Go Report Card](https://goreportcard.com/badge/github.com/TBD54566975/vc-jose-cose-go)](https://goreportcard.com/report/github.com/TBD54566975/vc-jose-cose-go) +[![license Apache 2](https://img.shields.io/badge/license-Apache%202-black)](https://github.com/TBD54566975/vc-jose-cose-go/blob/main/LICENSE) +[![issues](https://img.shields.io/github/issues/TBD54566975/vc-jose-cose-go)](https://github.com/TBD54566975/vc-jose-cose-go/issues) +![ci status](https://github.com/TBD54566975/vc-jose-cose-go/actions/workflows/ci.yml/badge.svg?branch=main&event=push) +[![codecov](https://codecov.io/github/TBD54566975/vc-jose-cose-go/graph/badge.svg?token=PIS07W0RQJ)](https://codecov.io/github/TBD54566975/vc-jose-cose-go) + +# VC JOSE COSE in go A lightweight go implementation of the [W3C Verifiable Credentials v2 Data Model](https://www.w3.org/TR/vc-data-model-2.0) with support for [Securing Verifiable Credentials using JOSE and COSE](https://www.w3.org/TR/vc-jose-cose/). +## Usage + +This library provides Go implementations for signing and verifying Verifiable Credentials (VCs) and Verifiable Presentations (VPs) using JOSE, SD-JWT, and COSE formats. + +## Installation + +``` +go get github.com/TBD54566975/vc-jose-cose-go +``` + +### JOSE (JSON Object Signing and Encryption) + +```go +import ( + "github.com/TBD54566975/vc-jose-cose-go/jose" + "github.com/TBD54566975/vc-jose-cose-go/credential" + "github.com/TBD54566975/vc-jose-cose-go/util" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwa" +) + +func main() { + // Create a VC + vc := credential.VerifiableCredential{ + Context: []string{"https://www.w3.org/2018/credentials/v1"}, + ID: "https://example.edu/credentials/1872", + Type: []string{"VerifiableCredential"}, + Issuer: credential.NewIssuerHolderFromString("did:example:issuer"), + ValidFrom: "2010-01-01T19:23:24Z", + CredentialSubject: map[string]any{ + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + }, + } + + // Create the issuer's key + key, _ := util.GenerateJWK(jwa.Ed25519) + + // Sign the VC + jwt, err := jose.SignVerifiableCredential(vc, key) + if err != nil { + // Handle error + } + + vc, err := jose.VerifyVerifiableCredential(jwt, key) + if err != nil { + // Handle error + } + // Use the verified VC +} +``` + +### SD-JWT (Selective Disclosure JWT) + +```go + import ( + "github.com/TBD54566975/vc-jose-cose-go/sdjwt" + "github.com/TBD54566975/vc-jose-cose-go/credential" + "github.com/TBD54566975/vc-jose-cose-go/util" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwa" + ) + + func main() { + vc := credential.VerifiableCredential{ + Context: []string{"https://www.w3.org/2018/credentials/v1"}, + ID: "https://example.edu/credentials/1872", + Type: []string{"VerifiableCredential"}, + Issuer: credential.NewIssuerHolderFromString("did:example:issuer"), + ValidFrom: "2010-01-01T19:23:24Z", + CredentialSubject: map[string]any{ + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + }, + } + + // Define disclosure paths + disclosurePaths := []sdjwt.DisclosurePath{ + "issuer", + "credentialSubject.id", + } + + // Create the issuer's key + key, _ := util.GenerateJWK(jwa.Ed25519) + + // Create SD-JWT + sdJWT, err := sdjwt.SignVerifiableCredential(vc, disclosurePaths, issuerKey) + if err != nil { + // Handle error + } + + verifiedVC, err := sdjwt.VerifyVerifiableCredential(*sdJWT, issuerKey) + if err != nil { + // Handle error + } + } +``` + +### COSE (CBOR Object Signing and Encryption) + +```go +import ( + "github.com/TBD54566975/vc-jose-cose-go/cose" + "github.com/TBD54566975/vc-jose-cose-go/credential" + "github.com/TBD54566975/vc-jose-cose-go/util" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwa" +) + +func main() { + // Create a VC + vc := credential.VerifiableCredential{ + Context: []string{"https://www.w3.org/2018/credentials/v1"}, + ID: "https://example.edu/credentials/1872", + Type: []string{"VerifiableCredential"}, + Issuer: credential.NewIssuerHolderFromString("did:example:issuer"), + ValidFrom: "2010-01-01T19:23:24Z", + CredentialSubject: map[string]any{ + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + }, + } + + // Create the issuer's key + key, _ := util.GenerateJWK(jwa.Ed25519) + + // Sign the VC + cs1, err := cose.SignVerifiableCredential(vc, key) + if err != nil { + // Handle error + } + + vc, err := cose.VerifyVerifiableCredential(cs1, key) + if err != nil { + // Handle error + } + // Use the verified VC +} +``` + ## Project Resources | Resource | Description | | ------------------------------------------ | ------------------------------------------------------------------------------ | | [CODEOWNERS](./CODEOWNERS) | Outlines the project lead(s) | | [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) | Expected behavior for project contributors, promoting a welcoming environment | -| [CONTRIBUTING.md](./CONTRIBUTING.md) | Developer guide to build, test, run, access CI, chat, discuss, file issues | +| [CONTRIBUTING.md](./CONTRIBUTING.md) | Developer guide to build, test, run, access CI, chat, discuss, file issues | | [GOVERNANCE.md](./GOVERNANCE.md) | Project governance | -| [LICENSE](./LICENSE) | Apache License, Version 2.0 | +| [LICENSE](./LICENSE) | Apache License, Version 2.0 | \ No newline at end of file diff --git a/codecov.yaml b/codecov.yaml new file mode 100644 index 0000000..c06e23f --- /dev/null +++ b/codecov.yaml @@ -0,0 +1,14 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: nearest + range: "80...100" + +comment: + layout: "reach, diff, flags, files" + behavior: default + require_changes: true + require_head: no + require_base: no \ No newline at end of file diff --git a/cose/cose.go b/cose/cose.go new file mode 100644 index 0000000..33dafda --- /dev/null +++ b/cose/cose.go @@ -0,0 +1,240 @@ +package cose + +import ( + "crypto" + "crypto/rand" + "errors" + "fmt" + + "github.com/goccy/go-json" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/veraison/go-cose" + + "github.com/TBD54566975/vc-jose-cose-go/credential" +) + +const ( + VCCOSEType = "application/vc+cose" + VPCOSEType = "application/vp+cose" +) + +// SignVerifiableCredential signs a VerifiableCredential using COSE. +func SignVerifiableCredential(vc credential.VerifiableCredential, key jwk.Key) ([]byte, error) { + if vc.IsEmpty() { + return nil, errors.New("VerifiableCredential is empty") + } + if key == nil { + return nil, errors.New("key is required") + } + if key.KeyID() == "" { + return nil, errors.New("key ID is required") + } + if key.Algorithm().String() == "" { + return nil, errors.New("key algorithm is required") + } + + signer, err := getCOSESigner(key) + if err != nil { + return nil, fmt.Errorf("failed to get COSE signer: %w", err) + } + + message := cose.NewSign1Message() + message.Headers.Protected.SetAlgorithm(signer.Algorithm()) + _, _ = message.Headers.Protected.SetType(VCCOSEType) + message.Headers.Protected[cose.HeaderLabelContentType] = "application/" + credential.VCContentType + message.Headers.Protected[cose.HeaderLabelKeyID] = []byte(key.KeyID()) + + // Convert VC to a JSON object + vcBytes, err := json.Marshal(vc) + if err != nil { + return nil, fmt.Errorf("failed to convert VC to JSON bytes: %w", err) + } + + message.Payload = vcBytes + if err = message.Sign(rand.Reader, nil, signer); err != nil { + return nil, fmt.Errorf("failed to sign COSE message: %w", err) + } + + // Marshal the claims to CBOR + payload, err := message.MarshalCBOR() + if err != nil { + return nil, fmt.Errorf("failed to marshal VC to CBOR: %w", err) + } + + return payload, nil +} + +// getCOSESigner returns a COSE Signer for the provided JWK key. +func getCOSESigner(key jwk.Key) (cose.Signer, error) { + var rawKey crypto.Signer + if err := key.Raw(&rawKey); err != nil { + return nil, fmt.Errorf("failed to get raw key from JWK: %w", err) + } + var alg cose.Algorithm + switch key.Algorithm() { + case jwa.ES256: + alg = cose.AlgorithmES256 + case jwa.ES384: + alg = cose.AlgorithmES384 + case jwa.ES512: + alg = cose.AlgorithmES512 + case jwa.EdDSA: + alg = cose.AlgorithmEdDSA + default: + return nil, fmt.Errorf("unsupported algorithm: %s", key.Algorithm()) + } + return cose.NewSigner(alg, rawKey) +} + +// VerifyVerifiableCredential verifies a COSE-signed VerifiableCredential using the provided key. +func VerifyVerifiableCredential(payload []byte, key jwk.Key) (*credential.VerifiableCredential, error) { + if payload == nil { + return nil, errors.New("payload is required") + } + if key == nil { + return nil, errors.New("key is required") + } + if key.KeyID() == "" { + return nil, errors.New("key ID is required") + } + if key.Algorithm().String() == "" { + return nil, errors.New("key algorithm is required") + } + + // Parse the COSE message + var message cose.Sign1Message + if err := message.UnmarshalCBOR(payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal COSE message: %w", err) + } + + // Verify the COSE signature + verifier, err := getCOSEVerifier(key) + if err != nil { + return nil, fmt.Errorf("failed to get COSE verifier: %w", err) + } + + if err = message.Verify(nil, verifier); err != nil { + return nil, fmt.Errorf("failed to verify COSE signature: %w", err) + } + + // Unmarshal the payload + var vc credential.VerifiableCredential + if err = json.Unmarshal(message.Payload, &vc); err != nil { + return nil, fmt.Errorf("failed to unmarshal VerifiableCredential: %w", err) + } + + return &vc, nil +} + +// getCOSESigner returns a COSE Signer for the provided JWK key. +func getCOSEVerifier(key jwk.Key) (cose.Verifier, error) { + var rawKey crypto.PublicKey + pubKey, err := key.PublicKey() + if err != nil { + return nil, fmt.Errorf("failed to get public key from JWK: %w", err) + } + if err = pubKey.Raw(&rawKey); err != nil { + return nil, fmt.Errorf("failed to get raw key from JWK: %w", err) + } + var alg cose.Algorithm + switch key.Algorithm() { + case jwa.ES256: + alg = cose.AlgorithmES256 + case jwa.ES384: + alg = cose.AlgorithmES384 + case jwa.ES512: + alg = cose.AlgorithmES512 + case jwa.EdDSA: + alg = cose.AlgorithmEdDSA + default: + return nil, fmt.Errorf("unsupported algorithm: %s", key.Algorithm()) + } + return cose.NewVerifier(alg, rawKey) +} + +// SignVerifiablePresentation signs a VerifiablePresentation using COSE. +func SignVerifiablePresentation(vp credential.VerifiablePresentation, key jwk.Key) ([]byte, error) { + if vp.IsEmpty() { + return nil, errors.New("VerifiablePresentation is empty") + } + if key == nil { + return nil, errors.New("key is required") + } + if key.KeyID() == "" { + return nil, errors.New("key ID is required") + } + if key.Algorithm().String() == "" { + return nil, errors.New("key algorithm is required") + } + + signer, err := getCOSESigner(key) + if err != nil { + return nil, fmt.Errorf("failed to get COSE signer: %w", err) + } + + message := cose.NewSign1Message() + message.Headers.Protected.SetAlgorithm(signer.Algorithm()) + _, _ = message.Headers.Protected.SetType(VPCOSEType) + message.Headers.Protected[cose.HeaderLabelContentType] = "application/" + credential.VPContentType + message.Headers.Protected[cose.HeaderLabelKeyID] = []byte(key.KeyID()) + + // Convert VP to a JSON object + vpBytes, err := json.Marshal(vp) + if err != nil { + return nil, fmt.Errorf("failed to convert VP to JSON bytes: %w", err) + } + + message.Payload = vpBytes + if err = message.Sign(rand.Reader, nil, signer); err != nil { + return nil, fmt.Errorf("failed to sign COSE message: %w", err) + } + + // Marshal the claims to CBOR + payload, err := message.MarshalCBOR() + if err != nil { + return nil, fmt.Errorf("failed to marshal VP to CBOR: %w", err) + } + + return payload, nil +} + +// VerifyVerifiablePresentation verifies a COSE-signed VerifiablePresentation using the provided key. +func VerifyVerifiablePresentation(payload []byte, key jwk.Key) (*credential.VerifiablePresentation, error) { + if payload == nil { + return nil, errors.New("payload is required") + } + if key == nil { + return nil, errors.New("key is required") + } + if key.KeyID() == "" { + return nil, errors.New("key ID is required") + } + if key.Algorithm().String() == "" { + return nil, errors.New("key algorithm is required") + } + + // Parse the COSE message + var message cose.Sign1Message + if err := message.UnmarshalCBOR(payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal COSE message: %w", err) + } + + // Verify the COSE signature + verifier, err := getCOSEVerifier(key) + if err != nil { + return nil, fmt.Errorf("failed to get COSE verifier: %w", err) + } + + if err = message.Verify(nil, verifier); err != nil { + return nil, fmt.Errorf("failed to verify COSE signature: %w", err) + } + + // Unmarshal the payload + var vp credential.VerifiablePresentation + if err = json.Unmarshal(message.Payload, &vp); err != nil { + return nil, fmt.Errorf("failed to unmarshal VerifiablePresentation: %w", err) + } + + return &vp, nil +} diff --git a/cose/cose_test.go b/cose/cose_test.go new file mode 100644 index 0000000..baae3c4 --- /dev/null +++ b/cose/cose_test.go @@ -0,0 +1,88 @@ +package cose + +import ( + "testing" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/TBD54566975/vc-jose-cose-go/credential" + "github.com/TBD54566975/vc-jose-cose-go/util" +) + +func Test_Sign_Verify_VerifiableCredential(t *testing.T) { + tests := []struct { + name string + curve jwa.EllipticCurveAlgorithm + }{ + {"EC P-256", jwa.P256}, + {"EC P-384", jwa.P384}, + {"EC P-521", jwa.P521}, + {"OKP EdDSA", jwa.Ed25519}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, err := util.GenerateJWK(tt.curve) + require.NoError(t, err) + + vc := credential.VerifiableCredential{ + Context: []string{"https://www.w3.org/2018/credentials/v1"}, + ID: "https://example.edu/credentials/1872", + Type: []string{"VerifiableCredential"}, + Issuer: credential.NewIssuerHolderFromString("did:example:issuer"), + ValidFrom: "2010-01-01T19:23:24Z", + CredentialSubject: map[string]any{ + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + }, + } + + cs1, err := SignVerifiableCredential(vc, key) + require.NoError(t, err) + assert.NotEmpty(t, cs1) + + // Verify the VC + verifiedVC, err := VerifyVerifiableCredential(cs1, key) + require.NoError(t, err) + assert.Equal(t, vc.ID, verifiedVC.ID) + assert.Equal(t, vc.Issuer.ID(), verifiedVC.Issuer.ID()) + }) + } +} + +func Test_Sign_Verify_VerifiablePresentation(t *testing.T) { + tests := []struct { + name string + curve jwa.EllipticCurveAlgorithm + }{ + {"EC P-256", jwa.P256}, + {"EC P-384", jwa.P384}, + {"EC P-521", jwa.P521}, + {"OKP EdDSA", jwa.Ed25519}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, err := util.GenerateJWK(tt.curve) + require.NoError(t, err) + + vp := credential.VerifiablePresentation{ + Context: []string{"https://www.w3.org/2018/credentials/v1"}, + ID: "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5", + Type: []string{"VerifiablePresentation"}, + Holder: credential.NewIssuerHolderFromString("did:example:ebfeb1f712ebc6f1c276e12ec21"), + } + + cs1, err := SignVerifiablePresentation(vp, key) + require.NoError(t, err) + assert.NotEmpty(t, cs1) + + // Verify the VP + verifiedVP, err := VerifyVerifiablePresentation(cs1, key) + require.NoError(t, err) + assert.Equal(t, vp.ID, verifiedVP.ID) + assert.Equal(t, vp.Holder.ID(), verifiedVP.Holder.ID()) + }) + } +} diff --git a/go.mod b/go.mod index 5a3b4b4..72f42fd 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,11 @@ require ( golang.org/x/term v0.25.0 ) +require ( + github.com/fxamacker/cbor/v2 v2.5.0 // indirect + github.com/x448/float16 v0.8.4 // indirect +) + require ( github.com/MichaelFraser99/go-sd-jwt v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.4 @@ -29,6 +34,7 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/segmentio/asm v1.2.0 // indirect + github.com/veraison/go-cose v1.3.0 golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect diff --git a/go.sum b/go.sum index e432725..e658fef 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= +github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -51,6 +53,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7rk= +github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= diff --git a/jose/jose.go b/jose/jose.go index 30b1a6f..937fdad 100644 --- a/jose/jose.go +++ b/jose/jose.go @@ -24,6 +24,9 @@ func SignVerifiableCredential(vc credential.VerifiableCredential, key jwk.Key) ( if vc.IsEmpty() { return nil, errors.New("VerifiableCredential is empty") } + if key == nil { + return nil, errors.New("key is required") + } if key.KeyID() == "" { return nil, errors.New("key ID is required") } @@ -82,6 +85,19 @@ func SignVerifiableCredential(vc credential.VerifiableCredential, key jwk.Key) ( // VerifyVerifiableCredential verifies a VerifiableCredential JWT using the provided key. func VerifyVerifiableCredential(jwt string, key jwk.Key) (*credential.VerifiableCredential, error) { + if jwt == "" { + return nil, errors.New("JWT is required") + } + if key == nil { + return nil, errors.New("key is required") + } + if key.KeyID() == "" { + return nil, errors.New("key ID is required") + } + if key.Algorithm().String() == "" { + return nil, errors.New("key algorithm is required") + } + // Verify the JWT signature and get the payload payload, err := jws.Verify([]byte(jwt), jws.WithKey(key.Algorithm(), key)) if err != nil { @@ -99,7 +115,12 @@ 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) { - var alg jwa.SignatureAlgorithm + if vp.IsEmpty() { + return "", errors.New("VerifiablePresentation is empty") + } + if key == nil { + return "", errors.New("key is required") + } if key.KeyID() == "" { return "", errors.New("key ID is required") } @@ -107,6 +128,7 @@ func SignVerifiablePresentation(vp credential.VerifiablePresentation, key jwk.Ke return "", errors.New("key algorithm is required") } + var alg jwa.SignatureAlgorithm kty := key.KeyType() switch kty { case jwa.EC: @@ -185,6 +207,19 @@ func SignVerifiablePresentation(vp credential.VerifiablePresentation, key jwk.Ke // VerifyVerifiablePresentation verifies a VerifiablePresentation JWT using the provided key. func VerifyVerifiablePresentation(jwt string, key jwk.Key) (*credential.VerifiablePresentation, error) { + if jwt == "" { + return nil, errors.New("JWT is required") + } + if key == nil { + return nil, errors.New("key is required") + } + if key.KeyID() == "" { + return nil, errors.New("key ID is required") + } + if key.Algorithm().String() == "" { + return nil, errors.New("key algorithm is required") + } + // Verify the JWT signature and get the payload payload, err := jws.Verify([]byte(jwt), jws.WithKey(key.Algorithm(), key)) if err != nil { diff --git a/jose/jose_test.go b/jose/jose_test.go index e1f1ead..2066f60 100644 --- a/jose/jose_test.go +++ b/jose/jose_test.go @@ -24,7 +24,7 @@ func Test_Sign_Verify_VerifiableCredential(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - key, err := util.GenerateJWKWithAlgorithm(tt.curve) + key, err := util.GenerateJWK(tt.curve) require.NoError(t, err) vc := credential.VerifiableCredential{ @@ -64,7 +64,7 @@ func Test_Sign_Verify_VerifiablePresentation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - key, err := util.GenerateJWKWithAlgorithm(tt.curve) + key, err := util.GenerateJWK(tt.curve) require.NoError(t, err) vp := credential.VerifiablePresentation{ diff --git a/magefile.go b/magefile.go index 08210dc..0e0caa0 100644 --- a/magefile.go +++ b/magefile.go @@ -27,7 +27,7 @@ const ( // Build builds the library. func Build() error { println("Building...") - return sh.Run(Go, "build", "-tags", "jwx_es256k", "./...") + return sh.Run(Go, "build", "./...") } // Clean deletes any build artifacts. @@ -47,7 +47,6 @@ func runTests(extraTestArgs ...string) error { if mg.Verbose() { args = append(args, "-v") } - args = append(args, "-tags=jwx_es256k") args = append(args, extraTestArgs...) args = append(args, "./...") testEnv := map[string]string{ @@ -202,7 +201,6 @@ func runCITests(extraTestArgs ...string) error { if mg.Verbose() { args = append(args, "-v") } - args = append(args, "-tags=jwx_es256k") args = append(args, "-covermode=atomic") args = append(args, "-coverprofile=coverage.out") args = append(args, "-race") diff --git a/sd-jwt/sd_jwt.go b/sdjwt/sdjwt.go similarity index 86% rename from sd-jwt/sd_jwt.go rename to sdjwt/sdjwt.go index 4a9b404..90f9848 100644 --- a/sd-jwt/sd_jwt.go +++ b/sdjwt/sdjwt.go @@ -2,12 +2,13 @@ package sdjwt import ( "crypto" - "encoding/json" "errors" "fmt" "strconv" "strings" + "github.com/goccy/go-json" + sdjwt "github.com/MichaelFraser99/go-sd-jwt" "github.com/MichaelFraser99/go-sd-jwt/disclosure" "github.com/lestrrat-go/jwx/v2/jwk" @@ -34,6 +35,9 @@ func SignVerifiableCredential(vc credential.VerifiableCredential, disclosurePath if vc.IsEmpty() { return nil, errors.New("VerifiableCredential is empty") } + if key == nil { + return nil, errors.New("key is required") + } if key.KeyID() == "" { return nil, errors.New("key ID is required") } @@ -101,8 +105,8 @@ func SignVerifiableCredential(vc credential.VerifiableCredential, disclosurePath sdJWTParts = append(sdJWTParts, d.EncodedValue) } - sdJwt := fmt.Sprintf("%s~", strings.Join(sdJWTParts, "~")) - return &sdJwt, nil + sdJWT := fmt.Sprintf("%s~", strings.Join(sdJWTParts, "~")) + return &sdJWT, nil } // processDisclosures traverses the credential map and creates disclosures for specified paths @@ -214,19 +218,33 @@ func processPath(data map[string]any, pathParts []string, disclosures *[]disclos if !ok { return fmt.Errorf("field %s is not an object", field) } + return processPath(nextMap, pathParts[1:], disclosures) } // VerifyVerifiableCredential verifies an SD-JWT credential and returns the disclosed claims -func VerifyVerifiableCredential(sdJwtStr string, key jwk.Key) (*credential.VerifiableCredential, error) { +func VerifyVerifiableCredential(sdJWT string, key jwk.Key) (*credential.VerifiableCredential, error) { + if sdJWT == "" { + return nil, errors.New("SD-JWT is required") + } + if key == nil { + return nil, errors.New("key is required") + } + if key.KeyID() == "" { + return nil, errors.New("key ID is required") + } + if key.Algorithm().String() == "" { + return nil, errors.New("key algorithm is required") + } + // Parse and verify the SD-JWT - sdJwt, err := sdjwt.New(sdJwtStr) + sdJWTContainer, err := sdjwt.New(sdJWT) if err != nil { return nil, fmt.Errorf("failed to parse SD-JWT: %w", err) } // Get disclosed claims - claims, err := sdJwt.GetDisclosedClaims() + claims, err := sdJWTContainer.GetDisclosedClaims() if err != nil { return nil, fmt.Errorf("failed to get disclosed claims: %w", err) } @@ -243,7 +261,7 @@ func VerifyVerifiableCredential(sdJwtStr string, key jwk.Key) (*credential.Verif } // Extract signature from SD-JWT - parts := strings.Split(sdJwtStr, "~") + parts := strings.Split(sdJWT, "~") if len(parts) < 1 { return nil, errors.New("invalid SD-JWT format") } @@ -266,12 +284,18 @@ func SignVerifiablePresentation(vp credential.VerifiablePresentation, disclosure if vp.IsEmpty() { return nil, errors.New("VerifiablePresentation is empty") } + if key == nil { + return nil, errors.New("key is required") + } if key.KeyID() == "" { return nil, errors.New("key ID is required") } if key.Algorithm().String() == "" { return nil, errors.New("key algorithm is required") } + if len(disclosurePaths) == 0 { + return nil, errors.New("at least one disclosure path is required") + } // Convert VP to a map for manipulation vpMap, err := vp.ToMap() @@ -327,20 +351,33 @@ func SignVerifiablePresentation(vp credential.VerifiablePresentation, disclosure sdJWTParts = append(sdJWTParts, d.EncodedValue) } - sdJwt := fmt.Sprintf("%s~", strings.Join(sdJWTParts, "~")) - return &sdJwt, nil + sdJWT := fmt.Sprintf("%s~", strings.Join(sdJWTParts, "~")) + return &sdJWT, nil } // VerifyVerifiablePresentation verifies an SD-JWT presentation and returns the disclosed claims -func VerifyVerifiablePresentation(sdJwtStr string, key jwk.Key) (*credential.VerifiablePresentation, error) { +func VerifyVerifiablePresentation(sdJWT string, key jwk.Key) (*credential.VerifiablePresentation, error) { + if sdJWT == "" { + return nil, errors.New("SD-JWT is required") + } + if key == nil { + return nil, errors.New("key is required") + } + if key.KeyID() == "" { + return nil, errors.New("key ID is required") + } + if key.Algorithm().String() == "" { + return nil, errors.New("key algorithm is required") + } + // Parse and verify the SD-JWT - sdJwt, err := sdjwt.New(sdJwtStr) + sdJWTContainer, err := sdjwt.New(sdJWT) if err != nil { return nil, fmt.Errorf("failed to parse SD-JWT: %w", err) } // Get disclosed claims - claims, err := sdJwt.GetDisclosedClaims() + claims, err := sdJWTContainer.GetDisclosedClaims() if err != nil { return nil, fmt.Errorf("failed to get disclosed claims: %w", err) } @@ -357,7 +394,7 @@ func VerifyVerifiablePresentation(sdJwtStr string, key jwk.Key) (*credential.Ver } // Extract signature from SD-JWT - parts := strings.Split(sdJwtStr, "~") + parts := strings.Split(sdJWT, "~") if len(parts) < 1 { return nil, errors.New("invalid SD-JWT format") } diff --git a/sd-jwt/sd_jwt_test.go b/sdjwt/sdjwt_test.go similarity index 94% rename from sd-jwt/sd_jwt_test.go rename to sdjwt/sdjwt_test.go index 89982a9..3a46461 100644 --- a/sd-jwt/sd_jwt_test.go +++ b/sdjwt/sdjwt_test.go @@ -231,16 +231,16 @@ func Test_Sign_Verify_VerifiableCredential(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Generate issuer key - issuerKey, err := util.GenerateJWKWithAlgorithm(tt.curve) + issuerKey, err := util.GenerateJWK(tt.curve) require.NoError(t, err) // Sign the credential - sdJwt, err := SignVerifiableCredential(*tt.vc, tt.disclosurePaths, issuerKey) + sdJWT, err := SignVerifiableCredential(*tt.vc, tt.disclosurePaths, issuerKey) require.NoError(t, err) - require.NotNil(t, sdJwt) + require.NotNil(t, sdJWT) // Verify the credential - verifiedVC, err := VerifyVerifiableCredential(*sdJwt, issuerKey) + verifiedVC, err := VerifyVerifiableCredential(*sdJWT, issuerKey) require.NoError(t, err) require.NotNil(t, verifiedVC) @@ -255,9 +255,9 @@ func Test_Sign_Verify_VerifiableCredential(t *testing.T) { } // Verify validation fails with wrong key - wrongKey, err := util.GenerateJWKWithAlgorithm(tt.curve) + wrongKey, err := util.GenerateJWK(tt.curve) require.NoError(t, err) - _, err = VerifyVerifiableCredential(*sdJwt, wrongKey) + _, err = VerifyVerifiableCredential(*sdJWT, wrongKey) assert.Error(t, err) }) } @@ -351,16 +351,16 @@ func Test_Sign_Verify_VerifiablePresentation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Generate holder key - holderKey, err := util.GenerateJWKWithAlgorithm(tt.curve) + holderKey, err := util.GenerateJWK(tt.curve) require.NoError(t, err) // Sign the presentation - sdJwt, err := SignVerifiablePresentation(*tt.vp, tt.disclosurePaths, holderKey) + sdJWT, err := SignVerifiablePresentation(*tt.vp, tt.disclosurePaths, holderKey) require.NoError(t, err) - require.NotNil(t, sdJwt) + require.NotNil(t, sdJWT) // Verify the presentation - verifiedVP, err := VerifyVerifiablePresentation(*sdJwt, holderKey) + verifiedVP, err := VerifyVerifiablePresentation(*sdJWT, holderKey) require.NoError(t, err) require.NotNil(t, verifiedVP) @@ -375,9 +375,9 @@ func Test_Sign_Verify_VerifiablePresentation(t *testing.T) { } // Verify validation fails with wrong key - wrongKey, err := util.GenerateJWKWithAlgorithm(tt.curve) + wrongKey, err := util.GenerateJWK(tt.curve) require.NoError(t, err) - _, err = VerifyVerifiablePresentation(*sdJwt, wrongKey) + _, err = VerifyVerifiablePresentation(*sdJWT, wrongKey) assert.Error(t, err) }) } diff --git a/util/crypto.go b/util/crypto.go index d7bbaf0..adc9e36 100644 --- a/util/crypto.go +++ b/util/crypto.go @@ -40,7 +40,9 @@ const ( P521 KeyType = "P-521" ) -func GenerateJWKWithAlgorithm(eca jwa.EllipticCurveAlgorithm) (jwk.Key, error) { +// GenerateJWK creates a new JWK key pair for the given elliptic curve algorithm +// The key ID is set to the base64 URL-encoded SHA-256 thumbprint of the key. +func GenerateJWK(eca jwa.EllipticCurveAlgorithm) (jwk.Key, error) { // Generate the key pair _, privKey, err := GenerateKeyByEllipticCurveAlgorithm(eca) if err != nil {