Skip to content
Open
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
101 changes: 101 additions & 0 deletions v3/lints/etsi/lint_qcstatem_psd2_roles_valid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* ZLint Copyright 2026 Regents of the University of Michigan
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package etsi

import (
"fmt"

"github.com/zmap/zcrypto/x509"
"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/util"
)

// psd2KnownRoles is the full set of role OIDs ETSI TS 119 495 Annex A
// currently defines, verified byte-for-byte against the current edition
// (V1.8.1), keyed by the OID's dotted string form. Roles whose OID isn't
// in this table are intentionally not validated — see GEN-5.2.2-4 in the
// block comment below.
var psd2KnownRoles = map[string]string{
"0.4.0.19495.1.0": "Unspecified",
"0.4.0.19495.1.1": "PSP_AS",
"0.4.0.19495.1.2": "PSP_PI",
"0.4.0.19495.1.3": "PSP_AI",
"0.4.0.19495.1.4": "PSP_IC",
"0.4.0.19495.1.5": "PSP_CB",
"0.4.0.19495.1.6": "PSP_PA",
"0.4.0.19495.1.51": "VOP_RS",
"0.4.0.19495.1.52": "VOP_VS",
}

type qcStatemPsd2RolesValid struct{}

// ETSI TS 119 495 V1.1.2 (2018-07), Section 5.2.2:
//
// REG-5.2.2-5: The TSP shall ensure that the name in roleOfPspName is
// the one associated with the role object identifier held in
// roleOfPspOid.
//
// GEN-5.2.2-4: For any other role the role object identifier and role
// name should be defined and registered by an organization recognized
// at the European or national level.
func init() {
lint.RegisterCertificateLint(&lint.CertificateLint{
LintMetadata: lint.LintMetadata{
Name: "e_qcstatem_psd2_roles_valid",
Description: "Checks that, for each role in a PSD2 QcStatement's RolesOfPSP whose OID is one of ETSI's registered role OIDs, the role name exactly matches that OID; roles with an unrecognized OID are not checked",
Citation: "ETSI TS 119 495 V1.1.2 (2018-07), Section 5.2.2, REG-5.2.2-5",
Source: lint.EtsiEsi,
EffectiveDate: util.EtsiTs119495_V1_1_2_Date,
},
Lint: NewQcStatemPsd2RolesValid,
})
}

func NewQcStatemPsd2RolesValid() lint.LintInterface {
return &qcStatemPsd2RolesValid{}
}

func (l *qcStatemPsd2RolesValid) CheckApplies(c *x509.Certificate) bool {
if !util.IsExtInCert(c, util.QcStateOid) {
return false
}
return util.ParseQcStatem(util.GetExtFromCert(c, util.QcStateOid).Value, util.IdEtsiPsd2Statem).IsPresent()
}

func (l *qcStatemPsd2RolesValid) Execute(c *x509.Certificate) *lint.LintResult {
ext := util.GetExtFromCert(c, util.QcStateOid)
s := util.ParseQcStatem(ext.Value, util.IdEtsiPsd2Statem)
if s.GetErrorInfo() != "" {
return &lint.LintResult{Status: lint.Error, Details: s.GetErrorInfo()}
}
psd2, ok := s.(util.EtsiPsd2)
if !ok {
return &lint.LintResult{Status: lint.Fatal, Details: "parsed QC statement is not of type EtsiPsd2"}
}

for _, role := range psd2.Decoded.RolesOfPSP {
oid := role.RoleOfPspOid.String()
name, ok := psd2KnownRoles[oid]
if !ok {
continue
}
if role.RoleOfPspName != name {
return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf(
"role name %q does not match the expected name %q for role OID %s",
role.RoleOfPspName, name, oid)}
}
}
return &lint.LintResult{Status: lint.Pass}
}
45 changes: 45 additions & 0 deletions v3/lints/etsi/lint_qcstatem_psd2_roles_valid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package etsi

