Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,12 @@ mod ee {
"EE keyUsage must not assert keyCertSign".to_string(),
)));
}

if !key_usage.digital_signature() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Note: CABF 7.1.2.7.11 requires digitalSignature for subscriber certs (i.e. EEs) that convey ECC keys, but does not require it for subscriber certs that convey RSA keys:

Image

This is an incredible mess in practice: the BR says that an RSA-conveying subscriber cert MUST have at least one keyUsage, but no specific one is a MUST in practice unless the validator knows the TLS version being used.

Given that, I think we have two options here:

  1. Continue to just enforce digitalSignature. This...probably works fine in practice, even though it's technically stricter than what the BRs say?
  2. Differentiate this check based on the subscriber's key type: for ECC keys it should be the current check, while for RSA keys it should at be at least one of digitalSignature | keyEncipherment | dataEncipherment.

Curious what @alex and @reaperhulk think about this as well 🙂

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess I'm still unsure if we want to enforce this at all, or if we want to allow configuring it on the policy builder? (Or just require users to provide their own extension validator for KU)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it makes sense to enforce in our default policy, since we're otherwise relatively strict about CABF (and the limbo results suggest that other validators check this). But it is indeed somewhat annoying to enforce correctly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we know if the other validators are key-type aware?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we know if the other validators are key-type aware?

Didn't test that yet, will do so tonight 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes the word "modern" was doing perhaps a bit too much heavy lifting in my initial description.

I've now added distinct handling for RSA that considers keyEncipherment sufficient (and allows both, despite being NOT RECOMMENDED) to be more permissive for the default validation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@woodruffw did you get a chance to test this?

return Err(ValidationError::new(ValidationErrorKind::Other(
"EE keyUsage must assert digitalSignature when present".to_string(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

At this point we have a keyUsage, so "when present" feels redundant 🙂

Suggested change
"EE keyUsage must assert digitalSignature when present".to_string(),
"EE keyUsage must assert digitalSignature".to_string(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've cut this.

)));
}
}

Ok(())
Expand Down
134 changes: 134 additions & 0 deletions tests/x509/verification/test_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,140 @@ def test_error_message(self):
verifier.verify(leaf, [])


def _key_usage(
*,
digital_signature=False,
key_encipherment=False,
key_cert_sign=False,
crl_sign=False,
) -> x509.KeyUsage:
return x509.KeyUsage(
digital_signature=digital_signature,
content_commitment=False,
key_encipherment=key_encipherment,
data_encipherment=False,
key_agreement=False,
key_cert_sign=key_cert_sign,
crl_sign=crl_sign,
encipher_only=False,
decipher_only=False,
)


def _chain_with_leaf_key_usage(
key_usage: Optional[x509.KeyUsage],
):
ca_key = ec.generate_private_key(ec.SECP256R1())
leaf_key = ec.generate_private_key(ec.SECP256R1())

not_before = datetime.datetime(2024, 1, 1)
not_after = datetime.datetime(2034, 1, 1)
validation_time = datetime.datetime(2025, 1, 1)

ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Test CA")])
ca = (
x509.CertificateBuilder()
.subject_name(ca_name)
.issuer_name(ca_name)
.public_key(ca_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(not_before)
.not_valid_after(not_after)
.add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True,
)
.add_extension(
_key_usage(key_cert_sign=True, crl_sign=True),
critical=True,
)
.sign(ca_key, hashes.SHA256())
)

leaf_name = x509.Name(
[x509.NameAttribute(NameOID.COMMON_NAME, "example.com")]
)
leaf_builder = (
x509.CertificateBuilder()
.subject_name(leaf_name)
.issuer_name(ca_name)
.public_key(leaf_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(not_before)
.not_valid_after(not_after)
.add_extension(
x509.SubjectAlternativeName([x509.DNSName("example.com")]),
critical=False,
)
.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(
ca_key.public_key()
),
critical=False,
)
)
if key_usage is not None:
leaf_builder = leaf_builder.add_extension(key_usage, critical=True)

return ca, leaf_builder.sign(ca_key, hashes.SHA256()), validation_time


@pytest.mark.parametrize("verifier_kind", ["server", "client"])
@pytest.mark.parametrize(
("key_usage", "error"),
[
(None, None),
(_key_usage(digital_signature=True), None),
(
_key_usage(key_encipherment=True),
"EE keyUsage must assert digitalSignature when present",
),
(
_key_usage(digital_signature=True, key_cert_sign=True),
"EE keyUsage must not assert keyCertSign",
),
],
)
def test_default_ee_key_usage(verifier_kind, key_usage, error):
ca, leaf, validation_time = _chain_with_leaf_key_usage(key_usage)
builder = PolicyBuilder().store(Store([ca])).time(validation_time)

def verify_leaf():
if verifier_kind == "server":
builder.build_server_verifier(x509.DNSName("example.com")).verify(
leaf, []
)
else:
builder.build_client_verifier().verify(leaf, [])

if error is None:
verify_leaf()
else:
with pytest.raises(VerificationError, match=error):
verify_leaf()


def test_default_ee_key_usage_can_be_overridden():
ca, leaf, validation_time = _chain_with_leaf_key_usage(
_key_usage(key_encipherment=True)
)
ee_policy = ExtensionPolicy.webpki_defaults_ee().may_be_present(
x509.KeyUsage, Criticality.AGNOSTIC, None
)
builder = (
PolicyBuilder()
.store(Store([ca]))
.time(validation_time)
.extension_policies(
ca_policy=ExtensionPolicy.webpki_defaults_ca(),
ee_policy=ee_policy,
)
)

verifier = builder.build_server_verifier(x509.DNSName("example.com"))
verifier.verify(leaf, [])


SUPPORTED_EXTENSION_TYPES = (
x509.AuthorityInformationAccess,
x509.AuthorityKeyIdentifier,
Expand Down
Loading