Coverage for pki_tools/types/csr.py: 70%
77 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-11-02 22:00 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-11-02 22:00 +0000
1import re
3import yaml
4from cryptography import x509
6from pki_tools.exceptions import MissingInit
7from pki_tools.types.crypto_parser import (
8 CryptoConfig,
9 HelperFunc,
10 InitCryptoParser,
11)
12from pki_tools.types.extensions import Extensions
13from pki_tools.types.key_pair import (
14 CryptoKeyPair,
15 CryptoPrivateKey,
16 CryptoPublicKey,
17 Ed448PrivateKey,
18 Ed25519PrivateKey,
19)
20from pki_tools.types.name import Name
21from pki_tools.types.signature_algorithm import (
22 SignatureAlgorithm,
23)
24from pki_tools.types.utils import _byte_to_hex
26PEM_CSR_REGEX = re.compile(
27 r"\s*-+BEGIN CERTIFICATE REQUEST-+[\w+/\s=]*-+END CERTIFICATE REQUEST-+\s*"
28)
31class CertificateSigningRequest(InitCryptoParser):
32 """
33 Represents a certificate signing request (CSR).
35 Attributes:
36 subject: The subject of the CSR.
37 public_key: Public key associated with the CSR.
38 extensions: Extensions associated with the CSR.
39 attributes: Attributes of the CSR.
40 signature_algorithm: Signature algorithm used while signing the CSR.
42 --8<-- "docs/examples/csr.md"
43 """
45 subject: Name
47 public_key: CryptoPublicKey | None = None
48 extensions: Extensions | None = None
49 attributes: dict[str, bytes] | None = None
50 signature_algorithm: SignatureAlgorithm | None = None
52 _private_key: CryptoPrivateKey | None
54 @classmethod
55 def from_cryptography(
56 cls: type["CertificateSigningRequest"],
57 crypto_csr: x509.CertificateSigningRequest,
58 ) -> "CertificateSigningRequest":
59 """
60 Create a CertificateSigningRequest object from a cryptography
61 CertificateSigningRequest.
63 Args:
64 crypto_csr: Cryptography CertificateSigningRequest.
66 Returns:
67 Instance of CertificateSigningRequest.
69 --8<-- "docs/examples/csr_from_cryptography.md"
70 """
71 attributes = {}
72 for att in crypto_csr.attributes:
73 attributes[att.oid.dotted_string] = att.value
75 signature_algorithm = None
76 if crypto_csr.signature_hash_algorithm:
77 signature_algorithm = SignatureAlgorithm.from_cryptography(
78 crypto_csr.signature_hash_algorithm,
79 crypto_csr.signature_algorithm_parameters,
80 )
82 ret = cls(
83 subject=Name.from_cryptography(crypto_csr.subject),
84 extensions=Extensions.from_cryptography(crypto_csr.extensions),
85 signature_algorithm=signature_algorithm,
86 signature_value=_byte_to_hex(crypto_csr.signature),
87 public_key=CryptoPublicKey.from_cryptography(
88 crypto_csr.public_key()
89 ),
90 attributes=attributes,
91 _x509_obj=crypto_csr,
92 )
93 ret._x509_obj = crypto_csr
94 return ret
96 @property
97 def tbs_bytes(self) -> bytes:
98 """
99 Get the bytes to be signed of the CSR.
101 Returns:
102 TBS bytes of the CSR.
103 """
104 return self._crypto_object.tbs_certrequest_bytes
106 def sign(
107 self,
108 key_pair: CryptoKeyPair,
109 signature_algorithm: SignatureAlgorithm | None = None,
110 ):
111 """
112 Sign the CSR with the provided key pair and signature algorithm.
114 Args:
115 key_pair: Key pair with the private key to use while signing the
116 CSR
117 signature_algorithm: Signature algorithm to use for signing.
118 """
119 self._private_key = key_pair.private_key
120 self.signature_algorithm = signature_algorithm
121 self._x509_obj = self._to_cryptography()
123 def _string_dict(self):
124 ret = {
125 "Certificate Signing Request": {
126 "Subject": str(self.subject),
127 "Public Key": self.public_key._string_dict(),
128 }
129 }
130 if self.extensions:
131 ret["Extensions"] = self.extensions._string_dict()
132 if self.signature_algorithm is not None:
133 signature_alg = self.signature_algorithm.algorithm.name.value
134 ret["Signature Algorithm"] = signature_alg
135 if self.attributes is not None:
136 attributes = []
138 for k in self.attributes:
139 val = self.attributes[k]
140 if k == "1.2.840.113549.1.9.14":
141 val = _byte_to_hex(val)
142 elif k == "1.2.840.113549.1.9.7":
143 val = val.decode()
144 attributes.append(f"{k}: {val!s}")
146 if attributes:
147 ret["Certificate Signing Request"]["Attributes"] = attributes
148 return ret
150 def _to_cryptography(self) -> x509.CertificateSigningRequest:
151 if not hasattr(self, "_private_key"):
152 raise MissingInit(
153 f"Please use CertificateSigningRequest."
154 f"{self._init_func} function"
155 )
157 crypto_key = self._private_key._to_cryptography()
159 builder = x509.CertificateSigningRequestBuilder()
160 builder = builder.subject_name(self.subject._to_cryptography())
162 if self.extensions is not None:
163 for extension in self.extensions:
164 builder = builder.add_extension(
165 extension._to_cryptography(), extension.critical
166 )
168 if self.attributes is not None:
169 for attribute_oid, value in self.attributes.items():
170 oid = x509.ObjectIdentifier(attribute_oid)
171 builder = builder.add_attribute(oid, value)
173 if isinstance(self._private_key, (Ed448PrivateKey, Ed25519PrivateKey)):
174 alg = None
175 else:
176 alg = self.signature_algorithm.algorithm._to_cryptography()
178 return builder.sign(crypto_key, alg)
180 def __str__(self) -> str:
181 return yaml.safe_dump(
182 self._string_dict(),
183 indent=2,
184 default_flow_style=False,
185 explicit_start=True,
186 default_style="",
187 )
189 @classmethod
190 def _crypto_config(cls) -> CryptoConfig:
191 return CryptoConfig(
192 load_pem=HelperFunc(func=x509.load_pem_x509_csr),
193 load_der=HelperFunc(func=x509.load_der_x509_csr),
194 pem_regexp=PEM_CSR_REGEX,
195 )