/*
* ZLint Copyright 2026 Regents of the University of Michigan
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import (
"testing"

"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/test"
)

func TestEtsiQcStatemPsd2RolesValid(t *testing.T) {
cases := map[string]struct {
status lint.LintStatus
details string // empty means "don't check Details"
}{
"QcStmtEtsiPsd2ValidCert01.pem": {status: lint.Pass},
"QcStmtEtsiPsd2RoleNameMismatchCert01.pem": {status: lint.Error, details: "role name \"PSP_PI\" does not match the expected name \"PSP_AS\" for role OID 0.4.0.19495.1.1"},
"QcStmtEtsiPsd2RoleUnrecognizedOidCert01.pem": {status: lint.Pass},
"QcStmtEtsiPsd2RoleUnspecifiedCert01.pem": {status: lint.Pass},
"QcStmtEtsiPsd2WrongEncodingCert01.pem": {status: lint.Error, details: "error with ASN.1 encoding, possibly a wrong ASN.1 string type was used"},
"QcStmtEtsiValidCert11.pem": {status: lint.NA},
}
for inputPath, tc := range cases {
out := test.TestLint("e_qcstatem_psd2_roles_valid", inputPath)
if out.Status != tc.status {
t.Errorf("%s: expected %s, got %s", inputPath, tc.status, out.Status)
}
if tc.details != "" && out.Details != tc.details {
t.Errorf("%s: expected details %q, got %q", inputPath, tc.details, out.Details)
}
}
}
42 changes: 42 additions & 0 deletions v3/testdata/QcStmtEtsiPsd2RoleNameMismatchCert01.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 2 (0x2)
Signature Algorithm: ecdsa-with-SHA256
Issuer:
Validity
Not Before: Jan 1 00:00:00 2020 GMT
Not After : Jan 1 00:00:00 2030 GMT
Subject: CN = PSD2 Role Name Test Leaf
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:5c:10:22:a2:a8:f9:8f:1e:71:73:8d:b0:85:57:
89:40:26:7d:5d:f6:f6:98:6d:cc:10:30:a9:61:7b:
1d:5e:de:fa:8f:c1:ee:0e:3b:20:23:60:f6:86:1f:
c9:c1:3f:51:23:cb:e0:d4:f0:cf:d1:9c:ea:12:6c:
34:39:a9:96:47
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
qcStatements:
0;09......'.0/0.0.......'....PSP_PI..Banco de Espa..a..ES-BDE
Signature Algorithm: ecdsa-with-SHA256
Signature Value:
30:45:02:21:00:e7:3a:eb:0f:04:29:98:f9:11:33:45:17:3d:
74:66:57:3a:18:77:0b:0e:ef:b2:7e:76:ee:5a:38:b4:80:c3:
dd:02:20:44:bc:54:c1:b1:f5:ee:b5:6b:67:64:e5:ef:05:fb:
83:dd:67:34:c3:0d:bf:c3:d4:d7:e3:c5:59:6d:d0:95:0c
-----BEGIN CERTIFICATE-----
MIIBbTCCAROgAwIBAgIBAjAKBggqhkjOPQQDAjAAMB4XDTIwMDEwMTAwMDAwMFoX
DTMwMDEwMTAwMDAwMFowIzEhMB8GA1UEAxMYUFNEMiBSb2xlIE5hbWUgVGVzdCBM
ZWFmMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXBAioqj5jx5xc42whVeJQCZ9
Xfb2mG3MEDCpYXsdXt76j8HuDjsgI2D2hh/JwT9RI8vg1PDP0ZzqEmw0OamWR6Nb
MFkwDAYDVR0TAQH/BAIwADBJBggrBgEFBQcBAwQ9MDswOQYGBACBmCcCMC8wEzAR
BgcEAIGYJwEBDAZQU1BfUEkMEEJhbmNvIGRlIEVzcGHDsWEMBkVTLUJERTAKBggq
hkjOPQQDAgNIADBFAiEA5zrrDwQpmPkRM0UXPXRmVzoYdwsO77J+du5aOLSAw90C
IES8VMGx9e61a2dk5e8F+4PdZzTDDb/D1NfjxVlt0JUM
-----END CERTIFICATE-----
42 changes: 42 additions & 0 deletions v3/testdata/QcStmtEtsiPsd2RoleUnrecognizedOidCert01.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 3 (0x3)
Signature Algorithm: ecdsa-with-SHA256
Issuer:
Validity
Not Before: Jan 1 00:00:00 2020 GMT
Not After : Jan 1 00:00:00 2030 GMT
Subject: CN = PSD2 Role Name Test Leaf
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:86:76:a7:f2:1e:b0:30:ed:ef:69:3b:85:fc:12:
10:e9:59:cb:fb:eb:04:45:5d:72:45:bd:0a:43:95:
17:2d:9f:0a:d8:bf:df:bc:f4:96:f7:8e:b9:28:95:
42:4b:0d:46:fa:00:bb:e5:ab:56:36:6b:35:e8:94:
17:bb:47:8a:5e
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
qcStatements:
0A0?......'.050.0...*....SomeOtherOrgRole..Banco de Espa..a..ES-BDE
Signature Algorithm: ecdsa-with-SHA256
Signature Value:
30:45:02:20:41:ef:cd:34:2c:33:4b:1e:2e:c5:2a:6a:40:23:
fa:df:42:f7:71:c5:50:96:0b:cf:03:e9:fd:1b:52:5e:d2:28:
02:21:00:9e:2c:f9:5e:e6:02:09:9c:02:55:34:e1:2f:6c:a4:
18:42:91:e1:32:a7:bb:2f:a6:a9:5f:19:f8:49:11:90:dd
-----BEGIN CERTIFICATE-----
MIIBczCCARmgAwIBAgIBAzAKBggqhkjOPQQDAjAAMB4XDTIwMDEwMTAwMDAwMFoX
DTMwMDEwMTAwMDAwMFowIzEhMB8GA1UEAxMYUFNEMiBSb2xlIE5hbWUgVGVzdCBM
ZWFmMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhnan8h6wMO3vaTuF/BIQ6VnL
++sERV1yRb0KQ5UXLZ8K2L/fvPSW9465KJVCSw1G+gC75atWNms16JQXu0eKXqNh
MF8wDAYDVR0TAQH/BAIwADBPBggrBgEFBQcBAwRDMEEwPwYGBACBmCcCMDUwGTAX
BgMqAwQMEFNvbWVPdGhlck9yZ1JvbGUMEEJhbmNvIGRlIEVzcGHDsWEMBkVTLUJE
RTAKBggqhkjOPQQDAgNIADBFAiBB7800LDNLHi7FKmpAI/rfQvdxxVCWC88D6f0b
Ul7SKAIhAJ4s+V7mAgmcAlU04S9spBhCkeEyp7svpqlfGfhJEZDd
-----END CERTIFICATE-----
42 changes: 42 additions & 0 deletions v3/testdata/QcStmtEtsiPsd2RoleUnspecifiedCert01.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4 (0x4)
Signature Algorithm: ecdsa-with-SHA256
Issuer:
Validity
Not Before: Jan 1 00:00:00 2020 GMT
Not After : Jan 1 00:00:00 2030 GMT
Subject: CN = PSD2 Role Name Test Leaf
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:a6:5a:e5:48:96:c7:8a:26:1f:e8:84:9c:bd:56:
85:a1:dd:0e:06:bb:42:c8:a9:72:8f:15:10:0f:a4:
ab:e3:21:96:b2:a9:d5:a9:44:df:2d:ce:ec:cf:b2:
b1:a3:19:05:a9:31:eb:af:2a:4d:bb:bd:8f:38:c1:
df:23:81:07:47
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
qcStatements:
0@0>......'.040.0.......'....Unspecified..Banco de Espa..a..ES-BDE
Signature Algorithm: ecdsa-with-SHA256
Signature Value:
30:46:02:21:00:a0:5d:3d:1b:70:ef:90:4f:5c:14:93:8a:a7:
8f:10:39:4a:65:ad:db:05:ec:ab:8b:e6:10:65:41:5b:41:7b:
d6:02:21:00:95:76:d2:4a:ad:d4:22:05:7d:36:33:0e:64:dc:
8c:10:72:2e:b8:66:cc:37:92:d4:9c:32:a5:f4:15:51:36:a0
-----BEGIN CERTIFICATE-----
MIIBczCCARigAwIBAgIBBDAKBggqhkjOPQQDAjAAMB4XDTIwMDEwMTAwMDAwMFoX
DTMwMDEwMTAwMDAwMFowIzEhMB8GA1UEAxMYUFNEMiBSb2xlIE5hbWUgVGVzdCBM
ZWFmMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEplrlSJbHiiYf6IScvVaFod0O
BrtCyKlyjxUQD6Sr4yGWsqnVqUTfLc7sz7KxoxkFqTHrrypNu72POMHfI4EHR6Ng
MF4wDAYDVR0TAQH/BAIwADBOBggrBgEFBQcBAwRCMEAwPgYGBACBmCcCMDQwGDAW
BgcEAIGYJwEADAtVbnNwZWNpZmllZAwQQmFuY28gZGUgRXNwYcOxYQwGRVMtQkRF
MAoGCCqGSM49BAMCA0kAMEYCIQCgXT0bcO+QT1wUk4qnjxA5SmWt2wXsq4vmEGVB
W0F71gIhAJV20kqt1CIFfTYzDmTcjBByLrhmzDeS1JwypfQVUTag
-----END CERTIFICATE-----
Loading