diff options
| author | Alex Gaynor <alex.gaynor@gmail.com> | 2021-10-24 20:19:53 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-10-25 08:19:53 +0800 |
| commit | 16bfb57fc7ae67cab59c3dfb9338d33ba4f6e681 (patch) | |
| tree | 8d5e8146e43f22bcaa9cdeb0c62c4806e88433be /src | |
| parent | 6f20e0674a075930324fba012cf14c2a56fd742d (diff) | |
| download | cryptography-16bfb57fc7ae67cab59c3dfb9338d33ba4f6e681.tar.gz | |
Port OCSP Response generation to Rust (#6460)
* xxx
* The rest
* file
* first milestone!
* progress
* Good progress
* Aaaand, tests pass!
* linter fixes
* moar linting
* moar linting
* style on that coverage
* Flesh this out
* reformat
* Remove RSA+DSA support, will be added back later
* Refactor to avoid todo!() branch
* sha384 support
* Unused
* Suggesting I learn to spell? It's a bold move cotton, let's see how it pays off
Diffstat (limited to 'src')
| -rw-r--r-- | src/_cffi_src/build_openssl.py | 1 | ||||
| -rw-r--r-- | src/_cffi_src/openssl/ocsp.py | 40 | ||||
| -rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 123 | ||||
| -rw-r--r-- | src/cryptography/hazmat/bindings/_rust/ocsp.pyi | 13 | ||||
| -rw-r--r-- | src/cryptography/x509/ocsp.py | 8 | ||||
| -rw-r--r-- | src/rust/Cargo.lock | 2 | ||||
| -rw-r--r-- | src/rust/Cargo.toml | 2 | ||||
| -rw-r--r-- | src/rust/src/x509/certificate.rs | 10 | ||||
| -rw-r--r-- | src/rust/src/x509/common.rs | 6 | ||||
| -rw-r--r-- | src/rust/src/x509/mod.rs | 1 | ||||
| -rw-r--r-- | src/rust/src/x509/ocsp_resp.rs | 222 | ||||
| -rw-r--r-- | src/rust/src/x509/sign.rs | 137 |
12 files changed, 368 insertions, 197 deletions
diff --git a/src/_cffi_src/build_openssl.py b/src/_cffi_src/build_openssl.py index 2ee3f4bff..95e9d138e 100644 --- a/src/_cffi_src/build_openssl.py +++ b/src/_cffi_src/build_openssl.py @@ -99,7 +99,6 @@ ffi = build_ffi_for_binding( "hmac", "nid", "objects", - "ocsp", "opensslv", "osrandom_engine", "pem", diff --git a/src/_cffi_src/openssl/ocsp.py b/src/_cffi_src/openssl/ocsp.py deleted file mode 100644 index 093805a1c..000000000 --- a/src/_cffi_src/openssl/ocsp.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file is dual licensed under the terms of the Apache License, Version -# 2.0, and the BSD License. See the LICENSE file in the root of this repository -# for complete details. - - -INCLUDES = """ -#include <openssl/ocsp.h> -""" - -TYPES = """ -typedef ... OCSP_ONEREQ; -typedef ... OCSP_RESPONSE; -typedef ... OCSP_BASICRESP; -typedef ... OCSP_SINGLERESP; -typedef ... OCSP_CERTID; -static const long OCSP_NOCERTS; -static const long OCSP_RESPID_KEY; -""" - -FUNCTIONS = """ -OCSP_CERTID *OCSP_cert_to_id(const EVP_MD *, const X509 *, const X509 *); -void OCSP_CERTID_free(OCSP_CERTID *); - -OCSP_BASICRESP *OCSP_BASICRESP_new(void); -void OCSP_BASICRESP_free(OCSP_BASICRESP *); -OCSP_SINGLERESP *OCSP_basic_add1_status(OCSP_BASICRESP *, OCSP_CERTID *, int, - int, ASN1_TIME *, ASN1_TIME *, - ASN1_TIME *); -int OCSP_basic_add1_cert(OCSP_BASICRESP *, X509 *); -int OCSP_BASICRESP_add_ext(OCSP_BASICRESP *, X509_EXTENSION *, int); -int OCSP_basic_sign(OCSP_BASICRESP *, X509 *, EVP_PKEY *, const EVP_MD *, - Cryptography_STACK_OF_X509 *, unsigned long); -OCSP_RESPONSE *OCSP_response_create(int, OCSP_BASICRESP *); -void OCSP_RESPONSE_free(OCSP_RESPONSE *); - -int i2d_OCSP_RESPONSE_bio(BIO *, OCSP_RESPONSE *); -""" - -CUSTOMIZATIONS = """ -""" diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 764f8b781..23cebc892 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -16,9 +16,6 @@ from cryptography.hazmat.backends.interfaces import Backend as BackendInterface from cryptography.hazmat.backends.openssl import aead from cryptography.hazmat.backends.openssl.ciphers import _CipherContext from cryptography.hazmat.backends.openssl.cmac import _CMACContext -from cryptography.hazmat.backends.openssl.decode_asn1 import ( - _CRL_ENTRY_REASON_ENUM_TO_CODE, -) from cryptography.hazmat.backends.openssl.dh import ( _DHParameters, _DHPrivateKey, @@ -71,7 +68,6 @@ from cryptography.hazmat.backends.openssl.x509 import ( _RawRevokedCertificate, ) from cryptography.hazmat.bindings._rust import ( - ocsp as rust_ocsp, x509 as rust_x509, ) from cryptography.hazmat.bindings.openssl import binding @@ -119,7 +115,6 @@ from cryptography.hazmat.primitives.serialization.pkcs12 import ( PKCS12Certificate, PKCS12KeyAndCertificates, ) -from cryptography.x509 import ocsp from cryptography.x509.base import PUBLIC_KEY_TYPES @@ -1380,6 +1375,10 @@ class Backend(BackendInterface): return True + def _check_keys_correspond(self, key1, key2): + if self._lib.EVP_PKEY_cmp(key1._evp_pkey, key2._evp_pkey) != 1: + raise ValueError("Keys do not correspond") + def _load_key(self, openssl_read_func, convert_func, data, password): mem_bio = self._bytes_to_bio(data) @@ -1616,120 +1615,6 @@ class Backend(BackendInterface): self.openssl_assert(ec_cdata != self._ffi.NULL) return self._ffi.gc(ec_cdata, self._lib.EC_KEY_free) - def _create_ocsp_basic_response(self, builder, private_key, algorithm): - self._x509_check_signature_params(private_key, algorithm) - - basic = self._lib.OCSP_BASICRESP_new() - self.openssl_assert(basic != self._ffi.NULL) - basic = self._ffi.gc(basic, self._lib.OCSP_BASICRESP_free) - evp_md = self._evp_md_non_null_from_algorithm( - builder._response._algorithm - ) - ossl_cert = self._cert2ossl(builder._response._cert) - ossl_issuer = self._cert2ossl(builder._response._issuer) - certid = self._lib.OCSP_cert_to_id( - evp_md, - ossl_cert, - ossl_issuer, - ) - self.openssl_assert(certid != self._ffi.NULL) - certid = self._ffi.gc(certid, self._lib.OCSP_CERTID_free) - if builder._response._revocation_reason is None: - reason = -1 - else: - reason = _CRL_ENTRY_REASON_ENUM_TO_CODE[ - builder._response._revocation_reason - ] - if builder._response._revocation_time is None: - rev_time = self._ffi.NULL - else: - rev_time = self._create_asn1_time_gc( - builder._response._revocation_time - ) - - next_update = self._ffi.NULL - if builder._response._next_update is not None: - next_update = self._create_asn1_time_gc( - builder._response._next_update - ) - - this_update = self._create_asn1_time_gc(builder._response._this_update) - - res = self._lib.OCSP_basic_add1_status( - basic, - certid, - builder._response._cert_status.value, - reason, - rev_time, - this_update, - next_update, - ) - self.openssl_assert(res != self._ffi.NULL) - # okay, now sign the basic structure - evp_md = self._evp_md_x509_null_if_eddsa(private_key, algorithm) - responder_cert, responder_encoding = builder._responder_id - flags = self._lib.OCSP_NOCERTS - if responder_encoding is ocsp.OCSPResponderEncoding.HASH: - flags |= self._lib.OCSP_RESPID_KEY - - # This list is to keep the x509 values alive until end of function - ossl_certs = [] - if builder._certs is not None: - for cert in builder._certs: - ossl_cert = self._cert2ossl(cert) - ossl_certs.append(ossl_cert) - res = self._lib.OCSP_basic_add1_cert(basic, ossl_cert) - self.openssl_assert(res == 1) - - self._create_x509_extensions( - extensions=builder._extensions, - rust_handler=rust_ocsp.encode_ocsp_basic_response_extension, - x509_obj=basic, - add_func=self._lib.OCSP_BASICRESP_add_ext, - gc=True, - ) - - ossl_cert = self._cert2ossl(responder_cert) - res = self._lib.OCSP_basic_sign( - basic, - ossl_cert, - private_key._evp_pkey, - evp_md, - self._ffi.NULL, - flags, - ) - if res != 1: - errors = self._consume_errors_with_text() - raise ValueError( - "Error while signing. responder_cert must be signed " - "by private_key", - errors, - ) - - return basic - - def create_ocsp_response( - self, response_status, builder, private_key, algorithm - ): - if response_status is ocsp.OCSPResponseStatus.SUCCESSFUL: - basic = self._create_ocsp_basic_response( - builder, private_key, algorithm - ) - else: - basic = self._ffi.NULL - - ocsp_resp = self._lib.OCSP_response_create( - response_status.value, basic - ) - self.openssl_assert(ocsp_resp != self._ffi.NULL) - ocsp_resp = self._ffi.gc(ocsp_resp, self._lib.OCSP_RESPONSE_free) - - bio = self._create_mem_bio_gc() - res = self._lib.i2d_OCSP_RESPONSE_bio(bio, ocsp_resp) - self.openssl_assert(res > 0) - data = self._read_mem_bio(bio) - return ocsp.load_der_ocsp_response(data) - def elliptic_curve_exchange_algorithm_supported(self, algorithm, curve): if self._fips_enabled and not isinstance( curve, self._fips_ecdh_curves diff --git a/src/cryptography/hazmat/bindings/_rust/ocsp.pyi b/src/cryptography/hazmat/bindings/_rust/ocsp.pyi index 3ccb48e5e..91b84955f 100644 --- a/src/cryptography/hazmat/bindings/_rust/ocsp.pyi +++ b/src/cryptography/hazmat/bindings/_rust/ocsp.pyi @@ -1,11 +1,22 @@ +import typing + +from cryptography.hazmat.primitives.asymmetric.types import PRIVATE_KEY_TYPES +from cryptography.hazmat.primitives import hashes from cryptography.x509 import Extension from cryptography.x509.ocsp import ( OCSPRequest, OCSPRequestBuilder, OCSPResponse, + OCSPResponseStatus, + OCSPResponseBuilder, ) def load_der_ocsp_request(data: bytes) -> OCSPRequest: ... def load_der_ocsp_response(data: bytes) -> OCSPResponse: ... -def encode_ocsp_basic_response_extension(ext: Extension) -> bytes: ... def create_ocsp_request(builder: OCSPRequestBuilder) -> OCSPRequest: ... +def create_ocsp_response( + status: OCSPResponseStatus, + builder: typing.Optional[OCSPResponseBuilder], + private_key: typing.Optional[PRIVATE_KEY_TYPES], + hash_algorithm: typing.Optional[hashes.HashAlgorithm], +) -> OCSPResponse: ... diff --git a/src/cryptography/x509/ocsp.py b/src/cryptography/x509/ocsp.py index 1c02c2945..8eec4c0c6 100644 --- a/src/cryptography/x509/ocsp.py +++ b/src/cryptography/x509/ocsp.py @@ -452,14 +452,12 @@ class OCSPResponseBuilder(object): private_key: PRIVATE_KEY_TYPES, algorithm: typing.Optional[hashes.HashAlgorithm], ) -> OCSPResponse: - from cryptography.hazmat.backends.openssl.backend import backend - if self._response is None: raise ValueError("You must add a response before signing") if self._responder_id is None: raise ValueError("You must add a responder_id before signing") - return backend.create_ocsp_response( + return ocsp.create_ocsp_response( OCSPResponseStatus.SUCCESSFUL, self, private_key, algorithm ) @@ -467,8 +465,6 @@ class OCSPResponseBuilder(object): def build_unsuccessful( cls, response_status: OCSPResponseStatus ) -> OCSPResponse: - from cryptography.hazmat.backends.openssl.backend import backend - if not isinstance(response_status, OCSPResponseStatus): raise TypeError( "response_status must be an item from OCSPResponseStatus" @@ -476,7 +472,7 @@ class OCSPResponseBuilder(object): if response_status is OCSPResponseStatus.SUCCESSFUL: raise ValueError("response_status cannot be SUCCESSFUL") - return backend.create_ocsp_response(response_status, None, None, None) + return ocsp.create_ocsp_response(response_status, None, None, None) def load_der_ocsp_request(data: bytes) -> OCSPRequest: diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 5b75a3623..d695da4a5 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -65,8 +65,10 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ + "libc", "num-integer", "num-traits", + "winapi", ] [[package]] diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 4dd1b429d..62bc96057 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -10,7 +10,7 @@ lazy_static = "1" pyo3 = { version = "0.14.5" } asn1 = { version = "0.8.4", default-features = false, features = ["derive"] } pem = "1.0" -chrono = { version = "0.4", default-features = false, features = ["alloc"] } +chrono = { version = "0.4", default-features = false, features = ["alloc", "clock"] } ouroboros = "0.13" [features] diff --git a/src/rust/src/x509/certificate.rs b/src/rust/src/x509/certificate.rs index 8fe9b23ac..175dbb1ed 100644 --- a/src/rust/src/x509/certificate.rs +++ b/src/rust/src/x509/certificate.rs @@ -34,14 +34,14 @@ lazy_static::lazy_static! { static ref NAME_CONSTRAINTS_OID: asn1::ObjectIdentifier<'static> = asn1::ObjectIdentifier::from_string("2.5.29.30").unwrap(); } -#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq)] +#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq, Clone)] pub(crate) struct RawCertificate<'a> { pub(crate) tbs_cert: TbsCertificate<'a>, signature_alg: x509::AlgorithmIdentifier<'a>, signature: asn1::BitString<'a>, } -#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq)] +#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq, Clone)] pub(crate) struct TbsCertificate<'a> { #[explicit(0)] #[default(0)] @@ -51,7 +51,7 @@ pub(crate) struct TbsCertificate<'a> { pub(crate) issuer: x509::Name<'a>, validity: Validity, - subject: x509::Name<'a>, + pub(crate) subject: x509::Name<'a>, pub(crate) spki: SubjectPublicKeyInfo<'a>, #[implicit(1)] @@ -62,13 +62,13 @@ pub(crate) struct TbsCertificate<'a> { extensions: Option<x509::Extensions<'a>>, } -#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq)] +#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq, Clone)] pub(crate) struct Validity { not_before: x509::Time, not_after: x509::Time, } -#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq)] +#[derive(asn1::Asn1Read, asn1::Asn1Write, Hash, PartialEq, Clone)] pub(crate) struct SubjectPublicKeyInfo<'a> { _algorithm: x509::AlgorithmIdentifier<'a>, pub(crate) subject_public_key: asn1::BitString<'a>, diff --git a/src/rust/src/x509/common.rs b/src/rust/src/x509/common.rs index 3a071c65c..0dfb74cd9 100644 --- a/src/rust/src/x509/common.rs +++ b/src/rust/src/x509/common.rs @@ -43,7 +43,7 @@ pub(crate) type Name<'a> = Asn1ReadableOrWritable< >, >; -#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Hash)] +#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Hash, Clone)] pub(crate) struct AttributeTypeValue<'a> { pub(crate) type_id: asn1::ObjectIdentifier<'a>, pub(crate) value: RawTlv<'a>, @@ -51,7 +51,7 @@ pub(crate) struct AttributeTypeValue<'a> { // Like `asn1::Tlv` but doesn't store `full_data` so it can be constucted from // an un-encoded tag and value. -#[derive(Hash, PartialEq)] +#[derive(Hash, PartialEq, Clone)] pub(crate) struct RawTlv<'a> { tag: u8, value: &'a [u8], @@ -331,7 +331,7 @@ pub(crate) type Extensions<'a> = Asn1ReadableOrWritable< asn1::SequenceOfWriter<'a, Extension<'a>, Vec<Extension<'a>>>, >; -#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Hash)] +#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Hash, Clone)] pub(crate) struct AlgorithmIdentifier<'a> { pub(crate) oid: asn1::ObjectIdentifier<'a>, pub(crate) params: Option<asn1::Tlv<'a>>, diff --git a/src/rust/src/x509/mod.rs b/src/rust/src/x509/mod.rs index 33136741d..f8876c003 100644 --- a/src/rust/src/x509/mod.rs +++ b/src/rust/src/x509/mod.rs @@ -10,6 +10,7 @@ mod ocsp; pub(crate) mod ocsp_req; pub(crate) mod ocsp_resp; pub(crate) mod sct; +pub(crate) mod sign; pub(crate) use certificate::Certificate; pub(crate) use common::{ diff --git a/src/rust/src/x509/ocsp_resp.rs b/src/rust/src/x509/ocsp_resp.rs index d72b29b8a..0b7431ed5 100644 --- a/src/rust/src/x509/ocsp_resp.rs +++ b/src/rust/src/x509/ocsp_resp.rs @@ -4,7 +4,7 @@ use crate::asn1::{big_asn1_uint_to_py, PyAsn1Error, PyAsn1Result}; use crate::x509; -use crate::x509::{certificate, crl, ocsp, sct}; +use crate::x509::{certificate, crl, ocsp, py_to_chrono, sct}; use std::sync::Arc; lazy_static::lazy_static! { @@ -45,7 +45,11 @@ fn load_der_ocsp_response(_py: pyo3::Python<'_>, data: &[u8]) -> Result<OCSPResp )?; if let Some(basic_response) = raw.borrow_basic_response() { - let num_responses = basic_response.tbs_response_data.responses.len(); + let num_responses = basic_response + .tbs_response_data + .responses + .unwrap_read() + .len(); if num_responses != 1 { return Err(PyAsn1Error::from( pyo3::exceptions::PyValueError::new_err(format!( @@ -207,7 +211,7 @@ impl OCSPResponse { let resp = self.requires_successful_response()?; let py_certs = pyo3::types::PyList::empty(py); let certs = match &resp.certs { - Some(certs) => certs, + Some(certs) => certs.unwrap_read(), None => return Ok(py_certs), }; for i in 0..certs.len() { @@ -219,6 +223,7 @@ impl OCSPResponse { .certs .as_ref() .unwrap() + .unwrap_read() .clone() .nth(i) .unwrap() @@ -444,18 +449,35 @@ struct ResponseBytes<'a> { response: &'a [u8], } -#[derive(asn1::Asn1Read)] +type OCSPCerts<'a> = Option< + x509::Asn1ReadableOrWritable< + 'a, + asn1::SequenceOf<'a, certificate::RawCertificate<'a>>, + asn1::SequenceOfWriter< + 'a, + certificate::RawCertificate<'a>, + Vec<certificate::RawCertificate<'a>>, + >, + >, +>; + +#[derive(asn1::Asn1Read, asn1::Asn1Write)] struct BasicOCSPResponse<'a> { tbs_response_data: ResponseData<'a>, signature_algorithm: x509::AlgorithmIdentifier<'a>, signature: asn1::BitString<'a>, #[explicit(0)] - certs: Option<asn1::SequenceOf<'a, certificate::RawCertificate<'a>>>, + certs: OCSPCerts<'a>, } impl BasicOCSPResponse<'_> { fn single_response(&self) -> SingleResponse<'_> { - self.tbs_response_data.responses.clone().next().unwrap() + self.tbs_response_data + .responses + .unwrap_read() + .clone() + .next() + .unwrap() } } @@ -466,7 +488,11 @@ struct ResponseData<'a> { version: u8, responder_id: ResponderId<'a>, produced_at: asn1::GeneralizedTime, - responses: asn1::SequenceOf<'a, SingleResponse<'a>>, + responses: x509::Asn1ReadableOrWritable< + 'a, + asn1::SequenceOf<'a, SingleResponse<'a>>, + asn1::SequenceOfWriter<'a, SingleResponse<'a>, Vec<SingleResponse<'a>>>, + >, #[explicit(1)] response_extensions: Option<x509::Extensions<'a>>, } @@ -507,27 +533,181 @@ struct RevokedInfo { revocation_reason: Option<crl::CRLReason>, } +fn create_ocsp_basic_response<'p>( + py: pyo3::Python<'p>, + builder: &'p pyo3::PyAny, + private_key: &'p pyo3::PyAny, + hash_algorithm: &'p pyo3::PyAny, +) -> pyo3::PyResult<Vec<u8>> { + let ocsp_mod = py.import("cryptography.x509.ocsp")?; + + let py_single_resp = builder.getattr("_response")?; + let py_cert: pyo3::PyRef<'_, x509::Certificate> = py_single_resp.getattr("_cert")?.extract()?; + let py_issuer: pyo3::PyRef<'_, x509::Certificate> = + py_single_resp.getattr("_issuer")?.extract()?; + let py_cert_hash_algorithm = py_single_resp.getattr("_algorithm")?; + let (responder_cert, responder_encoding): (&pyo3::PyCell<x509::Certificate>, &pyo3::PyAny) = + builder.getattr("_responder_id")?.extract()?; + + let py_cert_status = py_single_resp.getattr("_cert_status")?; + let cert_status = if py_cert_status == ocsp_mod.getattr("OCSPCertStatus")?.getattr("GOOD")? { + CertStatus::Good(()) + } else if py_cert_status == ocsp_mod.getattr("OCSPCertStatus")?.getattr("UNKNOWN")? { + CertStatus::Unknown(()) + } else { + let revocation_reason = if !py_single_resp.getattr("_revocation_reason")?.is_none() { + let value = py + .import("cryptography.hazmat.backends.openssl.decode_asn1")? + .getattr("_CRL_ENTRY_REASON_ENUM_TO_CODE")? + .get_item(py_single_resp.getattr("_revocation_reason")?)? + .extract::<u32>()?; + Some(asn1::Enumerated::new(value)) + } else { + None + }; + // REVOKED + let revocation_time = + asn1::GeneralizedTime::new(py_to_chrono(py_single_resp.getattr("_revocation_time")?)?); + CertStatus::Revoked(RevokedInfo { + revocation_time, + revocation_reason, + }) + }; + let next_update = if !py_single_resp.getattr("_next_update")?.is_none() { + let py_next_update = py_single_resp.getattr("_next_update")?; + Some(asn1::GeneralizedTime::new(py_to_chrono(py_next_update)?)) + } else { + None + }; + let this_update = + asn1::GeneralizedTime::new(py_to_chrono(py_single_resp.getattr("_this_update")?)?); + + let responses = vec![SingleResponse { + cert_id: ocsp::CertID::new(py, &py_cert, &py_issuer, py_cert_hash_algorithm)?, + cert_status, + next_update, + this_update, + single_extensions: None, + }]; + + let borrowed_cert = responder_cert.borrow(); + let responder_id = + if responder_encoding == ocsp_mod.getattr("OCSPResponderEncoding")?.getattr("HASH")? { + let sha1 = py + .import("cryptography.hazmat.primitives.hashes")? + .getattr("SHA1")? + .call0()?; + ResponderId::ByKey(ocsp::hash_data( + py, + sha1, + borrowed_cert + .raw + .borrow_value_public() + .tbs_cert + .spki + .subject_public_key + .as_bytes(), + )?) + } else { + ResponderId::ByName( + borrowed_cert + .raw + .borrow_value_public() + .tbs_cert + .subject + .clone(), + ) + }; + + let tbs_response_data = ResponseData { + version: 0, + produced_at: asn1::GeneralizedTime::new(chrono::Utc::now()), + responder_id, + responses: x509::Asn1ReadableOrWritable::new_write(asn1::SequenceOfWriter::new(responses)), + response_extensions: x509::common::encode_extensions( + py, + builder.getattr("_extensions")?, + encode_ocsp_basic_response_extension, + )?, + }; + + let sigalg = x509::sign::compute_signature_algorithm(py, private_key, hash_algorithm)?; + let tbs_bytes = asn1::write_single(&tbs_response_data); + let signature = x509::sign::sign_data(py, private_key, hash_algorithm, &tbs_bytes)?; + + py.import("cryptography.hazmat.backends.openssl.backend")? + .getattr("backend")? + .call_method1( + "_check_keys_correspond", + ( + responder_cert.call_method0("public_key")?, + private_key.call_method0("public_key")?, + ), + )?; + + let py_certs: Option<Vec<pyo3::PyRef<'_, x509::Certificate>>> = + builder.getattr("_certs")?.extract()?; + let certs = py_certs.as_ref().map(|py_certs| { + x509::Asn1ReadableOrWritable::new_write(asn1::SequenceOfWriter::new( + py_certs + .iter() + .map(|c| c.raw.borrow_value_public().clone()) + .collect(), + )) + }); + + let basic_resp = BasicOCSPResponse { + tbs_response_data, + signature: asn1::BitString::new(signature, 0).unwrap(), + signature_algorithm: sigalg, + certs, + }; + Ok(asn1::write_single(&basic_resp)) +} + #[pyo3::prelude::pyfunction] -fn encode_ocsp_basic_response_extension(ext: &pyo3::PyAny) -> pyo3::PyResult<&pyo3::PyAny> { - let oid = asn1::ObjectIdentifier::from_string( - ext.getattr("oid")? - .getattr("dotted_string")? - .extract::<&str>()?, - ) - .unwrap(); - if oid == *ocsp::NONCE_OID { - Ok(ext.getattr("value")?.getattr("nonce")?) +fn create_ocsp_response( + py: pyo3::Python<'_>, + status: &pyo3::PyAny, + builder: &pyo3::PyAny, + private_key: &pyo3::PyAny, + hash_algorithm: &pyo3::PyAny, +) -> PyAsn1Result<OCSPResponse> { + let response_status = status.getattr("value")?.extract::<u32>()?; + let basic_resp_bytes; + let response_bytes = if response_status == SUCCESSFUL_RESPONSE { + basic_resp_bytes = create_ocsp_basic_response(py, builder, private_key, hash_algorithm)?; + Some(ResponseBytes { + response_type: (*BASIC_RESPONSE_OID).clone(), + response: &basic_resp_bytes, + }) + } else { + None + }; + + let resp = RawOCSPResponse { + response_status: asn1::Enumerated::new(response_status), + response_bytes, + }; + let data = asn1::write_single(&resp); + // TODO: extra copy as we round-trip through a slice + load_der_ocsp_response(py, &data) +} + +fn encode_ocsp_basic_response_extension( + oid: &asn1::ObjectIdentifier<'_>, + ext: &pyo3::PyAny, +) -> pyo3::PyResult<Option<Vec<u8>>> { + if oid == &*ocsp::NONCE_OID { + Ok(Some(ext.getattr("nonce")?.extract::<&[u8]>()?.to_vec())) } else { - Err(pyo3::exceptions::PyNotImplementedError::new_err(format!( - "Extension not supported: {}", - oid - ))) + Ok(None) } } pub(crate) fn add_to_module(module: &pyo3::prelude::PyModule) -> pyo3::PyResult<()> { module.add_wrapped(pyo3::wrap_pyfunction!(load_der_ocsp_response))?; - module.add_wrapped(pyo3::wrap_pyfunction!(encode_ocsp_basic_response_extension))?; + module.add_wrapped(pyo3::wrap_pyfunction!(create_ocsp_response))?; Ok(()) } diff --git a/src/rust/src/x509/sign.rs b/src/rust/src/x509/sign.rs new file mode 100644 index 000000000..62230f7fa --- /dev/null +++ b/src/rust/src/x509/sign.rs @@ -0,0 +1,137 @@ +// This file is dual licensed under the terms of the Apache License, Version +// 2.0, and the BSD License. See the LICENSE file in the root of this repository +// for complete details. + +use crate::x509; + +lazy_static::lazy_static! { + static ref ECDSA_WITH_SHA256_OID: asn1::ObjectIdentifier<'static> = asn1::ObjectIdentifier::from_string("1.2.840.10045.4.3.2").unwrap(); + static ref ECDSA_WITH_SHA384_OID: asn1::ObjectIdentifier<'static> = asn1::ObjectIdentifier::from_string("1.2.840.10045.4.3.3").unwrap(); + + static ref ED25519_OID: asn1::ObjectIdentifier<'static> = asn1::ObjectIdentifier::from_string("1.3.101.112").unwrap(); + static ref ED448_OID: asn1::ObjectIdentifier<'static> = asn1::ObjectIdentifier::from_string("1.3.101.113").unwrap(); +} + +enum KeyType { + Ec, + Ed25519, + Ed448, +} + +enum HashType { + None, + Sha256, + Sha384, +} + +fn identify_key_type(py: pyo3::Python<'_>, private_key: &pyo3::PyAny) -> pyo3::PyResult<KeyType> { + let ec_key_type: &pyo3::types::PyType = py + .import("cryptography.hazmat.primitives.asymmetric.ec")? + .getattr("EllipticCurvePrivateKey")? + .extract()?; + let ed25519_key_type: &pyo3::types::PyType = py + .import("cryptography.hazmat.primitives.asymmetric.ed25519")? + .getattr("Ed25519PrivateKey")? + .extract()?; + let ed448_key_type: &pyo3::types::PyType = py + .import("cryptography.hazmat.primitives.asymmetric.ed448")? + .getattr("Ed448PrivateKey")? + .extract()?; + + if ec_key_type.is_instance(private_key)? { + Ok(KeyType::Ec) + } else if ed25519_key_type.is_instance(private_key)? { + Ok(KeyType::Ed25519) + } else if ed448_key_type.is_instance(private_key)? { + Ok(KeyType::Ed448) + } else { + Err(pyo3::exceptions::PyTypeError::new_err( + "Key must be an rsa, dsa, ec, ed25519, or ed448 private key.", + )) + } +} + +fn identify_hash_type( + py: pyo3::Python<'_>, + hash_algorithm: &pyo3::PyAny, +) -> pyo3::PyResult<HashType> { + if hash_algorithm.is_none() { + return Ok(HashType::None); + } + + let hash_algorithm_type: &pyo3::types::PyType = py + .import("cryptography.hazmat.primitives.hashes")? + .getattr("HashAlgorithm")? + .extract()?; + if !hash_algorithm_type.is_instance(hash_algorithm)? { + return Err(pyo3::exceptions::PyTypeError::new_err( + "Algorithm must be a registered hash algorithm.", + )); + } + + match hash_algorithm.getattr("name")?.extract()? { + "sha256" => Ok(HashType::Sha256), + "sha384" => Ok(HashType::Sha384), + name => Err(pyo3::exceptions::PyValueError::new_err(format!( + "Hash algorithm {:?} not supported for signatures", + name + ))), + } +} + +pub(crate) fn compute_signature_algorithm<'p>( + py: pyo3::Python<'p>, + private_key: &'p pyo3::PyAny, + hash_algorithm: &'p pyo3::PyAny, +) -> pyo3::PyResult<x509::AlgorithmIdentifier<'static>> { + let key_type = identify_key_type(py, private_key)?; + let hash_type = identify_hash_type(py, hash_algorithm)?; + + match (key_type, hash_type) { + (KeyType::Ed25519, HashType::None) => Ok(x509::AlgorithmIdentifier { + oid: (*ED25519_OID).clone(), + params: None, + }), + (KeyType::Ed448, HashType::None) => Ok(x509::AlgorithmIdentifier { + oid: (*ED448_OID).clone(), + params: None, + }), + (KeyType::Ed25519, _) | (KeyType::Ed448, _) => { + Err(pyo3::exceptions::PyValueError::new_err( + "Algorithm must be None when signing via ed25519 or ed448", + )) + } + + (KeyType::Ec, HashType::Sha256) => Ok(x509::AlgorithmIdentifier { + oid: (*ECDSA_WITH_SHA256_OID).clone(), + params: None, + }), + (KeyType::Ec, HashType::Sha384) => Ok(x509::AlgorithmIdentifier { + oid: (*ECDSA_WITH_SHA384_OID).clone(), + params: None, + }), + + (_, HashType::None) => Err(pyo3::exceptions::PyTypeError::new_err( + "Algorithm must be a registered hash algorithm, not None.", + )), + } +} + +pub(crate) fn sign_data<'p>( + py: pyo3::Python<'p>, + private_key: &'p pyo3::PyAny, + hash_algorithm: &'p pyo3::PyAny, + data: &[u8], +) -> pyo3::PyResult<&'p [u8]> { + let key_type = identify_key_type(py, private_key)?; + + let signature = match key_type { + KeyType::Ed25519 | KeyType::Ed448 => private_key.call_method1("sign", (data,))?, + KeyType::Ec => { + let ec_mod = py.import("cryptography.hazmat.primitives.asymmetric.ec")?; + let ecdsa = ec_mod.getattr("ECDSA")?.call1((hash_algorithm,))?; + private_key.call_method1("sign", (data, ecdsa))? + } + }; + signature.extract() +} |
