Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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: 3 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/x509.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ class CertificateSigningRequest:
def tbs_certrequest_bytes(self) -> bytes: ...
@property
def is_signature_valid(self) -> bool: ...
def validate(

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.

This needs to be documented

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.

Looking at this more, why not refactor is_signature_valid to have it take an optional public key to handle this case?

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.

It can'tm because it's a property. But I do think it'd be more appropriate to name this verify_directly_signed_by or something so what it's checking is clear.

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.

Indeed, I considered allowing an optional public key (as with CertificateRevocationList) , but that would break existing code because the function would always evaluate to a truthy value.

I've renamed the validate function to verify_directly_signed_by, and aligned its behaviour with Certificate.verify_directly_issued_by: made the public key mandatory and raise exceptions instead of returning a boolean. I've also added appropriate test cases.

self, public_key: CertificateIssuerPublicKeyTypes | None = None
) -> bool: ...

class PolicyBuilder:
def time(self, time: datetime.datetime) -> PolicyBuilder: ...
Expand Down
48 changes: 46 additions & 2 deletions src/cryptography/x509/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,15 @@ class CertificateSigningRequestBuilder:
def __init__(
self,
subject_name: Name | None = None,
public_key: CertificatePublicKeyTypes | None = None,
extensions: list[Extension[ExtensionType]] = [],
attributes: list[tuple[ObjectIdentifier, bytes, int | None]] = [],
):
"""
Creates an empty X.509 certificate request (v1).
"""
self._subject_name = subject_name
self._public_key = public_key
self._extensions = extensions
self._attributes = attributes

Expand All @@ -202,7 +204,45 @@ def subject_name(self, name: Name) -> CertificateSigningRequestBuilder:
if self._subject_name is not None:
raise ValueError("The subject name may only be set once.")
return CertificateSigningRequestBuilder(
name, self._extensions, self._attributes
name, self._public_key, self._extensions, self._attributes
)

