diff --git a/auth/api/iam/openid_provider.go b/auth/api/iam/openid_provider.go index 0f296916a9..c74dd675c2 100644 --- a/auth/api/iam/openid_provider.go +++ b/auth/api/iam/openid_provider.go @@ -1,3 +1,21 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package iam import ( diff --git a/auth/api/iam/openid_relyingparty.go b/auth/api/iam/openid_relyingparty.go index 9f56c8bb4b..649e88a55d 100644 --- a/auth/api/iam/openid_relyingparty.go +++ b/auth/api/iam/openid_relyingparty.go @@ -130,6 +130,7 @@ func (r Wrapper) handleSIOPv2AuthzResponse(session *Session, params url.Values) } else { vpJSON, _ := json.Marshal(verifiablePresentationMap) vp, err := vc.ParseVerifiablePresentation(string(vpJSON)) + if err != nil { return OAuth2Error{ Code: InvalidRequest, // TODO: right? diff --git a/auth/api/iam/siopv2.go b/auth/api/iam/siopv2.go index f877c07b4c..e6058307c8 100644 --- a/auth/api/iam/siopv2.go +++ b/auth/api/iam/siopv2.go @@ -1,3 +1,21 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package iam import ( diff --git a/auth/api/iam/siopv2_test.go b/auth/api/iam/siopv2_test.go index 33eca200b7..1db7f9e797 100644 --- a/auth/api/iam/siopv2_test.go +++ b/auth/api/iam/siopv2_test.go @@ -1,3 +1,21 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package iam import ( diff --git a/vcr/credential/util.go b/vcr/credential/util.go new file mode 100644 index 0000000000..12014d2e2e --- /dev/null +++ b/vcr/credential/util.go @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package credential + +import ( + "errors" + "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/go-did/vc" +) + +// ResolveSubjectDID resolves the subject DID from the given credentials. +// It returns an error if: +// - the credentials do not have the same subject DID. +// - the credentials do not have a subject DID. +func ResolveSubjectDID(credentials ...vc.VerifiableCredential) (*did.DID, error) { + var subjectID did.DID + for _, credential := range credentials { + sid, err := credential.SubjectDID() + if err != nil { + return nil, err + } + if !subjectID.Empty() && !subjectID.Equals(*sid) { + return nil, errors.New("not all VCs have the same credentialSubject.id") + } + subjectID = *sid + } + return &subjectID, nil +} + +// VerifyPresenterIsHolder checks if the holder of the VP is the same as the subject of the VCs being presented. +// It returns an error when: +// - the VP does not have a holder. +// - the VP holder is not the same as the subject of the VCs. +// If the check succeeds, it returns nil. +func VerifyPresenterIsHolder(vp vc.VerifiablePresentation) error { + // Check VP signer == VC subject (presenter is holder of VCs) + if vp.Holder == nil { + // Is this even possible? + return errors.New("no holder") + } + credentialSubjectID, err := ResolveSubjectDID(vp.VerifiableCredential...) + if err != nil { + return err + } + if *vp.Holder != credentialSubjectID.URI() { + return errors.New("not all VC credentialSubject.id match VP holder") + } + return nil +} diff --git a/vcr/credential/util_test.go b/vcr/credential/util_test.go new file mode 100644 index 0000000000..ab8242711d --- /dev/null +++ b/vcr/credential/util_test.go @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package credential + +import ( + ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/go-did/vc" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestResolveSubjectDID(t *testing.T) { + did1 := did.MustParseDID("did:test:123") + did2 := did.MustParseDID("did:test:456") + credential1 := vc.VerifiableCredential{ + CredentialSubject: []interface{}{map[string]interface{}{"id": did1}}, + } + credential2 := vc.VerifiableCredential{ + CredentialSubject: []interface{}{map[string]interface{}{"id": did1}}, + } + credential3 := vc.VerifiableCredential{ + CredentialSubject: []interface{}{map[string]interface{}{"id": did2}}, + } + t.Run("all the same", func(t *testing.T) { + actual, err := ResolveSubjectDID(credential1, credential2) + assert.NoError(t, err) + assert.Equal(t, did1, *actual) + }) + t.Run("differ", func(t *testing.T) { + actual, err := ResolveSubjectDID(credential1, credential3) + assert.EqualError(t, err, "not all VCs have the same credentialSubject.id") + assert.Nil(t, actual) + }) + t.Run("no ID", func(t *testing.T) { + actual, err := ResolveSubjectDID(vc.VerifiableCredential{CredentialSubject: []interface{}{map[string]interface{}{}}}) + assert.EqualError(t, err, "unable to get subject DID from VC: credential subjects have no ID") + assert.Nil(t, actual) + }) + t.Run("no credentialSubject", func(t *testing.T) { + actual, err := ResolveSubjectDID(vc.VerifiableCredential{}) + assert.EqualError(t, err, "unable to get subject DID from VC: there must be at least 1 credentialSubject") + assert.Nil(t, actual) + }) + +} + +func TestVerifyPresenterIsHolder(t *testing.T) { + holder, _ := ssi.ParseURI("did:test:123") + t.Run("ok", func(t *testing.T) { + vp := vc.VerifiablePresentation{ + Holder: holder, + VerifiableCredential: []vc.VerifiableCredential{ + { + CredentialSubject: []interface{}{map[string]interface{}{"id": holder}}, + }, + }, + } + err := VerifyPresenterIsHolder(vp) + assert.NoError(t, err) + }) + t.Run("no holder", func(t *testing.T) { + vp := vc.VerifiablePresentation{} + err := VerifyPresenterIsHolder(vp) + assert.EqualError(t, err, "no holder") + }) + t.Run("no VC subject", func(t *testing.T) { + vp := vc.VerifiablePresentation{ + Holder: holder, + VerifiableCredential: []vc.VerifiableCredential{ + {}, + }, + } + err := VerifyPresenterIsHolder(vp) + assert.EqualError(t, err, "unable to get subject DID from VC: there must be at least 1 credentialSubject") + }) + t.Run("no VC subject ID", func(t *testing.T) { + vp := vc.VerifiablePresentation{ + Holder: holder, + VerifiableCredential: []vc.VerifiableCredential{ + { + CredentialSubject: []interface{}{map[string]interface{}{}}, + }, + }, + } + err := VerifyPresenterIsHolder(vp) + assert.EqualError(t, err, "unable to get subject DID from VC: credential subjects have no ID") + }) + t.Run("holder does not equal VC subject ID", func(t *testing.T) { + vp := vc.VerifiablePresentation{ + Holder: holder, + VerifiableCredential: []vc.VerifiableCredential{ + { + CredentialSubject: []interface{}{map[string]interface{}{"id": did.MustParseDID("did:test:456")}}, + }, + }, + } + err := VerifyPresenterIsHolder(vp) + assert.EqualError(t, err, "not all VC credentialSubject.id match VP holder") + }) +}