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

QCP-N-QSCD 411 1(411 2), 412-2 and 412 5 #129

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
3 changes: 2 additions & 1 deletion pkilint/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ def __init__(
attribute_allowances,
finding_code_classifier: str,
unknown_attribute_allowance: Rfc2119Word,
path: str = "certificate.tbsCertificate.subject.rdnSequence",
):
unexpected_attribute_finding = (
None
Expand All @@ -339,5 +340,5 @@ def __init__(
finding_code_classifier + ".{oid}_attribute_present",
finding_code_classifier + ".{oid}_attribute_absent",
unexpected_attribute_finding,
path="certificate.tbsCertificate.subject.rdnSequence",
path=path,
)
32 changes: 27 additions & 5 deletions pkilint/etsi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ def create_validators(
if additional_name_validators:
subject_validators.extend(additional_name_validators)

issuer_validators = []

qc_statement_validators = [
ts_119_495.RolesOfPspValidator(),
ts_119_495.NCANameLatinCharactersValidator(),
Expand Down Expand Up @@ -292,6 +294,9 @@ def create_validators(
if additional_top_level_validators:
top_level_validators.extend(additional_top_level_validators)

if certificate_type in etsi_constants.EU:
extension_validators.append(en_319_412_5.QcStatementPresenceValidator())

if (
certificate_type in etsi_constants.LEGAL_PERSON_CERTIFICATE_TYPES
and certificate_type not in etsi_constants.CABF_CERTIFICATE_TYPES
Expand All @@ -312,6 +317,16 @@ def create_validators(
en_319_412_2.NaturalPersonSubjectAttributeAllowanceValidator()
)

if certificate_type in etsi_constants.EU:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this if statement can be removed, as the issuer requirements are applicable for non-EU certs as well.

issuer_validators.extend(
[
en_319_412_2.LegalPersonIssuerCountryCodeValidator(),
en_319_412_2.LegalPersonIssuerOrganizationAttributesEqualityValidator(),
en_319_412_2.LegalPersonIssuerDuplicateAttributeAllowanceValidator(),
en_319_412_2.LegalPersonIssuerAttributeAllowanceValidator(),
]
)

if certificate_type not in etsi_constants.CABF_CERTIFICATE_TYPES:
extension_validators.extend(
[
Expand Down Expand Up @@ -349,11 +364,18 @@ def create_validators(
)
)
elif certificate_type in etsi_constants.NATURAL_PERSON_CERTIFICATE_TYPES:
extension_validators.append(
en_319_412_2.NaturalPersonKeyUsageValidator(
is_content_commitment_type=None
if certificate_type in etsi_constants.QCP_N_CERTIFICATE_TYPES:
extension_validators.append(
en_319_412_2.NaturalPersonKeyUsageValidator(
is_content_commitment_type=True
)
)
else:
extension_validators.append(
en_319_412_2.NaturalPersonKeyUsageValidator(
is_content_commitment_type=None
)
)
)

if certificate_type in etsi_constants.QEVCP_W_PSD2_EIDAS_CERTIFICATE_TYPES:
qc_statement_validators.append(ts_119_495.PresenceofQCEUPDSStatementValidator())
Expand Down Expand Up @@ -407,7 +429,7 @@ def create_validators(
)

return [
certificate.create_issuer_validator_container([]),
certificate.create_issuer_validator_container(issuer_validators),
certificate.create_validity_validator_container(
additional_validity_validators
),
Expand Down
67 changes: 65 additions & 2 deletions pkilint/etsi/en_319_412_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from pyasn1.type import univ
from pyasn1_alt_modules import rfc5280, rfc3739

import pkilint.etsi.asn1.en_319_411_2
import pkilint.etsi.en_319_412_3
from pkilint import validation, oid, document, common
from pkilint.etsi import asn1 as etsi_asn1, etsi_shared
from pkilint.etsi import etsi_constants
from pkilint.etsi import etsi_shared
from pkilint.etsi.asn1 import en_319_411_2
from pkilint.pkix import extension, name, Rfc2119Word
from pkilint.pkix.general_name import GeneralNameTypeName
Expand Down Expand Up @@ -463,6 +463,10 @@ class QualifiedCertificatePoliciesValidator(validation.Validator):
etsi_constants.QNCP_W_GEN_NP_EIDAS_CERTIFICATE_TYPES,
en_319_411_2.id_qncp_web_gen,
),
(
etsi_constants.QCP_N_QSCD_CERTIFICATE_TYPES,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you remove or change the TODO comment on line 453 to note that QSCD certs are now supported?

en_319_411_2.id_qcp_natural_qscd,
),
]

def __init__(self, certificate_type: etsi_constants.CertificateType):
Expand Down Expand Up @@ -561,3 +565,62 @@ class ExtensionsPresenceValidator(common.ExtensionsPresenceValidator):

def __init__(self):
super().__init__(self.VALIDATION_EXTENSIONS_FIELD_ABSENT)


_LEGAL_PERSON_REQUIRED_ATTRIBUTES = {
rfc5280.id_at_countryName,
rfc5280.id_at_organizationName,
rfc5280.id_at_commonName,
}


class LegalPersonIssuerAttributeAllowanceValidator(
etsi_shared.LegalPersonAttributeAllowanceValidator
):
_CODE_CLASSIFIER = "etsi.en_319_412_2.gen-4.2.3.1-2"

def __init__(self):
super().__init__(
self._CODE_CLASSIFIER,
_LEGAL_PERSON_REQUIRED_ATTRIBUTES,
"certificate.tbsCertificate.issuer.rdnSequence",
)


class LegalPersonIssuerDuplicateAttributeAllowanceValidator(
etsi_shared.LegalPersonDuplicateAttributeAllowanceValidator
):
VALIDATION_PROHIBITED_DUPLICATE_ATTRIBUTE_PRESENT = validation.ValidationFinding(
validation.ValidationFindingSeverity.ERROR,
"etsi.en_319_412_2.gen-4.2.3.1-5.prohibited_duplicate_attribute_present",
)

def __init__(self):
super().__init__(
self.VALIDATION_PROHIBITED_DUPLICATE_ATTRIBUTE_PRESENT,
_LEGAL_PERSON_REQUIRED_ATTRIBUTES,
)


class LegalPersonIssuerOrganizationAttributesEqualityValidator(
etsi_shared.LegalPersonOrganizationAttributesEqualityValidator
):
VALIDATION_ORGID_ORGNAME_ATTRIBUTE_VALUES_EQUAL = validation.ValidationFinding(
validation.ValidationFindingSeverity.ERROR,
"etsi.en_319_412_2.gen-4.2.3.1-3.organization_id_and_organization_name_attribute_values_equal",
)

def __init__(self):
super().__init__(self.VALIDATION_ORGID_ORGNAME_ATTRIBUTE_VALUES_EQUAL)


class LegalPersonIssuerCountryCodeValidator(
etsi_shared.LegalPersonCountryCodeValidator
):
VALIDATION_UNKNOWN_COUNTRY_CODE = validation.ValidationFinding(
validation.ValidationFindingSeverity.NOTICE,
"etsi.en_319_412_2.gen-4.2.3.1-6.unknown_country_code",
)

def __init__(self):
super().__init__(self.VALIDATION_UNKNOWN_COUNTRY_CODE)
92 changes: 17 additions & 75 deletions pkilint/etsi/en_319_412_3.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
from pyasn1_alt_modules import rfc5280

from pkilint import common
from pkilint import validation
from pkilint.common import organization_id
from pkilint.etsi import etsi_shared
from pkilint.itu import x520_name, asn1_util
from pkilint.pkix import Rfc2119Word, name
from pkilint.itu import x520_name

_REQUIRED_ATTRIBUTES = {
_LEGAL_PERSON_REQUIRED_ATTRIBUTES = {
rfc5280.id_at_countryName,
rfc5280.id_at_organizationName,
x520_name.id_at_organizationIdentifier,
Expand All @@ -16,7 +13,7 @@


class LegalPersonSubjectAttributeAllowanceValidator(
common.AttributeIdentifierAllowanceValidator
etsi_shared.LegalPersonAttributeAllowanceValidator
):
"""
LEG-4.2.1-2: The subject field shall include at least the following attributes as specified in Recommendation
Expand All @@ -25,15 +22,17 @@ class LegalPersonSubjectAttributeAllowanceValidator(

_CODE_CLASSIFIER = "etsi.en_319_412_3.leg-4.2.1-2"

_ATTRIBUTE_ALLOWANCES = {a: Rfc2119Word.MUST for a in _REQUIRED_ATTRIBUTES}

def __init__(self):
super().__init__(
self._ATTRIBUTE_ALLOWANCES, self._CODE_CLASSIFIER, Rfc2119Word.MAY
self._CODE_CLASSIFIER,
_LEGAL_PERSON_REQUIRED_ATTRIBUTES,
"certificate.tbsCertificate.subject.rdnSequence",
)


class LegalPersonDuplicateAttributeAllowanceValidator(validation.Validator):
class LegalPersonDuplicateAttributeAllowanceValidator(
etsi_shared.LegalPersonDuplicateAttributeAllowanceValidator
):
"""
LEG-4.2.1-3: Only one instance of each of these attributes shall be present.
"""
Expand All @@ -45,22 +44,14 @@ class LegalPersonDuplicateAttributeAllowanceValidator(validation.Validator):

def __init__(self):
super().__init__(
validations=[self.VALIDATION_PROHIBITED_DUPLICATE_ATTRIBUTE_PRESENT],
pdu_class=rfc5280.Name,
self.VALIDATION_PROHIBITED_DUPLICATE_ATTRIBUTE_PRESENT,
_LEGAL_PERSON_REQUIRED_ATTRIBUTES,
)

def validate(self, node):
attr_counts = name.get_name_attribute_counts(node)

for a in _REQUIRED_ATTRIBUTES:
if attr_counts[a] > 1:
raise validation.ValidationFindingEncountered(
self.VALIDATION_PROHIBITED_DUPLICATE_ATTRIBUTE_PRESENT,
f"Prohibited duplicate attribute present: {a}",
)


class LegalPersonOrganizationAttributesEqualityValidator(validation.Validator):
class LegalPersonOrganizationAttributesEqualityValidator(
etsi_shared.LegalPersonOrganizationAttributesEqualityValidator
):
"""
LEG-4.2.1-6: The organizationIdentifier attribute shall contain an identification of the subject organization
different from the organization name.
Expand All @@ -72,44 +63,7 @@ class LegalPersonOrganizationAttributesEqualityValidator(validation.Validator):
)

def __init__(self):
super().__init__(
validations=[self.VALIDATION_ORGID_ORGNAME_ATTRIBUTE_VALUES_EQUAL],
pdu_class=rfc5280.Name,
)

def validate(self, node):
# only get the first instance of the attributes
orgname_attr_and_idx = next(
iter(
name.get_name_attributes_by_type(node, rfc5280.id_at_organizationName)
),
None,
)
orgid_attr_and_idx = next(
iter(
name.get_name_attributes_by_type(
node, x520_name.id_at_organizationIdentifier
)
),
None,
)

if orgname_attr_and_idx and orgid_attr_and_idx:
orgname_attr, _ = orgname_attr_and_idx
orgid_attr, _ = orgid_attr_and_idx

orgname = asn1_util.get_string_value_from_attribute_node(orgname_attr)
orgid = asn1_util.get_string_value_from_attribute_node(orgid_attr)

# if any of the attributes were not decoded, then return early
if orgname is None or orgid is None:
return

if orgname.casefold() == orgid.casefold():
raise validation.ValidationFindingEncountered(
self.VALIDATION_ORGID_ORGNAME_ATTRIBUTE_VALUES_EQUAL,
f'Organization name and identifier attribute values are equal: "{orgname}"',
)
super().__init__(self.VALIDATION_ORGID_ORGNAME_ATTRIBUTE_VALUES_EQUAL)


class LegalPersonKeyUsageValidator(etsi_shared.KeyUsageValidator):
Expand Down Expand Up @@ -139,7 +93,7 @@ def __init__(self, is_content_commitment_type):
)


class LegalPersonCountryCodeValidator(validation.Validator):
class LegalPersonCountryCodeValidator(etsi_shared.LegalPersonCountryCodeValidator):
"""
LEG-4.2.1-4: The countryName attribute shall specify the country in which the subject (legal person) is established.
"""
Expand All @@ -150,16 +104,4 @@ class LegalPersonCountryCodeValidator(validation.Validator):
)

def __init__(self):
super().__init__(
validations=[self.VALIDATION_UNKNOWN_COUNTRY_CODE],
pdu_class=rfc5280.X520countryName,
)

def validate(self, node):
value_str = str(node.pdu)

if value_str not in organization_id.ISO3166_1_COUNTRY_CODES:
raise validation.ValidationFindingEncountered(
self.VALIDATION_UNKNOWN_COUNTRY_CODE,
f'Unknown country code: "{value_str}"',
)
super().__init__(self.VALIDATION_UNKNOWN_COUNTRY_CODE)
45 changes: 38 additions & 7 deletions pkilint/etsi/en_319_412_5.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from iso3166 import countries_by_alpha2
from iso4217 import Currency
from urllib.parse import urlparse
from pyasn1_alt_modules import rfc3739
from pyasn1_alt_modules import rfc3739, rfc5280
from pkilint.pkix import extension, Rfc2119Word
import iso639

Expand Down Expand Up @@ -160,6 +160,8 @@ def __init__(self, certificate_type):

if certificate_type in etsi_constants.WEB_AUTHENTICATION_CERTIFICATE_TYPES:
self._expected_qc_type = en_319_412_5.id_etsi_qct_web
elif certificate_type in etsi_constants.QCP_N_CERTIFICATE_TYPES:
self._expected_qc_type = en_319_412_5.id_etsi_qct_esign
else:
self._expected_qc_type = None

Expand Down Expand Up @@ -307,6 +309,24 @@ def __init__(self):
)


class QcStatementPresenceValidator(extension.ExtensionPresenceValidator):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this class is needed, as the NaturalPersonExtensionIdentifierAllowanceValidator flags when the QCStatements extension is missing.

"""
QCS-5-01: EU qualified certificates shall include QCStatements in accordance with table 2
"""

VALIDATION_QC_STATEMENTS_MISSING = validation.ValidationFinding(
validation.ValidationFindingSeverity.ERROR,
"etsi.en_319_412_5.qcs-5.01",
)

def __init__(self):
super().__init__(
extension_oid=rfc3739.id_pe_qcStatements,
validation=self.VALIDATION_QC_STATEMENTS_MISSING,
pdu_class=rfc5280.Extensions,
)


class QcStatementIdentifierAllowanceValidator(
common.ElementIdentifierAllowanceValidator
):
Expand All @@ -332,17 +352,28 @@ def retrieve_qualified_statement_id(cls, node):
def __init__(self, certificate_type: etsi_constants.CertificateType):
allowances = {}

if certificate_type in etsi_constants.EU_QWAC_TYPES:
if certificate_type in etsi_constants.EU:
# Table 2: 4.2.1
allowances[en_319_412_5.id_etsi_qcs_QcCompliance] = Rfc2119Word.MUST
# Table 2: 4.2.4
allowances[en_319_412_5.id_etsi_qcs_QcCClegislation] = Rfc2119Word.MUST_NOT
allowances[en_319_412_5.id_etsi_qcs_QcType] = Rfc2119Word.MUST
# Table 2: 4.2.2
if certificate_type in etsi_constants.EU_SSCD:
allowances[en_319_412_5.id_etsi_qcs_QcSSCD] = Rfc2119Word.MUST

if (certificate_type in etsi_constants.EU_QWAC_TYPES) or (
certificate_type in etsi_constants.QCP_N_CERTIFICATE_TYPES
):
# Table 2: 4.2.3 (QWAC is Annex IV, signatures is Annex I)
allowances[en_319_412_5.id_etsi_qcs_QcType] = Rfc2119Word.MUST
if certificate_type in etsi_constants.EU_QWAC_TYPES:
# PR Question: Table 2, 4.2.2 only defines MUST, is the MUST_NOT also from 412-5 somewhere?
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this comment can be removed since the conversation is resolved.

allowances[en_319_412_5.id_etsi_qcs_QcSSCD] = Rfc2119Word.MUST_NOT
breynders-cb marked this conversation as resolved.
Show resolved Hide resolved

elif certificate_type in etsi_constants.NON_EU_QWAC_TYPES:
allowances[en_319_412_5.id_etsi_qcs_QcCompliance] = Rfc2119Word.MUST
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe this line needs to be restored. See EN 319 412 5, clause 4.2.1:

This Qcstatement claims:
...
ii) that the certificate is a certificate that is issued as qualified within a defined legal framework from an identified
country or set of countries.

...
The precise meaning of this statement is enhanced by:
...
b) the QcCClegislation statement defined in clause 4.2.4 according to table 1A.

# PR Question: Is this from 415_5.qcs-4.2? Needs different classifier?
allowances[en_319_412_5.id_etsi_qcs_QcCClegislation] = Rfc2119Word.MUST
Comment on lines +374 to 375
Copy link
Author

Choose a reason for hiding this comment

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

I was wondering what the source was for this rule. I couldn't really find it other than the reference in 412-5 QCS 4.2. If so, would it need a different source in the validation finding?

Copy link
Collaborator

Choose a reason for hiding this comment

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

See table 1A in EN 319 412 5, clause 4.2.1. The CCLegislation statement is needed for certs that are qualified but not in the EU.


if certificate_type in etsi_constants.QWAC_TYPES:
allowances[en_319_412_5.id_etsi_qcs_QcSSCD] = Rfc2119Word.MUST_NOT

super().__init__(
"qualified statement",
self.retrieve_qualified_statement_id,
Expand Down
Loading