def public_key(

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.

This needs to be documented (with teh same caveats of why you'd need it)

self,
public_key: CertificatePublicKeyTypes,
) -> CertificateSigningRequestBuilder:
"""
Sets the requestor's public key.
"""
if not isinstance(
public_key,
(
dsa.DSAPublicKey,
rsa.RSAPublicKey,
ec.EllipticCurvePublicKey,
ed25519.Ed25519PublicKey,
ed448.Ed448PublicKey,
mldsa.MLDSA44PublicKey,
mldsa.MLDSA65PublicKey,
mldsa.MLDSA87PublicKey,
mlkem.MLKEM768PublicKey,
mlkem.MLKEM1024PublicKey,
x25519.X25519PublicKey,
x448.X448PublicKey,
),
):
raise TypeError(
"Expecting one of DSAPublicKey, RSAPublicKey,"
" EllipticCurvePublicKey, Ed25519PublicKey,"
" Ed448PublicKey, MLDSA44PublicKey, MLDSA65PublicKey,"
" MLDSA87PublicKey, MLKEM768PublicKey, MLKEM1024PublicKey,"
" X25519PublicKey or X448PublicKey."
)

if self._public_key is not None:
raise ValueError("The public key may only be set once.")
return CertificateSigningRequestBuilder(
self._subject_name, public_key, self._extensions, self._attributes
)

def add_extension(
Expand All @@ -219,6 +259,7 @@ def add_extension(

return CertificateSigningRequestBuilder(
self._subject_name,
self._public_key,
[*self._extensions, extension],
self._attributes,
)
Expand Down Expand Up @@ -251,6 +292,7 @@ def add_attribute(

return CertificateSigningRequestBuilder(
self._subject_name,
self._public_key,
self._extensions,
[*self._attributes, (oid, value, tag)],
)
Expand All @@ -265,7 +307,9 @@ def sign(
ecdsa_deterministic: bool | None = None,
) -> CertificateSigningRequest:
"""
Signs the request using the requestor's private key.
Signs the request using the requestor's private key. If no public key
was indicated, the public key associated with specified private key
will be included instead.
"""
if self._subject_name is None:
raise ValueError("A CertificateSigningRequest must have a subject")
Expand Down
20 changes: 17 additions & 3 deletions src/rust/src/x509/csr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,17 @@ impl CertificateSigningRequest {

#[getter]
fn is_signature_valid(&self, py: pyo3::Python<'_>) -> CryptographyResult<bool> {
let public_key = self.public_key(py)?;
self.validate(py, None)
}

#[pyo3(signature = (public_key = None))]
fn validate<'p>(
&self,
py: pyo3::Python<'p>,
public_key: Option<pyo3::Bound<'p, pyo3::PyAny>>,
) -> CryptographyResult<bool> {
let public_key = public_key.unwrap_or(self.public_key(py)?);

Ok(sign::verify_signature_with_signature_algorithm(
py,
public_key,
Expand Down Expand Up @@ -265,8 +275,12 @@ pub(crate) fn create_x509_csr(
rsa_padding.clone(),
)?;

let spki_bytes = private_key
.call_method0(pyo3::intern!(py, "public_key"))?
let public_key = match builder.getattr(pyo3::intern!(py, "_public_key"))? {
pk if pk.is_none() => private_key.call_method0(pyo3::intern!(py, "public_key"))?,
pk => pk,
};

let spki_bytes = public_key
.call_method1(
pyo3::intern!(py, "public_bytes"),
(
Expand Down
149 changes: 149 additions & 0 deletions tests/x509/test_x509.py
Original file line number Diff line number Diff line change
Expand Up @@ -2037,6 +2037,29 @@ def test_admissions_extension(self):
assert ext.value == x509.Admissions(authority=None, admissions=[])


class TestCertificateRequest:
def test_public_key_must_be_public_key(
self, rsa_key_2048: rsa.RSAPrivateKey
):
private_key = rsa_key_2048
builder = x509.CertificateSigningRequestBuilder()

with pytest.raises(TypeError):
builder.public_key(typing.cast(typing.Any, private_key))

def test_public_key_may_only_be_set_once(
self, rsa_key_2048: rsa.RSAPrivateKey
):
private_key = rsa_key_2048
public_key = private_key.public_key()
builder = x509.CertificateSigningRequestBuilder().public_key(
public_key
)

with pytest.raises(ValueError):
builder.public_key(public_key)


class TestRSACertificateRequest:
@pytest.mark.parametrize(
("path", "loader_func"),
Expand Down Expand Up @@ -6391,6 +6414,132 @@ def test_tbs_certrequest_bytes(self, backend):
)


class TestMLKEMCertificateRequest:
@pytest.mark.supported(
only_if=lambda backend: (
backend.mldsa_supported() and backend.mlkem_supported()
),
skip_message="Does not support ML-DSA and/or ML-KEM",
)
@pytest.mark.parametrize(
(
"enclosed_key_path",
"enclosed_pub_key_cls",
"signing_pub_key_path",
"signing_pub_key_cls",
"signature_algorithm_oid",
),
[
(
os.path.join("x509", "requests", "mldsa-mlkem768.pem"),
mlkem.MLKEM768PublicKey,
os.path.join(
"x509", "requests", "mldsa-mlkem768-signing-pubkey.pem"
),
mldsa.MLDSA65PublicKey,
SignatureAlgorithmOID.ML_DSA_65,
),
(
os.path.join("x509", "requests", "mldsa-mlkem1024.pem"),
mlkem.MLKEM1024PublicKey,
os.path.join(
"x509", "requests", "mldsa-mlkem1024-signing-pubkey.pem"
),
mldsa.MLDSA65PublicKey,
SignatureAlgorithmOID.ML_DSA_65,
),
],
)
def test_load_request_mldsa_mlkem(
self,
enclosed_key_path,
enclosed_pub_key_cls,
signing_pub_key_path,
signing_pub_key_cls,
signature_algorithm_oid,
):
request = _load_cert(enclosed_key_path, x509.load_pem_x509_csr)

signing_key = _load_cert(
signing_pub_key_path, serialization.load_pem_public_key
)

assert isinstance(request.public_key(), enclosed_pub_key_cls)
assert isinstance(signing_key, signing_pub_key_cls)

assert request.signature_algorithm_oid == signature_algorithm_oid

assert isinstance(request.subject, x509.Name)
assert list(request.subject) == [
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
]

assert not request.is_signature_valid
assert not request.validate()
assert request.validate(signing_key)

@pytest.mark.supported(
only_if=lambda backend: (
backend.mldsa_supported() and backend.mlkem_supported()
),
skip_message="Does not support ML-DSA and/or ML-KEM",
)
@pytest.mark.parametrize(
(
"enclosed_key_cls",
"enclosed_pub_key_cls",
"signing_key_cls",
"signature_algorithm_oid",
),
[
(
mlkem.MLKEM768PrivateKey,
mlkem.MLKEM768PublicKey,
mldsa.MLDSA65PrivateKey,
SignatureAlgorithmOID.ML_DSA_65,
),
(
mlkem.MLKEM1024PrivateKey,
mlkem.MLKEM1024PublicKey,
mldsa.MLDSA65PrivateKey,
SignatureAlgorithmOID.ML_DSA_65,
),
],
)
def test_build_request_mldsa_mlkem(
self,
enclosed_key_cls,
enclosed_pub_key_cls,
signing_key_cls,
signature_algorithm_oid,
):
enclosed_key = enclosed_key_cls.generate()
signing_key = signing_key_cls.generate()

request = (
x509.CertificateSigningRequestBuilder()
.subject_name(
x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "US")])
)
.public_key(enclosed_key.public_key())
.sign(signing_key, None)
)

public_key = request.public_key()
assert isinstance(public_key, enclosed_pub_key_cls)

assert request.signature_algorithm_oid == signature_algorithm_oid

assert isinstance(request.subject, x509.Name)
assert list(request.subject) == [
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
]

assert not request.is_signature_valid
assert not request.validate()
assert request.validate(signing_key.public_key())


class TestOtherCertificate:
def test_unsupported_subject_public_key_info(self):
cert = _load_cert(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-----BEGIN PUBLIC KEY-----

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.

All the vectors need to be documented in test-vectors.rst.

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 wasn't aware of that, I now have added descriptions.

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
vCZ/GVKv
-----END PUBLIC KEY-----
Loading