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")
+ })
+}