Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bugs in JWT VP handling #440

Merged
merged 5 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.20.6
go-version: 1.20.7

- name: Install Mage
run: go install github.com/magefile/mage
Expand All @@ -38,7 +38,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.20.6
go-version: 1.20.7

- name: Install Mage
run: go install github.com/magefile/mage
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.20.6
go-version: 1.20.7
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ When you're ready you may:

| Requirement | Tested Version | Installation Instructions |
|-------------|----------------|--------------------------------------------------------|
| Go | 1.20.6 | [go.dev](https://go.dev/doc/tutorial/compile-install) |
| Go | 1.20.7 | [go.dev](https://go.dev/doc/tutorial/compile-install) |
| Mage | 1.13.0-6 | [magefile.org](https://magefile.org/) |

### Go
Expand All @@ -23,7 +23,7 @@ You may verify your `go` installation via the terminal:

```
$> go version
go version go1.20.6 darwin/amd64
go version go1.20.7 darwin/amd64
```

If you do not have go, we recommend installing it by:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[![godoc ssi-sdk](https://img.shields.io/badge/godoc-ssi--sdk-blue)](https://pkg.go.dev/github.com/TBD54566975/ssi-sdk)
[![go version 1.20.6](https://img.shields.io/badge/go_version-1.20.6-brightgreen)](https://golang.org/)
[![go version 1.20.7](https://img.shields.io/badge/go_version-1.20.7-brightgreen)](https://golang.org/)
[![Go Report Card A+](https://goreportcard.com/badge/github.com/TBD54566975/ssi-sdk)](https://goreportcard.com/report/github.com/TBD54566975/ssi-sdk)
[![license Apache 2](https://img.shields.io/badge/license-Apache%202-black)](https://github.com/TBD54566975/ssi-sdk/blob/main/LICENSE)
[![issues](https://img.shields.io/github/issues/TBD54566975/ssi-sdk)](https://github.com/TBD54566975/ssi-sdk/issues)
Expand Down
2 changes: 1 addition & 1 deletion credential/exchange/submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func BuildPresentationSubmission(signer any, requester string, def PresentationD
if err != nil {
return nil, errors.Wrap(err, "unable to fulfill presentation definition with given credentials")
}
return integrity.SignVerifiablePresentationJWT(jwtSigner, integrity.JWTVVPParameters{Audience: []string{requester}}, *vpSubmission)
return integrity.SignVerifiablePresentationJWT(jwtSigner, &integrity.JWTVVPParameters{Audience: []string{requester}}, *vpSubmission)
default:
return nil, fmt.Errorf("presentation submission embed target <%s> is not implemented", et)
}
Expand Down
31 changes: 18 additions & 13 deletions credential/integrity/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ type JWTVVPParameters struct {

// SignVerifiablePresentationJWT transforms a VP into a VP JWT and signs it
// According to https://w3c.github.io/vc-jwt/#version-1.1
func SignVerifiablePresentationJWT(signer jwx.Signer, parameters JWTVVPParameters, presentation credential.VerifiablePresentation) ([]byte, error) {
func SignVerifiablePresentationJWT(signer jwx.Signer, parameters *JWTVVPParameters, presentation credential.VerifiablePresentation) ([]byte, error) {
if presentation.IsEmpty() {
return nil, errors.New("presentation cannot be empty")
}
Expand All @@ -219,7 +219,7 @@ func SignVerifiablePresentationJWT(signer jwx.Signer, parameters JWTVVPParameter
// NOTE: according to the JWT encoding rules (https://www.w3.org/TR/vc-data-model/#jwt-encoding) aud is a required
// property; however, aud is not required according to the JWT spec. Requiring audience limits a number of cases
// where JWT-VPs can be used, so we do not enforce this requirement.
if parameters.Audience != nil {
if parameters != nil && parameters.Audience != nil {
if err := t.Set(jwt.AudienceKey, parameters.Audience); err != nil {
return nil, errors.Wrap(err, "setting audience value")
}
Expand All @@ -236,7 +236,7 @@ func SignVerifiablePresentationJWT(signer jwx.Signer, parameters JWTVVPParameter
return nil, errors.Wrap(err, "setting nonce value")
}

if parameters.Expiration > 0 {
if parameters != nil && parameters.Expiration > 0 {
if err := t.Set(jwt.ExpirationKey, parameters.Expiration); err != nil {
return nil, errors.Wrap(err, "setting exp value")
}
Expand All @@ -251,8 +251,11 @@ func SignVerifiablePresentationJWT(signer jwx.Signer, parameters JWTVVPParameter
presentation.ID = ""
}
if presentation.Holder != "" {
if presentation.Holder != signer.ID {
return nil, errors.New("holder must be the same as the signer")
}
if err := t.Set(jwt.IssuerKey, presentation.Holder); err != nil {
return nil, errors.New("setting subject value")
return nil, errors.New("setting iss value")
}
// remove from VP
presentation.Holder = ""
Expand Down Expand Up @@ -296,16 +299,18 @@ func VerifyVerifiablePresentationJWT(ctx context.Context, verifier jwx.Verifier,
return nil, nil, nil, errors.Wrap(err, "parsing VP from JWT")
}

// make sure the audience matches the verifier
audMatch := false
for _, aud := range vpToken.Audience() {
if aud == verifier.ID || aud == verifier.KID {
audMatch = true
break
// make sure the audience matches the verifier, if we have an audience
if len(vpToken.Audience()) != 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this being tested?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a test

audMatch := false
for _, aud := range vpToken.Audience() {
if aud == verifier.ID || aud == verifier.KID {
audMatch = true
break
}
}
if !audMatch {
return nil, nil, nil, errors.Errorf("audience mismatch: expected [%s] or [%s], got %s", verifier.ID, verifier.KID, vpToken.Audience())
}
}
if !audMatch {
return nil, nil, nil, errors.Errorf("audience mismatch: expected [%s] or [%s], got %s", verifier.ID, verifier.KID, vpToken.Audience())
}

// verify signature for each credential in the vp
Expand Down
44 changes: 37 additions & 7 deletions credential/integrity/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,16 @@ func TestVerifiableCredentialJWT(t *testing.T) {

func TestVerifiablePresentationJWT(t *testing.T) {
t.Run("bad audience", func(tt *testing.T) {
signer := getTestVectorKey0Signer(tt)

testPresentation := credential.VerifiablePresentation{
Context: []string{"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/security/suites/jws-2020/v1"},
Type: []string{"VerifiablePresentation"},
Holder: "did:example:123",
Holder: signer.ID,
}

signer := getTestVectorKey0Signer(tt)
signed, err := SignVerifiablePresentationJWT(signer, JWTVVPParameters{Audience: []string{"bad-audience"}}, testPresentation)
signed, err := SignVerifiablePresentationJWT(signer, &JWTVVPParameters{Audience: []string{"bad-audience"}}, testPresentation)
assert.NoError(tt, err)

verifier, err := signer.ToVerifier(signer.ID)
Expand All @@ -117,16 +118,45 @@ func TestVerifiablePresentationJWT(t *testing.T) {
assert.Contains(tt, err.Error(), "audience mismatch")
})

t.Run("no VCs", func(tt *testing.T) {
t.Run("no audience", func(tt *testing.T) {
signer := getTestVectorKey0Signer(tt)

testPresentation := credential.VerifiablePresentation{
Context: []string{"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/security/suites/jws-2020/v1"},
Type: []string{"VerifiablePresentation"},
Holder: "did:example:123",
Holder: signer.ID,
}

signed, err := SignVerifiablePresentationJWT(signer, nil, testPresentation)
assert.NoError(tt, err)

verifier, err := signer.ToVerifier(signer.ID)
assert.NoError(tt, err)

token := string(signed)
err = verifier.Verify(token)
assert.NoError(tt, err)

resolver, err := resolution.NewResolver([]resolution.Resolver{key.Resolver{}}...)
require.NoError(tt, err)
require.NotEmpty(tt, resolver)

_, _, _, err = VerifyVerifiablePresentationJWT(context.Background(), *verifier, resolver, token)
assert.NoError(tt, err)
})

t.Run("no VCs", func(tt *testing.T) {
signer := getTestVectorKey0Signer(tt)
signed, err := SignVerifiablePresentationJWT(signer, JWTVVPParameters{Audience: []string{signer.ID}}, testPresentation)

testPresentation := credential.VerifiablePresentation{
Context: []string{"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/security/suites/jws-2020/v1"},
Type: []string{"VerifiablePresentation"},
Holder: signer.ID,
}

signed, err := SignVerifiablePresentationJWT(signer, &JWTVVPParameters{Audience: []string{signer.ID}}, testPresentation)
assert.NoError(tt, err)

verifier, err := signer.ToVerifier(signer.ID)
Expand Down Expand Up @@ -204,7 +234,7 @@ func TestVerifiablePresentationJWT(t *testing.T) {
// sign the presentation from the subject to the issuer
subjectSigner, err := jwx.NewJWXSigner(subjectDID.String(), subjectKID, subjectPrivKey)
assert.NoError(tt, err)
signed, err := SignVerifiablePresentationJWT(*subjectSigner, JWTVVPParameters{Audience: []string{issuerDID.String()}}, testPresentation)
signed, err := SignVerifiablePresentationJWT(*subjectSigner, &JWTVVPParameters{Audience: []string{issuerDID.String()}}, testPresentation)
assert.NoError(tt, err)

// parse the VP
Expand Down
44 changes: 43 additions & 1 deletion credential/integrity/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func VerifyJWTCredential(ctx context.Context, cred string, r resolution.Resolver
return false, errors.Wrapf(err, "error constructing verifier for credential<%s>", token.JwtID())
}
// verify the signature
if err = credVerifier.Verify(cred); err != nil {
if _, _, _, err = VerifyVerifiableCredentialJWT(*credVerifier, cred); err != nil {
return false, errors.Wrapf(err, "error verifying credential<%s>", token.JwtID())
}
return true, nil
Expand All @@ -110,3 +110,45 @@ func VerifyDataIntegrityCredential(_ context.Context, cred credential.Verifiable

return false, errors.New("not implemented")
}

// VerifyJWTPresentation verifies the signature of a JWT presentation after parsing it to resolve the issuer DID
// The issuer DID is resolution from the provided resolution, and used to find the issuer's public key matching
// the KID in the JWT header.
func VerifyJWTPresentation(ctx context.Context, pres string, r resolution.Resolver) (bool, error) {
if pres == "" {
return false, errors.New("presentation cannot be empty")
}
if r == nil {
return false, errors.New("resolution cannot be empty")
}
headers, token, _, err := ParseVerifiablePresentationFromJWT(pres)
if err != nil {
return false, errors.Wrap(err, "parsing JWT")
}

// get key to verify the presentation with
issuerKID := headers.KeyID()
if issuerKID == "" {
return false, errors.Errorf("missing kid in header of presentation<%s>", token.JwtID())
}
issuerDID, err := r.Resolve(ctx, token.Issuer())
if err != nil {
return false, errors.Wrapf(err, "error getting issuer DID<%s> to verify presentation<%s>", token.Issuer(), token.JwtID())
}
issuerKey, err := did.GetKeyFromVerificationMethod(issuerDID.Document, issuerKID)
if err != nil {
return false, errors.Wrapf(err, "error getting key to verify presentation<%s>", token.JwtID())
}

// construct a verifier
presVerifier, err := jwx.NewJWXVerifier(issuerDID.ID, issuerKID, issuerKey)
if err != nil {
return false, errors.Wrapf(err, "error constructing verifier for presentation<%s>", token.JwtID())
}
// verify the signature
if _, _, _, err = VerifyVerifiablePresentationJWT(ctx, *presVerifier, r, pres); err != nil {
return false, errors.Wrapf(err, "error verifying presentation<%s>", token.JwtID())
}

return true, nil
}
Loading
Loading