From 10e72b8521de682eff734b3e348599468873996d Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sat, 8 Aug 2026 08:44:55 -0700 Subject: [PATCH 01/12] util: add PSD2 QCStatement (ETSI TS 119 495) structural parsing --- v3/util/oid.go | 1 + v3/util/qc_stmt.go | 27 ++++++++ v3/util/qc_stmt_test.go | 140 ++++++++++++++++++++++++++++++++++++++++ v3/util/time.go | 1 + 4 files changed, 169 insertions(+) create mode 100644 v3/util/qc_stmt_test.go diff --git a/v3/util/oid.go b/v3/util/oid.go index 9eef942dc..95d4c0da7 100644 --- a/v3/util/oid.go +++ b/v3/util/oid.go @@ -127,6 +127,7 @@ var ( QEVCPwPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 4} QNCPwPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 5} QNCPwgenPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 6} + IdEtsiPsd2Statem = asn1.ObjectIdentifier{0, 4, 0, 19495, 2} // ETSI TS 119 495 V1.1.2, Annex A: id-etsi-psd2-qcStatement ) const ( diff --git a/v3/util/qc_stmt.go b/v3/util/qc_stmt.go index 28f371775..07533d74c 100644 --- a/v3/util/qc_stmt.go +++ b/v3/util/qc_stmt.go @@ -99,6 +99,22 @@ type EtsiQcPds struct { PdsLocations []PdsLocation } +type RoleOfPSP struct { + RoleOfPspOid asn1.ObjectIdentifier + RoleOfPspName string `asn1:"utf8"` +} + +type PSD2QcType struct { + RolesOfPSP []RoleOfPSP + NCAName string `asn1:"utf8"` + NCAId string `asn1:"utf8"` +} + +type EtsiPsd2 struct { + etsiBase + Decoded PSD2QcType +} + func AppendToStringSemicolonDelim(this *string, s string) { if len(*this) > 0 && len(s) > 0 { (*this) += "; " @@ -244,6 +260,17 @@ func ParseQcStatem(extVal []byte, sought asn1.ObjectIdentifier) EtsiQcStmtIf { return etsiBase{errorInfo: "error parsing IdEtsiQcsQcType extension statementInfo field", isPresent: true} } return qcType + } else if statem.Oid.Equal(IdEtsiPsd2Statem) { + etsiObj := EtsiPsd2{etsiBase: etsiBase{isPresent: true}} + rest, err := asn1.Unmarshal(statem.Any.FullBytes, &etsiObj.Decoded) + if len(rest) != 0 || err != nil { + etsiObj.errorInfo = "error parsing the statementInfo field" + } else { + AppendToStringSemicolonDelim(&etsiObj.errorInfo, + checkAsn1Reencoding(reflect.ValueOf(etsiObj.Decoded).Interface(), statem.Any.FullBytes, + "error with ASN.1 encoding, possibly a wrong ASN.1 string type was used")) + } + return etsiObj } else { return etsiBase{errorInfo: "", isPresent: true} } diff --git a/v3/util/qc_stmt_test.go b/v3/util/qc_stmt_test.go new file mode 100644 index 000000000..500f5e59e --- /dev/null +++ b/v3/util/qc_stmt_test.go @@ -0,0 +1,140 @@ +package util + +/* + * 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/zcrypto/encoding/asn1" +) + +// test-only mirrors of the PSD2 ASN.1 types. testPSD2QcTypeMalformed +// deliberately tags NCAName as PrintableString instead of UTF8String to +// produce a statement that parses but does not round-trip to the same +// bytes the real UTF8String-tagged PSD2QcType would produce. +type testRoleOfPSP struct { + RoleOfPspOid asn1.ObjectIdentifier + RoleOfPspName string `asn1:"utf8"` +} + +type testPSD2QcTypeValid struct { + RolesOfPSP []testRoleOfPSP + NCAName string `asn1:"utf8"` + NCAId string `asn1:"utf8"` +} + +type testPSD2QcTypeMalformed struct { + RolesOfPSP []testRoleOfPSP + NCAName string `asn1:"printable"` + NCAId string `asn1:"utf8"` +} + +type testQcStatement struct { + Oid asn1.ObjectIdentifier + Info asn1.RawValue +} + +// buildPsd2ExtValue wraps already-encoded PSD2QcType bytes into a +// one-statement QcStatements extension value, i.e. what CheckApplies / +// ParseQcStatem expect to receive as extVal. +func buildPsd2ExtValue(t *testing.T, psd2Bytes []byte) []byte { + t.Helper() + stmt := testQcStatement{ + Oid: IdEtsiPsd2Statem, + Info: asn1.RawValue{FullBytes: psd2Bytes}, + } + extVal, err := asn1.Marshal([]testQcStatement{stmt}) + if err != nil { + t.Fatalf("failed to marshal QcStatements extension value: %v", err) + } + return extVal +} + +func TestParseQcStatemPsd2Valid(t *testing.T) { + psd2 := testPSD2QcTypeValid{ + RolesOfPSP: []testRoleOfPSP{ + {RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"}, + }, + NCAName: "Banco de España", + NCAId: "ES-BDE", + } + psd2Bytes, err := asn1.Marshal(psd2) + if err != nil { + t.Fatalf("failed to marshal PSD2QcType: %v", err) + } + extVal := buildPsd2ExtValue(t, psd2Bytes) + + result := ParseQcStatem(extVal, IdEtsiPsd2Statem) + if !result.IsPresent() { + t.Fatalf("expected PSD2 QC statement to be present") + } + if result.GetErrorInfo() != "" { + t.Fatalf("expected no error info for valid PSD2 QC statement, got: %q", result.GetErrorInfo()) + } + psd2Result, ok := result.(EtsiPsd2) + if !ok { + t.Fatalf("expected result to be of type EtsiPsd2, got %T", result) + } + if len(psd2Result.Decoded.RolesOfPSP) != 1 { + t.Fatalf("expected 1 role, got %d", len(psd2Result.Decoded.RolesOfPSP)) + } + if psd2Result.Decoded.NCAId != "ES-BDE" { + t.Fatalf("expected NCAId %q, got %q", "ES-BDE", psd2Result.Decoded.NCAId) + } +} + +func TestParseQcStatemPsd2MalformedEncoding(t *testing.T) { + psd2 := testPSD2QcTypeMalformed{ + RolesOfPSP: []testRoleOfPSP{ + {RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"}, + }, + NCAName: "Banco de Espana", + NCAId: "ES-BDE", + } + psd2Bytes, err := asn1.Marshal(psd2) + if err != nil { + t.Fatalf("failed to marshal malformed PSD2QcType: %v", err) + } + extVal := buildPsd2ExtValue(t, psd2Bytes) + + result := ParseQcStatem(extVal, IdEtsiPsd2Statem) + if result.GetErrorInfo() == "" { + t.Fatalf("expected error info for PSD2 QC statement with wrong string type encoding, got none") + } +} + +func TestParseQcStatemPsd2NotPresent(t *testing.T) { + extVal := buildPsd2ExtValue(t, mustMarshal(t, testPSD2QcTypeValid{ + RolesOfPSP: []testRoleOfPSP{{RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"}}, + NCAName: "Banco de España", + NCAId: "ES-BDE", + })) + // Ask for a different (unrelated, but already-registered) statement OID: + // the PSD2 statement is present in extVal but we're not asking about it, + // so IsPresent() must be false. + result := ParseQcStatem(extVal, IdEtsiQcsQcCompliance) + if result.IsPresent() { + t.Fatalf("expected IdEtsiQcsQcCompliance to be absent from an extension containing only a PSD2 statement") + } +} + +func mustMarshal(t *testing.T, v interface{}) []byte { + t.Helper() + b, err := asn1.Marshal(v) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + return b +} diff --git a/v3/util/time.go b/v3/util/time.go index 5720178fc..3cc0ead63 100644 --- a/v3/util/time.go +++ b/v3/util/time.go @@ -69,6 +69,7 @@ var ( EtsiEn319_412_5_V2_6_0_Date = time.Date(2026, time.February, 1, 0, 0, 0, 0, time.UTC) EtsiEn319_412_5_V2_6_1_Date = time.Date(2026, time.May, 1, 0, 0, 0, 0, time.UTC) EtsiEn319_411_2_V2_5_0_Date = time.Date(2023, time.July, 1, 0, 0, 0, 0, time.UTC) + EtsiTS119495_V1_1_2_Date = time.Date(2018, time.July, 1, 0, 0, 0, 0, time.UTC) OnionOnlyEVDate = time.Date(2015, time.May, 1, 0, 0, 0, 0, time.UTC) CABV201Date = time.Date(2017, time.July, 28, 0, 0, 0, 0, time.UTC) AppleCTPolicyDate = time.Date(2018, time.October, 15, 0, 0, 0, 0, time.UTC) From d5888f070b2035bf64809c8fa0bde7b215133b3d Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sat, 8 Aug 2026 08:49:31 -0700 Subject: [PATCH 02/12] testdata: add PSD2 QCStatement valid/malformed-encoding fixtures --- v3/testdata/QcStmtEtsiPsd2ValidCert01.pem | 42 +++++++++++++++++++ .../QcStmtEtsiPsd2WrongEncodingCert01.pem | 42 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 v3/testdata/QcStmtEtsiPsd2ValidCert01.pem create mode 100644 v3/testdata/QcStmtEtsiPsd2WrongEncodingCert01.pem diff --git a/v3/testdata/QcStmtEtsiPsd2ValidCert01.pem b/v3/testdata/QcStmtEtsiPsd2ValidCert01.pem new file mode 100644 index 000000000..ee74778ac --- /dev/null +++ b/v3/testdata/QcStmtEtsiPsd2ValidCert01.pem @@ -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 Test Leaf + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:4b:bf:a0:29:15:25:63:9d:0d:30:fe:c1:82:16: + 4a:5d:15:18:15:0f:b2:89:1a:a9:c8:da:77:97:2f: + 4d:9c:b3:b3:66:76:64:1f:3f:ff:af:ac:55:2a:13: + 9c:5d:7e:b8:9c:83:f2:74:b4:d3:5d:c3:cd:95:3b: + 50:94:2f:15:34 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + qcStatements: + 0;09......'.0/0.0.......'....PSP_AS..Banco de Espa..a..ES-BDE + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:44:02:20:59:51:89:da:a6:4a:92:de:c6:e3:9e:a1:3e:0b: + 8e:35:c9:65:74:48:93:b4:e5:36:01:a7:e4:c0:a6:09:a8:7a: + 02:20:23:c2:7e:00:dc:80:c6:93:b8:15:98:42:4a:18:6f:b1: + 18:44:ef:77:08:de:b8:c7:f3:78:30:24:dc:e6:da:86 +-----BEGIN CERTIFICATE----- +MIIBYjCCAQmgAwIBAgIBAjAKBggqhkjOPQQDAjAAMB4XDTIwMDEwMTAwMDAwMFoX +DTMwMDEwMTAwMDAwMFowGTEXMBUGA1UEAxMOUFNEMiBUZXN0IExlYWYwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAARLv6ApFSVjnQ0w/sGCFkpdFRgVD7KJGqnI2neX +L02cs7NmdmQfP/+vrFUqE5xdfricg/J0tNNdw82VO1CULxU0o1swWTAMBgNVHRMB +Af8EAjAAMEkGCCsGAQUFBwEDBD0wOzA5BgYEAIGYJwIwLzATMBEGBwQAgZgnAQEM +BlBTUF9BUwwQQmFuY28gZGUgRXNwYcOxYQwGRVMtQkRFMAoGCCqGSM49BAMCA0cA +MEQCIFlRidqmSpLexuOeoT4LjjXJZXRIk7TlNgGn5MCmCah6AiAjwn4A3IDGk7gV +mEJKGG+xGETvdwjeuMfzeDAk3Obahg== +-----END CERTIFICATE----- diff --git a/v3/testdata/QcStmtEtsiPsd2WrongEncodingCert01.pem b/v3/testdata/QcStmtEtsiPsd2WrongEncodingCert01.pem new file mode 100644 index 000000000..b0b034f82 --- /dev/null +++ b/v3/testdata/QcStmtEtsiPsd2WrongEncodingCert01.pem @@ -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 Test Leaf + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:37:14:76:df:e7:27:f2:6c:83:c1:3a:60:10:74: + 60:2c:96:6c:70:97:55:4d:e8:4e:e8:09:58:0d:a4: + 67:93:ba:f4:ba:b2:96:08:37:d9:84:95:9e:f7:64: + 76:86:89:47:a2:25:54:6d:5c:76:d7:48:1c:bc:6d: + d8:02:fb:49:1c + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + qcStatements: + 0:08......'.0.0.0.......'....PSP_AS..Banco de Espana..ES-BDE + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:45:02:20:05:e4:aa:35:90:c8:4a:84:ab:4f:7e:cc:f5:32: + 50:aa:6a:84:cb:81:55:65:1c:7d:6a:3d:b3:74:0e:08:c5:1b: + 02:21:00:8d:d6:95:fa:38:73:eb:0c:ae:0b:af:7c:12:f3:72: + e2:73:24:8f:af:2b:7a:29:ca:e3:72:3e:bf:e5:10:fe:d5 +-----BEGIN CERTIFICATE----- +MIIBYjCCAQigAwIBAgIBAzAKBggqhkjOPQQDAjAAMB4XDTIwMDEwMTAwMDAwMFoX +DTMwMDEwMTAwMDAwMFowGTEXMBUGA1UEAxMOUFNEMiBUZXN0IExlYWYwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAAQ3FHbf5yfybIPBOmAQdGAslmxwl1VN6E7oCVgN +pGeTuvS6spYIN9mElZ73ZHaGiUeiJVRtXHbXSBy8bdgC+0kco1owWDAMBgNVHRMB +Af8EAjAAMEgGCCsGAQUFBwEDBDwwOjA4BgYEAIGYJwIwLjATMBEGBwQAgZgnAQEM +BlBTUF9BUxMPQmFuY28gZGUgRXNwYW5hDAZFUy1CREUwCgYIKoZIzj0EAwIDSAAw +RQIgBeSqNZDISoSrT37M9TJQqmqEy4FVZRx9aj2zdA4IxRsCIQCN1pX6OHPrDK4L +r3wS83LicySPryt6Kcrjcj6/5RD+1Q== +-----END CERTIFICATE----- From 20556edee94bb834e297cc25446b4408aaa3c6a3 Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sat, 8 Aug 2026 08:53:16 -0700 Subject: [PATCH 03/12] lints/etsi: add e_qcstatem_psd2_valid --- v3/lints/etsi/lint_qcstatem_psd2_valid.go | 56 +++++++++++++++++++ .../etsi/lint_qcstatem_psd2_valid_test.go | 36 ++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 v3/lints/etsi/lint_qcstatem_psd2_valid.go create mode 100644 v3/lints/etsi/lint_qcstatem_psd2_valid_test.go diff --git a/v3/lints/etsi/lint_qcstatem_psd2_valid.go b/v3/lints/etsi/lint_qcstatem_psd2_valid.go new file mode 100644 index 000000000..9467dd55b --- /dev/null +++ b/v3/lints/etsi/lint_qcstatem_psd2_valid.go @@ -0,0 +1,56 @@ +/* + * 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 ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type qcStatemPsd2Valid struct{} + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_qcstatem_psd2_valid", + Description: "Checks that a QC Statement of the type id-etsi-psd2-qcStatement has the correct ASN.1 encoding", + Citation: "ETSI TS 119 495 V1.1.2 (2018-07), Annex A (normative): ASN.1 Declaration", + Source: lint.EtsiEsi, + EffectiveDate: util.EtsiTS119495_V1_1_2_Date, + }, + Lint: NewQcStatemPsd2Valid, + }) +} + +func NewQcStatemPsd2Valid() lint.LintInterface { + return &qcStatemPsd2Valid{} +} + +func (l *qcStatemPsd2Valid) 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 *qcStatemPsd2Valid) 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()} + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/v3/lints/etsi/lint_qcstatem_psd2_valid_test.go b/v3/lints/etsi/lint_qcstatem_psd2_valid_test.go new file mode 100644 index 000000000..8fbc02a08 --- /dev/null +++ b/v3/lints/etsi/lint_qcstatem_psd2_valid_test.go @@ -0,0 +1,36 @@ +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 TestEtsiQcStatemPsd2Valid(t *testing.T) { + m := map[string]lint.LintStatus{ + "QcStmtEtsiPsd2ValidCert01.pem": lint.Pass, + "QcStmtEtsiPsd2WrongEncodingCert01.pem": lint.Error, + "QcStmtEtsiValidCert11.pem": lint.NA, + } + for inputPath, expected := range m { + out := test.TestLint("e_qcstatem_psd2_valid", inputPath) + if out.Status != expected { + t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) + } + } +} From ed71764503602cd919b7fcc930113960163a6f97 Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sat, 8 Aug 2026 09:15:36 -0700 Subject: [PATCH 04/12] fix: tighten PSD2 test assertions and naming per final review - Assert the exact Details string for the Error case in TestEtsiQcStatemPsd2Valid so the test still catches a regression in the ASN.1 re-encoding check, not just a change in lint status. - Add TestParseQcStatemPsd2UnmarshalFailure to cover the previously untested "asn1.Unmarshal fails outright" branch of the PSD2 case in ParseQcStatem, using truncated statementInfo bytes. - Rename EtsiTS119495_V1_1_2_Date to EtsiTs119495_V1_1_2_Date to match the mixed-case naming convention used by sibling ETSI date constants. --- v3/lints/etsi/lint_qcstatem_psd2_valid.go | 2 +- .../etsi/lint_qcstatem_psd2_valid_test.go | 6 ++++ v3/util/qc_stmt_test.go | 35 +++++++++++++++++++ v3/util/time.go | 2 +- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/v3/lints/etsi/lint_qcstatem_psd2_valid.go b/v3/lints/etsi/lint_qcstatem_psd2_valid.go index 9467dd55b..fcc051669 100644 --- a/v3/lints/etsi/lint_qcstatem_psd2_valid.go +++ b/v3/lints/etsi/lint_qcstatem_psd2_valid.go @@ -29,7 +29,7 @@ func init() { Description: "Checks that a QC Statement of the type id-etsi-psd2-qcStatement has the correct ASN.1 encoding", Citation: "ETSI TS 119 495 V1.1.2 (2018-07), Annex A (normative): ASN.1 Declaration", Source: lint.EtsiEsi, - EffectiveDate: util.EtsiTS119495_V1_1_2_Date, + EffectiveDate: util.EtsiTs119495_V1_1_2_Date, }, Lint: NewQcStatemPsd2Valid, }) diff --git a/v3/lints/etsi/lint_qcstatem_psd2_valid_test.go b/v3/lints/etsi/lint_qcstatem_psd2_valid_test.go index 8fbc02a08..a48d8966b 100644 --- a/v3/lints/etsi/lint_qcstatem_psd2_valid_test.go +++ b/v3/lints/etsi/lint_qcstatem_psd2_valid_test.go @@ -33,4 +33,10 @@ func TestEtsiQcStatemPsd2Valid(t *testing.T) { t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) } } + + const wrongEncodingDetails = "error with ASN.1 encoding, possibly a wrong ASN.1 string type was used" + out := test.TestLint("e_qcstatem_psd2_valid", "QcStmtEtsiPsd2WrongEncodingCert01.pem") + if out.Details != wrongEncodingDetails { + t.Errorf("QcStmtEtsiPsd2WrongEncodingCert01.pem: expected details %q, got %q", wrongEncodingDetails, out.Details) + } } diff --git a/v3/util/qc_stmt_test.go b/v3/util/qc_stmt_test.go index 500f5e59e..079a5d942 100644 --- a/v3/util/qc_stmt_test.go +++ b/v3/util/qc_stmt_test.go @@ -115,6 +115,41 @@ func TestParseQcStatemPsd2MalformedEncoding(t *testing.T) { } } +func TestParseQcStatemPsd2UnmarshalFailure(t *testing.T) { + psd2 := testPSD2QcTypeValid{ + RolesOfPSP: []testRoleOfPSP{ + {RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"}, + }, + NCAName: "Banco de España", + NCAId: "ES-BDE", + } + psd2Bytes, err := asn1.Marshal(psd2) + if err != nil { + t.Fatalf("failed to marshal PSD2QcType: %v", err) + } + // Truncate the otherwise-valid, already-marshaled PSD2QcType bytes by one + // byte so the outer SEQUENCE's declared length no longer matches the + // available content. Note: appending extra trailing bytes instead does + // NOT work here, because zcrypto's asn1 fork tolerates unconsumed + // trailing content within a declared SEQUENCE length, and any bytes + // appended past the declared length get silently dropped when the + // RawValue capturing statem.Any.FullBytes reads exactly one TLV in + // buildPsd2ExtValue's wrapping. Truncation instead makes the declared + // length exceed the available bytes, which reliably makes + // asn1.Unmarshal fail with "data truncated" -- exercising the + // "error parsing the statementInfo field" branch of ParseQcStatem + // (asn1.Unmarshal(statem.Any.FullBytes, &etsiObj.Decoded) failing + // outright), as opposed to the checkAsn1Reencoding round-trip branch + // already covered by TestParseQcStatemPsd2MalformedEncoding. + truncated := psd2Bytes[:len(psd2Bytes)-1] + extVal := buildPsd2ExtValue(t, truncated) + + result := ParseQcStatem(extVal, IdEtsiPsd2Statem) + if result.GetErrorInfo() == "" { + t.Fatalf("expected error info for PSD2 QC statement with truncated statementInfo bytes, got none") + } +} + func TestParseQcStatemPsd2NotPresent(t *testing.T) { extVal := buildPsd2ExtValue(t, mustMarshal(t, testPSD2QcTypeValid{ RolesOfPSP: []testRoleOfPSP{{RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"}}, diff --git a/v3/util/time.go b/v3/util/time.go index 3cc0ead63..c0c352f1a 100644 --- a/v3/util/time.go +++ b/v3/util/time.go @@ -69,7 +69,7 @@ var ( EtsiEn319_412_5_V2_6_0_Date = time.Date(2026, time.February, 1, 0, 0, 0, 0, time.UTC) EtsiEn319_412_5_V2_6_1_Date = time.Date(2026, time.May, 1, 0, 0, 0, 0, time.UTC) EtsiEn319_411_2_V2_5_0_Date = time.Date(2023, time.July, 1, 0, 0, 0, 0, time.UTC) - EtsiTS119495_V1_1_2_Date = time.Date(2018, time.July, 1, 0, 0, 0, 0, time.UTC) + EtsiTs119495_V1_1_2_Date = time.Date(2018, time.July, 1, 0, 0, 0, 0, time.UTC) OnionOnlyEVDate = time.Date(2015, time.May, 1, 0, 0, 0, 0, time.UTC) CABV201Date = time.Date(2017, time.July, 28, 0, 0, 0, 0, time.UTC) AppleCTPolicyDate = time.Date(2018, time.October, 15, 0, 0, 0, 0, time.UTC) From 648be681386a29ad96691cdca3129673e405ddef Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sat, 8 Aug 2026 09:40:23 -0700 Subject: [PATCH 05/12] test: use real util.RoleOfPSP/PSD2QcType instead of test-only mirror types --- v3/util/qc_stmt_test.go | 71 ++++++++++++----------------------------- 1 file changed, 21 insertions(+), 50 deletions(-) diff --git a/v3/util/qc_stmt_test.go b/v3/util/qc_stmt_test.go index 079a5d942..98b224649 100644 --- a/v3/util/qc_stmt_test.go +++ b/v3/util/qc_stmt_test.go @@ -20,61 +20,39 @@ import ( "github.com/zmap/zcrypto/encoding/asn1" ) -// test-only mirrors of the PSD2 ASN.1 types. testPSD2QcTypeMalformed -// deliberately tags NCAName as PrintableString instead of UTF8String to -// produce a statement that parses but does not round-trip to the same -// bytes the real UTF8String-tagged PSD2QcType would produce. -type testRoleOfPSP struct { - RoleOfPspOid asn1.ObjectIdentifier - RoleOfPspName string `asn1:"utf8"` -} - -type testPSD2QcTypeValid struct { - RolesOfPSP []testRoleOfPSP - NCAName string `asn1:"utf8"` - NCAId string `asn1:"utf8"` -} - +// testPSD2QcTypeMalformed deliberately tags NCAName as PrintableString +// instead of UTF8String to produce a statement that parses but does not +// round-trip to the same bytes the real UTF8String-tagged PSD2QcType would +// produce. RoleOfPSP and PSD2QcType themselves are used directly from +// qc_stmt.go below; this file is package util, so it has direct access to +// the real types and doesn't need mirrors of them. type testPSD2QcTypeMalformed struct { - RolesOfPSP []testRoleOfPSP + RolesOfPSP []RoleOfPSP NCAName string `asn1:"printable"` NCAId string `asn1:"utf8"` } -type testQcStatement struct { - Oid asn1.ObjectIdentifier - Info asn1.RawValue -} - // buildPsd2ExtValue wraps already-encoded PSD2QcType bytes into a // one-statement QcStatements extension value, i.e. what CheckApplies / // ParseQcStatem expect to receive as extVal. func buildPsd2ExtValue(t *testing.T, psd2Bytes []byte) []byte { t.Helper() - stmt := testQcStatement{ - Oid: IdEtsiPsd2Statem, - Info: asn1.RawValue{FullBytes: psd2Bytes}, + stmt := qcStatementWithInfoField{ + Oid: IdEtsiPsd2Statem, + Any: asn1.RawValue{FullBytes: psd2Bytes}, } - extVal, err := asn1.Marshal([]testQcStatement{stmt}) - if err != nil { - t.Fatalf("failed to marshal QcStatements extension value: %v", err) - } - return extVal + return mustMarshal(t, []qcStatementWithInfoField{stmt}) } func TestParseQcStatemPsd2Valid(t *testing.T) { - psd2 := testPSD2QcTypeValid{ - RolesOfPSP: []testRoleOfPSP{ + psd2 := PSD2QcType{ + RolesOfPSP: []RoleOfPSP{ {RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"}, }, NCAName: "Banco de España", NCAId: "ES-BDE", } - psd2Bytes, err := asn1.Marshal(psd2) - if err != nil { - t.Fatalf("failed to marshal PSD2QcType: %v", err) - } - extVal := buildPsd2ExtValue(t, psd2Bytes) + extVal := buildPsd2ExtValue(t, mustMarshal(t, psd2)) result := ParseQcStatem(extVal, IdEtsiPsd2Statem) if !result.IsPresent() { @@ -97,17 +75,13 @@ func TestParseQcStatemPsd2Valid(t *testing.T) { func TestParseQcStatemPsd2MalformedEncoding(t *testing.T) { psd2 := testPSD2QcTypeMalformed{ - RolesOfPSP: []testRoleOfPSP{ + RolesOfPSP: []RoleOfPSP{ {RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"}, }, NCAName: "Banco de Espana", NCAId: "ES-BDE", } - psd2Bytes, err := asn1.Marshal(psd2) - if err != nil { - t.Fatalf("failed to marshal malformed PSD2QcType: %v", err) - } - extVal := buildPsd2ExtValue(t, psd2Bytes) + extVal := buildPsd2ExtValue(t, mustMarshal(t, psd2)) result := ParseQcStatem(extVal, IdEtsiPsd2Statem) if result.GetErrorInfo() == "" { @@ -116,17 +90,14 @@ func TestParseQcStatemPsd2MalformedEncoding(t *testing.T) { } func TestParseQcStatemPsd2UnmarshalFailure(t *testing.T) { - psd2 := testPSD2QcTypeValid{ - RolesOfPSP: []testRoleOfPSP{ + psd2 := PSD2QcType{ + RolesOfPSP: []RoleOfPSP{ {RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"}, }, NCAName: "Banco de España", NCAId: "ES-BDE", } - psd2Bytes, err := asn1.Marshal(psd2) - if err != nil { - t.Fatalf("failed to marshal PSD2QcType: %v", err) - } + psd2Bytes := mustMarshal(t, psd2) // Truncate the otherwise-valid, already-marshaled PSD2QcType bytes by one // byte so the outer SEQUENCE's declared length no longer matches the // available content. Note: appending extra trailing bytes instead does @@ -151,8 +122,8 @@ func TestParseQcStatemPsd2UnmarshalFailure(t *testing.T) { } func TestParseQcStatemPsd2NotPresent(t *testing.T) { - extVal := buildPsd2ExtValue(t, mustMarshal(t, testPSD2QcTypeValid{ - RolesOfPSP: []testRoleOfPSP{{RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"}}, + extVal := buildPsd2ExtValue(t, mustMarshal(t, PSD2QcType{ + RolesOfPSP: []RoleOfPSP{{RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"}}, NCAName: "Banco de España", NCAId: "ES-BDE", })) From ea7d03f93ce76c62a36d52376f2cc383cfc22a5b Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sat, 8 Aug 2026 18:57:09 -0700 Subject: [PATCH 06/12] testdata: add PSD2 NCAName fixtures (empty, too-long, PSP_CB/PA NA carve-out) --- .../QcStmtEtsiPsd2NcaNameEmptyCert01.pem | 42 +++++++++++++++++ ...cStmtEtsiPsd2NcaNamePspCbInvalidCert01.pem | 42 +++++++++++++++++ .../QcStmtEtsiPsd2NcaNamePspCbValidCert01.pem | 42 +++++++++++++++++ .../QcStmtEtsiPsd2NcaNamePspPaValidCert01.pem | 42 +++++++++++++++++ .../QcStmtEtsiPsd2NcaNameTooLongCert01.pem | 47 +++++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 v3/testdata/QcStmtEtsiPsd2NcaNameEmptyCert01.pem create mode 100644 v3/testdata/QcStmtEtsiPsd2NcaNamePspCbInvalidCert01.pem create mode 100644 v3/testdata/QcStmtEtsiPsd2NcaNamePspCbValidCert01.pem create mode 100644 v3/testdata/QcStmtEtsiPsd2NcaNamePspPaValidCert01.pem create mode 100644 v3/testdata/QcStmtEtsiPsd2NcaNameTooLongCert01.pem diff --git a/v3/testdata/QcStmtEtsiPsd2NcaNameEmptyCert01.pem b/v3/testdata/QcStmtEtsiPsd2NcaNameEmptyCert01.pem new file mode 100644 index 000000000..60faa5d84 --- /dev/null +++ b/v3/testdata/QcStmtEtsiPsd2NcaNameEmptyCert01.pem @@ -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 NCAName Test Leaf + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:5e:79:45:e9:69:ae:23:1a:53:74:23:1f:5f:ac: + e7:1c:70:f4:d0:02:b4:61:74:dc:01:67:c8:5d:07: + d2:b2:87:9c:b7:be:6b:62:88:1c:86:23:c1:85:c3: + 98:39:d0:40:23:3a:1f:6c:56:1c:97:2c:c4:0f:3b: + e6:12:f8:0e:03 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + qcStatements: + 0+0)......'.0.0.0.......'....PSP_AS....ES-BDE + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:46:02:21:00:94:1d:4a:b1:4a:50:a6:a1:6a:f2:d5:39:c4: + e0:6f:10:6b:ae:37:18:3e:41:90:5b:c0:96:87:35:49:53:f9: + c7:02:21:00:d4:ff:ee:29:6c:43:a8:71:21:40:af:90:90:9c: + df:a4:46:ac:b9:8e:14:74:33:f4:f9:df:82:fb:82:4d:79:e6 +-----BEGIN CERTIFICATE----- +MIIBXDCCAQGgAwIBAgIBAjAKBggqhkjOPQQDAjAAMB4XDTIwMDEwMTAwMDAwMFoX +DTMwMDEwMTAwMDAwMFowITEfMB0GA1UEAxMWUFNEMiBOQ0FOYW1lIFRlc3QgTGVh +ZjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABF55RelpriMaU3QjH1+s5xxw9NAC +tGF03AFnyF0H0rKHnLe+a2KIHIYjwYXDmDnQQCM6H2xWHJcsxA875hL4DgOjSzBJ +MAwGA1UdEwEB/wQCMAAwOQYIKwYBBQUHAQMELTArMCkGBgQAgZgnAjAfMBMwEQYH +BACBmCcBAQwGUFNQX0FTDAAMBkVTLUJERTAKBggqhkjOPQQDAgNJADBGAiEAlB1K +sUpQpqFq8tU5xOBvEGuuNxg+QZBbwJaHNUlT+ccCIQDU/+4pbEOocSFAr5CQnN+k +Rqy5jhR0M/T534L7gk155g== +-----END CERTIFICATE----- diff --git a/v3/testdata/QcStmtEtsiPsd2NcaNamePspCbInvalidCert01.pem b/v3/testdata/QcStmtEtsiPsd2NcaNamePspCbInvalidCert01.pem new file mode 100644 index 000000000..e65003f50 --- /dev/null +++ b/v3/testdata/QcStmtEtsiPsd2NcaNamePspCbInvalidCert01.pem @@ -0,0 +1,42 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 6 (0x6) + 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 NCAName Test Leaf + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:29:51:cc:1d:d0:6d:0e:7b:ea:03:23:a6:c8:f4: + 13:92:5d:36:a3:bd:ce:b0:05:13:fa:3c:64:1b:be: + 84:a9:23:9f:d3:b8:51:ba:2d:bb:15:8d:b2:32:0a: + 48:fb:8d:e0:fe:46:4f:e7:30:a2:17:66:8e:42:b5: + 16:0e:87:b2:bf + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + qcStatements: + 0705......'.0+0.0.......'....PSP_CB..Banco de Espa..a..NA + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:44:02:20:08:76:c4:60:b3:bd:65:56:30:d8:20:0f:c1:5c: + 96:1c:30:50:6a:e3:b8:2a:ac:31:7c:a6:69:68:c0:eb:3b:fa: + 02:20:5d:a5:13:e2:38:39:d6:d7:24:d2:47:b8:57:ed:1c:69: + 02:70:f5:4d:d1:7e:80:9e:28:05:17:b7:60:e8:40:4b +-----BEGIN CERTIFICATE----- +MIIBZjCCAQ2gAwIBAgIBBjAKBggqhkjOPQQDAjAAMB4XDTIwMDEwMTAwMDAwMFoX +DTMwMDEwMTAwMDAwMFowITEfMB0GA1UEAxMWUFNEMiBOQ0FOYW1lIFRlc3QgTGVh +ZjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABClRzB3QbQ576gMjpsj0E5JdNqO9 +zrAFE/o8ZBu+hKkjn9O4UbotuxWNsjIKSPuN4P5GT+cwohdmjkK1Fg6Hsr+jVzBV +MAwGA1UdEwEB/wQCMAAwRQYIKwYBBQUHAQMEOTA3MDUGBgQAgZgnAjArMBMwEQYH +BACBmCcBBQwGUFNQX0NCDBBCYW5jbyBkZSBFc3Bhw7FhDAJOQTAKBggqhkjOPQQD +AgNHADBEAiAIdsRgs71lVjDYIA/BXJYcMFBq47gqrDF8pmlowOs7+gIgXaUT4jg5 +1tck0ke4V+0caQJw9U3RfoCeKAUXt2DoQEs= +-----END CERTIFICATE----- diff --git a/v3/testdata/QcStmtEtsiPsd2NcaNamePspCbValidCert01.pem b/v3/testdata/QcStmtEtsiPsd2NcaNamePspCbValidCert01.pem new file mode 100644 index 000000000..916ce3260 --- /dev/null +++ b/v3/testdata/QcStmtEtsiPsd2NcaNamePspCbValidCert01.pem @@ -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 NCAName Test Leaf + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:64:ec:a7:f8:e8:eb:39:55:bc:96:5d:6c:97:e4: + 64:5e:1d:a8:11:1c:3b:5b:fb:b9:1a:21:00:a1:51: + f9:c6:55:17:c1:d2:07:b2:41:39:9a:dc:43:7f:bf: + e6:a6:cb:14:ed:a6:00:71:97:c0:6f:b1:84:15:3f: + 25:44:e9:9c:71 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + qcStatements: + 0)0'......'.0.0.0.......'....PSP_CB..NA..NA + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:45:02:20:03:dd:d7:fb:ec:5f:c9:2f:bb:c9:bd:76:aa:7b: + fa:85:2a:fd:f9:ba:cb:37:9c:93:fd:32:31:80:2d:72:b2:1a: + 02:21:00:f1:35:18:1b:88:5f:e0:46:82:cd:45:49:a2:a4:ff: + 92:7e:f0:f7:2a:61:0f:fb:18:49:e7:6b:a1:10:de:d3:34 +-----BEGIN CERTIFICATE----- +MIIBWDCB/6ADAgECAgEEMAoGCCqGSM49BAMCMAAwHhcNMjAwMTAxMDAwMDAwWhcN +MzAwMTAxMDAwMDAwWjAhMR8wHQYDVQQDExZQU0QyIE5DQU5hbWUgVGVzdCBMZWFm +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZOyn+OjrOVW8ll1sl+RkXh2oERw7 +W/u5GiEAoVH5xlUXwdIHskE5mtxDf7/mpssU7aYAcZfAb7GEFT8lROmccaNJMEcw +DAYDVR0TAQH/BAIwADA3BggrBgEFBQcBAwQrMCkwJwYGBACBmCcCMB0wEzARBgcE +AIGYJwEFDAZQU1BfQ0IMAk5BDAJOQTAKBggqhkjOPQQDAgNIADBFAiAD3df77F/J +L7vJvXaqe/qFKv35uss3nJP9MjGALXKyGgIhAPE1GBuIX+BGgs1FSaKk/5J+8Pcq +YQ/7GEnna6EQ3tM0 +-----END CERTIFICATE----- diff --git a/v3/testdata/QcStmtEtsiPsd2NcaNamePspPaValidCert01.pem b/v3/testdata/QcStmtEtsiPsd2NcaNamePspPaValidCert01.pem new file mode 100644 index 000000000..27ea3d22c --- /dev/null +++ b/v3/testdata/QcStmtEtsiPsd2NcaNamePspPaValidCert01.pem @@ -0,0 +1,42 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 5 (0x5) + 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 NCAName Test Leaf + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:1e:69:4a:94:58:0e:54:56:38:53:42:25:65:ac: + e7:10:3a:e3:45:92:0b:25:88:74:86:fb:eb:7c:1b: + 90:42:95:2f:ab:0b:c0:67:f9:eb:0a:db:33:a4:f9: + 33:79:a5:ec:37:4a:25:4b:57:d2:a2:78:e9:ac:4f: + 3e:6d:16:83:5e + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + qcStatements: + 0)0'......'.0.0.0.......'....PSP_PA..NA..NA + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:44:02:20:78:08:3a:82:3f:76:7c:ff:97:84:cb:c5:43:65: + bd:5f:d1:02:4b:a4:42:d1:d8:98:c5:30:1b:12:4e:bc:33:e8: + 02:20:70:77:2f:7c:ad:c0:e9:d8:b6:87:64:2b:32:79:99:36: + c7:0f:ba:17:81:c5:0b:0a:dc:f1:49:26:0c:56:2b:8c +-----BEGIN CERTIFICATE----- +MIIBVzCB/6ADAgECAgEFMAoGCCqGSM49BAMCMAAwHhcNMjAwMTAxMDAwMDAwWhcN +MzAwMTAxMDAwMDAwWjAhMR8wHQYDVQQDExZQU0QyIE5DQU5hbWUgVGVzdCBMZWFm +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHmlKlFgOVFY4U0IlZaznEDrjRZIL +JYh0hvvrfBuQQpUvqwvAZ/nrCtszpPkzeaXsN0olS1fSonjprE8+bRaDXqNJMEcw +DAYDVR0TAQH/BAIwADA3BggrBgEFBQcBAwQrMCkwJwYGBACBmCcCMB0wEzARBgcE +AIGYJwEGDAZQU1BfUEEMAk5BDAJOQTAKBggqhkjOPQQDAgNHADBEAiB4CDqCP3Z8 +/5eEy8VDZb1f0QJLpELR2JjFMBsSTrwz6AIgcHcvfK3A6di2h2QrMnmZNscPuheB +xQsK3PFJJgxWK4w= +-----END CERTIFICATE----- diff --git a/v3/testdata/QcStmtEtsiPsd2NcaNameTooLongCert01.pem b/v3/testdata/QcStmtEtsiPsd2NcaNameTooLongCert01.pem new file mode 100644 index 000000000..3cd3801f8 --- /dev/null +++ b/v3/testdata/QcStmtEtsiPsd2NcaNameTooLongCert01.pem @@ -0,0 +1,47 @@ +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 NCAName Test Leaf + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:4e:3a:ec:ed:68:f3:e5:1f:a1:38:41:67:00:6e: + e3:1c:d3:be:cf:fc:6e:b3:fe:56:2d:2d:db:04:a7: + 78:d9:99:71:a2:f3:6a:fb:78:6d:19:6d:2a:cc:f4: + 6f:1a:e1:8f:da:59:e8:d5:c7:f8:08:93:26:5b:30: + f4:d1:2c:7a:70 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + qcStatements: + 0..20.........'.0.."0.0.......'....PSP_AS....AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..ES-BDE + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:45:02:21:00:86:49:31:16:fb:56:52:da:54:6c:9a:a1:ac: + 93:fb:fe:05:d2:3b:42:50:98:2e:a6:a2:14:6e:a2:23:3c:b2: + 4c:02:20:1d:96:4c:15:7a:97:c1:8d:a4:57:d5:f5:ea:d6:3c: + a2:2d:5b:bc:9d:ac:58:e7:99:cc:39:ce:3d:f0:0c:0d:69 +-----BEGIN CERTIFICATE----- +MIICbDCCAhKgAwIBAgIBAzAKBggqhkjOPQQDAjAAMB4XDTIwMDEwMTAwMDAwMFoX +DTMwMDEwMTAwMDAwMFowITEfMB0GA1UEAxMWUFNEMiBOQ0FOYW1lIFRlc3QgTGVh +ZjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE467O1o8+UfoThBZwBu4xzTvs/8 +brP+Vi0t2wSneNmZcaLzavt4bRltKsz0bxrhj9pZ6NXH+AiTJlsw9NEsenCjggFa +MIIBVjAMBgNVHRMBAf8EAjAAMIIBRAYIKwYBBQUHAQMEggE2MIIBMjCCAS4GBgQA +gZgnAjCCASIwEzARBgcEAIGYJwEBDAZQU1BfQVMMggEBQUFBQUFBQUFBQUFBQUFB +QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB +QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB +QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB +QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB +QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB +QUEMBkVTLUJERTAKBggqhkjOPQQDAgNIADBFAiEAhkkxFvtWUtpUbJqhrJP7/gXS +O0JQmC6mohRuoiM8skwCIB2WTBV6l8GNpFfV9erWPKItW7ydrFjnmcw5zj3wDA1p +-----END CERTIFICATE----- From 45e58aed1691c458bc71085a27cb94aa8a1750bd Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sat, 8 Aug 2026 19:02:18 -0700 Subject: [PATCH 07/12] lints/etsi: add e_qcstatem_psd2_ncaname_valid --- .../etsi/lint_qcstatem_psd2_ncaname_valid.go | 77 +++++++++++++++++++ .../lint_qcstatem_psd2_ncaname_valid_test.go | 48 ++++++++++++ v3/util/oid.go | 4 +- v3/util/qc_stmt.go | 12 +++ v3/util/qc_stmt_test.go | 22 ++++++ 5 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go create mode 100644 v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid_test.go diff --git a/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go new file mode 100644 index 000000000..194d0c2cb --- /dev/null +++ b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go @@ -0,0 +1,77 @@ +/* + * 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 ( + "unicode/utf8" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type qcStatemPsd2NcaNameValid struct{} + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_qcstatem_psd2_ncaname_valid", + Description: "Checks that the NCAName field of a PSD2 QcStatement is non-empty and at most 256 characters, or 'NA' for a certificate declaring a PSP_CB or PSP_PA role", + Citation: "ETSI TS 119 495 V1.1.2 (2018-07), Section 5.2.3, GEN-5.2.3-1", + Source: lint.EtsiEsi, + EffectiveDate: util.EtsiTs119495_V1_1_2_Date, + }, + Lint: NewQcStatemPsd2NcaNameValid, + }) +} + +func NewQcStatemPsd2NcaNameValid() lint.LintInterface { + return &qcStatemPsd2NcaNameValid{} +} + +func (l *qcStatemPsd2NcaNameValid) 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 *qcStatemPsd2NcaNameValid) 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"} + } + + if util.IsPsd2CentralBankOrPublicAuthority(psd2.Decoded.RolesOfPSP) { + if psd2.Decoded.NCAName != "NA" { + return &lint.LintResult{Status: lint.Error, Details: "NCAName must be 'NA' for a PSD2 QcStatement declaring a PSP_CB or PSP_PA role"} + } + return &lint.LintResult{Status: lint.Pass} + } + + nameLen := utf8.RuneCountInString(psd2.Decoded.NCAName) + if nameLen == 0 { + return &lint.LintResult{Status: lint.Error, Details: "NCAName must not be empty"} + } + if nameLen > 256 { + return &lint.LintResult{Status: lint.Error, Details: "NCAName must be at most 256 characters"} + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid_test.go b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid_test.go new file mode 100644 index 000000000..3fc419af8 --- /dev/null +++ b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid_test.go @@ -0,0 +1,48 @@ +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 TestEtsiQcStatemPsd2NcaNameValid(t *testing.T) { + cases := map[string]struct { + status lint.LintStatus + details string // empty means "don't check Details" + }{ + "QcStmtEtsiPsd2ValidCert01.pem": {status: lint.Pass}, + "QcStmtEtsiPsd2NcaNameEmptyCert01.pem": {status: lint.Error, details: "NCAName must not be empty"}, + "QcStmtEtsiPsd2NcaNameTooLongCert01.pem": {status: lint.Error, details: "NCAName must be at most 256 characters"}, + "QcStmtEtsiPsd2NcaNamePspCbValidCert01.pem": {status: lint.Pass}, + "QcStmtEtsiPsd2NcaNamePspPaValidCert01.pem": {status: lint.Pass}, + "QcStmtEtsiPsd2NcaNamePspCbInvalidCert01.pem": {status: lint.Error, + details: "NCAName must be 'NA' for a PSD2 QcStatement declaring a PSP_CB or PSP_PA role"}, + "QcStmtEtsiPsd2WrongEncodingCert01.pem": {status: lint.Error}, + "QcStmtEtsiValidCert11.pem": {status: lint.NA}, + } + for inputPath, tc := range cases { + out := test.TestLint("e_qcstatem_psd2_ncaname_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) + } + } +} diff --git a/v3/util/oid.go b/v3/util/oid.go index 95d4c0da7..51a2f5996 100644 --- a/v3/util/oid.go +++ b/v3/util/oid.go @@ -127,7 +127,9 @@ var ( QEVCPwPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 4} QNCPwPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 5} QNCPwgenPolicyOID = asn1.ObjectIdentifier{0, 4, 0, 194112, 1, 6} - IdEtsiPsd2Statem = asn1.ObjectIdentifier{0, 4, 0, 19495, 2} // ETSI TS 119 495 V1.1.2, Annex A: id-etsi-psd2-qcStatement + IdEtsiPsd2Statem = asn1.ObjectIdentifier{0, 4, 0, 19495, 2} // ETSI TS 119 495 V1.1.2, Annex A: id-etsi-psd2-qcStatement + IdEtsiPsd2RolePspCb = asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 5} // ETSI TS 119 495, Annex A: id-psd2-role-psp-cb (Central Bank) + IdEtsiPsd2RolePspPa = asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 6} // ETSI TS 119 495, Annex A: id-psd2-role-psp-pa (Public authority) ) const ( diff --git a/v3/util/qc_stmt.go b/v3/util/qc_stmt.go index 07533d74c..c47881584 100644 --- a/v3/util/qc_stmt.go +++ b/v3/util/qc_stmt.go @@ -115,6 +115,18 @@ type EtsiPsd2 struct { Decoded PSD2QcType } +// IsPsd2CentralBankOrPublicAuthority reports whether roles declares a PSP_CB +// (Central Bank) or PSP_PA (Public authority) role, per ETSI TS 119 495 +// Annex A. Certificates declaring either role are subject to the +// GEN-5.2.3-1A/GEN-5.2.3-5 "NA" carve-out for NCAName/NCAId. +func IsPsd2CentralBankOrPublicAuthority(roles []RoleOfPSP) bool { + oids := make([]asn1.ObjectIdentifier, 0, len(roles)) + for _, role := range roles { + oids = append(oids, role.RoleOfPspOid) + } + return SliceContainsOID(oids, IdEtsiPsd2RolePspCb) || SliceContainsOID(oids, IdEtsiPsd2RolePspPa) +} + func AppendToStringSemicolonDelim(this *string, s string) { if len(*this) > 0 && len(s) > 0 { (*this) += "; " diff --git a/v3/util/qc_stmt_test.go b/v3/util/qc_stmt_test.go index 98b224649..42b759f9f 100644 --- a/v3/util/qc_stmt_test.go +++ b/v3/util/qc_stmt_test.go @@ -144,3 +144,25 @@ func mustMarshal(t *testing.T, v interface{}) []byte { } return b } + +func TestIsPsd2CentralBankOrPublicAuthority(t *testing.T) { + cb := RoleOfPSP{RoleOfPspOid: IdEtsiPsd2RolePspCb, RoleOfPspName: "PSP_CB"} + pa := RoleOfPSP{RoleOfPspOid: IdEtsiPsd2RolePspPa, RoleOfPspName: "PSP_PA"} + as := RoleOfPSP{RoleOfPspOid: asn1.ObjectIdentifier{0, 4, 0, 19495, 1, 1}, RoleOfPspName: "PSP_AS"} + + if !IsPsd2CentralBankOrPublicAuthority([]RoleOfPSP{cb}) { + t.Errorf("expected true for a role list containing only PSP_CB") + } + if !IsPsd2CentralBankOrPublicAuthority([]RoleOfPSP{pa}) { + t.Errorf("expected true for a role list containing only PSP_PA") + } + if !IsPsd2CentralBankOrPublicAuthority([]RoleOfPSP{as, cb}) { + t.Errorf("expected true for a role list containing PSP_AS and PSP_CB") + } + if IsPsd2CentralBankOrPublicAuthority([]RoleOfPSP{as}) { + t.Errorf("expected false for a role list containing only PSP_AS") + } + if IsPsd2CentralBankOrPublicAuthority(nil) { + t.Errorf("expected false for an empty role list") + } +} From 8d6e3f0ff8d9f81cc9023df1400d2eda672b2ce4 Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sat, 8 Aug 2026 19:10:21 -0700 Subject: [PATCH 08/12] fix: assert Details on structural-deferral test case, correct plan doc claim --- v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid_test.go b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid_test.go index 3fc419af8..459315d64 100644 --- a/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid_test.go +++ b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid_test.go @@ -33,7 +33,7 @@ func TestEtsiQcStatemPsd2NcaNameValid(t *testing.T) { "QcStmtEtsiPsd2NcaNamePspPaValidCert01.pem": {status: lint.Pass}, "QcStmtEtsiPsd2NcaNamePspCbInvalidCert01.pem": {status: lint.Error, details: "NCAName must be 'NA' for a PSD2 QcStatement declaring a PSP_CB or PSP_PA role"}, - "QcStmtEtsiPsd2WrongEncodingCert01.pem": {status: lint.Error}, + "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 { From 39d6114e4f69fdb3af87dc25aec5fd1115773897 Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sat, 8 Aug 2026 19:15:59 -0700 Subject: [PATCH 09/12] refactor: simplify IsPsd2CentralBankOrPublicAuthority to avoid allocation --- v3/util/qc_stmt.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/v3/util/qc_stmt.go b/v3/util/qc_stmt.go index c47881584..8dd6b0565 100644 --- a/v3/util/qc_stmt.go +++ b/v3/util/qc_stmt.go @@ -120,11 +120,12 @@ type EtsiPsd2 struct { // Annex A. Certificates declaring either role are subject to the // GEN-5.2.3-1A/GEN-5.2.3-5 "NA" carve-out for NCAName/NCAId. func IsPsd2CentralBankOrPublicAuthority(roles []RoleOfPSP) bool { - oids := make([]asn1.ObjectIdentifier, 0, len(roles)) for _, role := range roles { - oids = append(oids, role.RoleOfPspOid) + if role.RoleOfPspOid.Equal(IdEtsiPsd2RolePspCb) || role.RoleOfPspOid.Equal(IdEtsiPsd2RolePspPa) { + return true + } } - return SliceContainsOID(oids, IdEtsiPsd2RolePspCb) || SliceContainsOID(oids, IdEtsiPsd2RolePspPa) + return false } func AppendToStringSemicolonDelim(this *string, s string) { From 307c70af61b54394618cd71426c88a95fe42d431 Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sat, 8 Aug 2026 20:43:49 -0700 Subject: [PATCH 10/12] docs: add raw ETSI TS 119 495 citation text as block comment --- v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go index 194d0c2cb..71c4417f6 100644 --- a/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go +++ b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go @@ -24,6 +24,18 @@ import ( type qcStatemPsd2NcaNameValid struct{} +// ETSI TS 119 495 V1.1.2 (2018-07), Section 5.2.3: +// +// GEN-5.2.3-1: The NCAName shall be plain text using Latin alphabet +// provided by the Competent Authority itself for purpose of +// identification in certificates. +// +// NCAName ::= UTF8String (SIZE(1..256)) +// +// GEN-5.2.3-1A (added in a later edition than V1.1.2, verified against the +// current edition V1.8.1): If the subject role is international central +// bank (PSP_CB) or international public authority (PSP_PA), NCAName shall +// have the value "NA". func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ From b36a4a758ccbd324eed642f0e084485e5d765c00 Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sun, 16 Aug 2026 10:42:08 -0700 Subject: [PATCH 11/12] fix: peg e_qcstatem_psd2_ncaname_valid's citation to latest ETSI edition Repoint Citation and the embedded clause text at V1.8.1 (2026-04), and add GEN-5.2.3-1A (the NCAName "NA" carve-out) to the formal Citation field -- it was only mentioned in the comment before. --- v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go index 71c4417f6..6300fa761 100644 --- a/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go +++ b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go @@ -24,7 +24,7 @@ import ( type qcStatemPsd2NcaNameValid struct{} -// ETSI TS 119 495 V1.1.2 (2018-07), Section 5.2.3: +// ETSI TS 119 495 V1.8.1 (2026-04), Section 5.2.3: // // GEN-5.2.3-1: The NCAName shall be plain text using Latin alphabet // provided by the Competent Authority itself for purpose of @@ -32,16 +32,15 @@ type qcStatemPsd2NcaNameValid struct{} // // NCAName ::= UTF8String (SIZE(1..256)) // -// GEN-5.2.3-1A (added in a later edition than V1.1.2, verified against the -// current edition V1.8.1): If the subject role is international central -// bank (PSP_CB) or international public authority (PSP_PA), NCAName shall +// GEN-5.2.3-1A: If the subject role is international central bank +// (PSP_CB) or international public authority (PSP_PA), NCAName shall // have the value "NA". func init() { lint.RegisterCertificateLint(&lint.CertificateLint{ LintMetadata: lint.LintMetadata{ Name: "e_qcstatem_psd2_ncaname_valid", Description: "Checks that the NCAName field of a PSD2 QcStatement is non-empty and at most 256 characters, or 'NA' for a certificate declaring a PSP_CB or PSP_PA role", - Citation: "ETSI TS 119 495 V1.1.2 (2018-07), Section 5.2.3, GEN-5.2.3-1", + Citation: "ETSI TS 119 495 V1.8.1 (2026-04), Section 5.2.3, GEN-5.2.3-1, GEN-5.2.3-1A", Source: lint.EtsiEsi, EffectiveDate: util.EtsiTs119495_V1_1_2_Date, }, From fff51a6cec0bf26bdfe2c2e048593908aeafde18 Mon Sep 17 00:00:00 2001 From: christopher-henderson Date: Sun, 16 Aug 2026 11:01:47 -0700 Subject: [PATCH 12/12] fix: don't let the generic structural error shadow this lint's own NCAName message e_qcstatem_psd2_valid's base util.ParseQcStatem now enforces Annex A's SIZE(1..256) bound structurally (as of e_qcstatem_psd2_valid@c006e0bf, merged into this PR's CI view via the base branch). Since Execute() checked s.GetErrorInfo() before its own nameLen checks, an empty or oversized NCAName now hit that generic structural message first, making this lint's own "NCAName must not be empty" / "must be at most 256 characters" Details unreachable -- breaking TestEtsiQcStatemPsd2NcaNameValid once tested against the merged state. Reorder so this lint's own NCAName-specific checks (against psd2.Decoded, which stays populated even when GetErrorInfo() is non-empty for a SIZE violation) run first, falling back to the generic structural message only for problems they can't explain (wrong ASN.1 type, issues in other fields). --- .../etsi/lint_qcstatem_psd2_ncaname_valid.go | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go index 6300fa761..d72b90999 100644 --- a/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go +++ b/v3/lints/etsi/lint_qcstatem_psd2_ncaname_valid.go @@ -62,27 +62,32 @@ func (l *qcStatemPsd2NcaNameValid) CheckApplies(c *x509.Certificate) bool { func (l *qcStatemPsd2NcaNameValid) 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"} } + // Check NCAName itself against psd2.Decoded (populated whenever the + // PSD2QcType SEQUENCE itself unmarshaled, even if e_qcstatem_psd2_valid's + // structural SIZE(1..256) check already flagged it) before falling back + // to s.GetErrorInfo()'s generic message, so this lint's own + // NCAName-specific Details take precedence over that structural check. if util.IsPsd2CentralBankOrPublicAuthority(psd2.Decoded.RolesOfPSP) { if psd2.Decoded.NCAName != "NA" { return &lint.LintResult{Status: lint.Error, Details: "NCAName must be 'NA' for a PSD2 QcStatement declaring a PSP_CB or PSP_PA role"} } - return &lint.LintResult{Status: lint.Pass} + } else { + nameLen := utf8.RuneCountInString(psd2.Decoded.NCAName) + if nameLen == 0 { + return &lint.LintResult{Status: lint.Error, Details: "NCAName must not be empty"} + } + if nameLen > 256 { + return &lint.LintResult{Status: lint.Error, Details: "NCAName must be at most 256 characters"} + } } - nameLen := utf8.RuneCountInString(psd2.Decoded.NCAName) - if nameLen == 0 { - return &lint.LintResult{Status: lint.Error, Details: "NCAName must not be empty"} - } - if nameLen > 256 { - return &lint.LintResult{Status: lint.Error, Details: "NCAName must be at most 256 characters"} + if s.GetErrorInfo() != "" { + return &lint.LintResult{Status: lint.Error, Details: s.GetErrorInfo()} } return &lint.LintResult{Status: lint.Pass} }