2480 lines
78 KiB
Python
2480 lines
78 KiB
Python
from __future__ import annotations
|
|
|
|
import calendar
|
|
import datetime
|
|
import functools
|
|
import typing
|
|
import warnings
|
|
from base64 import b16encode
|
|
from collections.abc import Iterable, Sequence
|
|
from functools import partial
|
|
from os import PathLike
|
|
from typing import (
|
|
Any,
|
|
Callable,
|
|
Union,
|
|
)
|
|
|
|
from cryptography import utils, x509
|
|
from cryptography.hazmat.primitives.asymmetric import (
|
|
dsa,
|
|
ec,
|
|
ed448,
|
|
ed25519,
|
|
rsa,
|
|
)
|
|
|
|
from OpenSSL._util import (
|
|
byte_string as _byte_string,
|
|
)
|
|
from OpenSSL._util import (
|
|
exception_from_error_queue as _exception_from_error_queue,
|
|
)
|
|
from OpenSSL._util import (
|
|
ffi as _ffi,
|
|
)
|
|
from OpenSSL._util import (
|
|
lib as _lib,
|
|
)
|
|
from OpenSSL._util import (
|
|
make_assert as _make_assert,
|
|
)
|
|
from OpenSSL._util import (
|
|
path_bytes as _path_bytes,
|
|
)
|
|
|
|
__all__ = [
|
|
"FILETYPE_ASN1",
|
|
"FILETYPE_PEM",
|
|
"FILETYPE_TEXT",
|
|
"TYPE_DSA",
|
|
"TYPE_RSA",
|
|
"X509",
|
|
"Error",
|
|
"PKey",
|
|
"X509Extension",
|
|
"X509Name",
|
|
"X509Req",
|
|
"X509Store",
|
|
"X509StoreContext",
|
|
"X509StoreContextError",
|
|
"X509StoreFlags",
|
|
"dump_certificate",
|
|
"dump_certificate_request",
|
|
"dump_privatekey",
|
|
"dump_publickey",
|
|
"get_elliptic_curve",
|
|
"get_elliptic_curves",
|
|
"load_certificate",
|
|
"load_certificate_request",
|
|
"load_privatekey",
|
|
"load_publickey",
|
|
]
|
|
|
|
|
|
_PrivateKey = Union[
|
|
dsa.DSAPrivateKey,
|
|
ec.EllipticCurvePrivateKey,
|
|
ed25519.Ed25519PrivateKey,
|
|
ed448.Ed448PrivateKey,
|
|
rsa.RSAPrivateKey,
|
|
]
|
|
_PublicKey = Union[
|
|
dsa.DSAPublicKey,
|
|
ec.EllipticCurvePublicKey,
|
|
ed25519.Ed25519PublicKey,
|
|
ed448.Ed448PublicKey,
|
|
rsa.RSAPublicKey,
|
|
]
|
|
_Key = Union[_PrivateKey, _PublicKey]
|
|
StrOrBytesPath = Union[str, bytes, PathLike]
|
|
PassphraseCallableT = Union[bytes, Callable[..., bytes]]
|
|
|
|
|
|
FILETYPE_PEM: int = _lib.SSL_FILETYPE_PEM
|
|
FILETYPE_ASN1: int = _lib.SSL_FILETYPE_ASN1
|
|
|
|
# TODO This was an API mistake. OpenSSL has no such constant.
|
|
FILETYPE_TEXT = 2**16 - 1
|
|
|
|
TYPE_RSA: int = _lib.EVP_PKEY_RSA
|
|
TYPE_DSA: int = _lib.EVP_PKEY_DSA
|
|
TYPE_DH: int = _lib.EVP_PKEY_DH
|
|
TYPE_EC: int = _lib.EVP_PKEY_EC
|
|
|
|
|
|
class Error(Exception):
|
|
"""
|
|
An error occurred in an `OpenSSL.crypto` API.
|
|
"""
|
|
|
|
|
|
_raise_current_error = partial(_exception_from_error_queue, Error)
|
|
_openssl_assert = _make_assert(Error)
|
|
|
|
|
|
def _new_mem_buf(buffer: bytes | None = None) -> Any:
|
|
"""
|
|
Allocate a new OpenSSL memory BIO.
|
|
|
|
Arrange for the garbage collector to clean it up automatically.
|
|
|
|
:param buffer: None or some bytes to use to put into the BIO so that they
|
|
can be read out.
|
|
"""
|
|
if buffer is None:
|
|
bio = _lib.BIO_new(_lib.BIO_s_mem())
|
|
free = _lib.BIO_free
|
|
else:
|
|
data = _ffi.new("char[]", buffer)
|
|
bio = _lib.BIO_new_mem_buf(data, len(buffer))
|
|
|
|
# Keep the memory alive as long as the bio is alive!
|
|
def free(bio: Any, ref: Any = data) -> Any:
|
|
return _lib.BIO_free(bio)
|
|
|
|
_openssl_assert(bio != _ffi.NULL)
|
|
|
|
bio = _ffi.gc(bio, free)
|
|
return bio
|
|
|
|
|
|
def _bio_to_string(bio: Any) -> bytes:
|
|
"""
|
|
Copy the contents of an OpenSSL BIO object into a Python byte string.
|
|
"""
|
|
result_buffer = _ffi.new("char**")
|
|
buffer_length = _lib.BIO_get_mem_data(bio, result_buffer)
|
|
return _ffi.buffer(result_buffer[0], buffer_length)[:]
|
|
|
|
|
|
def _set_asn1_time(boundary: Any, when: bytes) -> None:
|
|
"""
|
|
The the time value of an ASN1 time object.
|
|
|
|
@param boundary: An ASN1_TIME pointer (or an object safely
|
|
castable to that type) which will have its value set.
|
|
@param when: A string representation of the desired time value.
|
|
|
|
@raise TypeError: If C{when} is not a L{bytes} string.
|
|
@raise ValueError: If C{when} does not represent a time in the required
|
|
format.
|
|
@raise RuntimeError: If the time value cannot be set for some other
|
|
(unspecified) reason.
|
|
"""
|
|
if not isinstance(when, bytes):
|
|
raise TypeError("when must be a byte string")
|
|
# ASN1_TIME_set_string validates the string without writing anything
|
|
# when the destination is NULL.
|
|
_openssl_assert(boundary != _ffi.NULL)
|
|
|
|
set_result = _lib.ASN1_TIME_set_string(boundary, when)
|
|
if set_result == 0:
|
|
raise ValueError("Invalid string")
|
|
|
|
|
|
def _new_asn1_time(when: bytes) -> Any:
|
|
"""
|
|
Behaves like _set_asn1_time but returns a new ASN1_TIME object.
|
|
|
|
@param when: A string representation of the desired time value.
|
|
|
|
@raise TypeError: If C{when} is not a L{bytes} string.
|
|
@raise ValueError: If C{when} does not represent a time in the required
|
|
format.
|
|
@raise RuntimeError: If the time value cannot be set for some other
|
|
(unspecified) reason.
|
|
"""
|
|
ret = _lib.ASN1_TIME_new()
|
|
_openssl_assert(ret != _ffi.NULL)
|
|
ret = _ffi.gc(ret, _lib.ASN1_TIME_free)
|
|
_set_asn1_time(ret, when)
|
|
return ret
|
|
|
|
|
|
def _get_asn1_time(timestamp: Any) -> bytes | None:
|
|
"""
|
|
Retrieve the time value of an ASN1 time object.
|
|
|
|
@param timestamp: An ASN1_GENERALIZEDTIME* (or an object safely castable to
|
|
that type) from which the time value will be retrieved.
|
|
|
|
@return: The time value from C{timestamp} as a L{bytes} string in a certain
|
|
format. Or C{None} if the object contains no time value.
|
|
"""
|
|
string_timestamp = _ffi.cast("ASN1_STRING*", timestamp)
|
|
if _lib.ASN1_STRING_length(string_timestamp) == 0:
|
|
return None
|
|
elif (
|
|
_lib.ASN1_STRING_type(string_timestamp) == _lib.V_ASN1_GENERALIZEDTIME
|
|
):
|
|
return _ffi.string(_lib.ASN1_STRING_get0_data(string_timestamp))
|
|
else:
|
|
generalized_timestamp = _ffi.new("ASN1_GENERALIZEDTIME**")
|
|
_lib.ASN1_TIME_to_generalizedtime(timestamp, generalized_timestamp)
|
|
_openssl_assert(generalized_timestamp[0] != _ffi.NULL)
|
|
|
|
string_timestamp = _ffi.cast("ASN1_STRING*", generalized_timestamp[0])
|
|
string_data = _lib.ASN1_STRING_get0_data(string_timestamp)
|
|
string_result = _ffi.string(string_data)
|
|
_lib.ASN1_GENERALIZEDTIME_free(generalized_timestamp[0])
|
|
return string_result
|
|
|
|
|
|
class _X509NameInvalidator:
|
|
def __init__(self) -> None:
|
|
self._names: list[X509Name] = []
|
|
|
|
def add(self, name: X509Name) -> None:
|
|
self._names.append(name)
|
|
|
|
def clear(self) -> None:
|
|
for name in self._names:
|
|
# Breaks the object, but also prevents UAF!
|
|
del name._name
|
|
|
|
|
|
class PKey:
|
|
"""
|
|
A class representing an DSA or RSA public key or key pair.
|
|
"""
|
|
|
|
_only_public = False
|
|
_initialized = True
|
|
|
|
def __init__(self) -> None:
|
|
pkey = _lib.EVP_PKEY_new()
|
|
self._pkey = _ffi.gc(pkey, _lib.EVP_PKEY_free)
|
|
self._initialized = False
|
|
|
|
def to_cryptography_key(self) -> _Key:
|
|
"""
|
|
Export as a ``cryptography`` key.
|
|
|
|
:rtype: One of ``cryptography``'s `key interfaces`_.
|
|
|
|
.. _key interfaces: https://cryptography.io/en/latest/hazmat/\
|
|
primitives/asymmetric/rsa/#key-interfaces
|
|
|
|
.. versionadded:: 16.1.0
|
|
"""
|
|
from cryptography.hazmat.primitives.serialization import (
|
|
load_der_private_key,
|
|
load_der_public_key,
|
|
)
|
|
|
|
if self._only_public:
|
|
der = dump_publickey(FILETYPE_ASN1, self)
|
|
return load_der_public_key(der)
|
|
else:
|
|
der = dump_privatekey(FILETYPE_ASN1, self)
|
|
return load_der_private_key(der, None)
|
|
|
|
@classmethod
|
|
def from_cryptography_key(cls, crypto_key: _Key) -> PKey:
|
|
"""
|
|
Construct based on a ``cryptography`` *crypto_key*.
|
|
|
|
:param crypto_key: A ``cryptography`` key.
|
|
:type crypto_key: One of ``cryptography``'s `key interfaces`_.
|
|
|
|
:rtype: PKey
|
|
|
|
.. versionadded:: 16.1.0
|
|
"""
|
|
if not isinstance(
|
|
crypto_key,
|
|
(
|
|
dsa.DSAPrivateKey,
|
|
dsa.DSAPublicKey,
|
|
ec.EllipticCurvePrivateKey,
|
|
ec.EllipticCurvePublicKey,
|
|
ed25519.Ed25519PrivateKey,
|
|
ed25519.Ed25519PublicKey,
|
|
ed448.Ed448PrivateKey,
|
|
ed448.Ed448PublicKey,
|
|
rsa.RSAPrivateKey,
|
|
rsa.RSAPublicKey,
|
|
),
|
|
):
|
|
raise TypeError("Unsupported key type")
|
|
|
|
from cryptography.hazmat.primitives.serialization import (
|
|
Encoding,
|
|
NoEncryption,
|
|
PrivateFormat,
|
|
PublicFormat,
|
|
)
|
|
|
|
if isinstance(
|
|
crypto_key,
|
|
(
|
|
dsa.DSAPublicKey,
|
|
ec.EllipticCurvePublicKey,
|
|
ed25519.Ed25519PublicKey,
|
|
ed448.Ed448PublicKey,
|
|
rsa.RSAPublicKey,
|
|
),
|
|
):
|
|
return load_publickey(
|
|
FILETYPE_ASN1,
|
|
crypto_key.public_bytes(
|
|
Encoding.DER, PublicFormat.SubjectPublicKeyInfo
|
|
),
|
|
)
|
|
else:
|
|
der = crypto_key.private_bytes(
|
|
Encoding.DER, PrivateFormat.PKCS8, NoEncryption()
|
|
)
|
|
return load_privatekey(FILETYPE_ASN1, der)
|
|
|
|
def generate_key(self, type: int, bits: int) -> None:
|
|
"""
|
|
Generate a key pair of the given type, with the given number of bits.
|
|
|
|
This generates a key "into" the this object.
|
|
|
|
:param type: The key type.
|
|
:type type: :py:data:`TYPE_RSA` or :py:data:`TYPE_DSA`
|
|
:param bits: The number of bits.
|
|
:type bits: :py:data:`int` ``>= 0``
|
|
:raises TypeError: If :py:data:`type` or :py:data:`bits` isn't
|
|
of the appropriate type.
|
|
:raises ValueError: If the number of bits isn't an integer of
|
|
the appropriate size.
|
|
:return: ``None``
|
|
"""
|
|
if not isinstance(type, int):
|
|
raise TypeError("type must be an integer")
|
|
|
|
if not isinstance(bits, int):
|
|
raise TypeError("bits must be an integer")
|
|
|
|
if type == TYPE_RSA:
|
|
if bits <= 0:
|
|
raise ValueError("Invalid number of bits")
|
|
|
|
# TODO Check error return
|
|
exponent = _lib.BN_new()
|
|
exponent = _ffi.gc(exponent, _lib.BN_free)
|
|
_lib.BN_set_word(exponent, _lib.RSA_F4)
|
|
|
|
rsa = _lib.RSA_new()
|
|
|
|
result = _lib.RSA_generate_key_ex(rsa, bits, exponent, _ffi.NULL)
|
|
_openssl_assert(result == 1)
|
|
|
|
result = _lib.EVP_PKEY_assign_RSA(self._pkey, rsa)
|
|
_openssl_assert(result == 1)
|
|
|
|
elif type == TYPE_DSA:
|
|
dsa = _lib.DSA_new()
|
|
_openssl_assert(dsa != _ffi.NULL)
|
|
|
|
dsa = _ffi.gc(dsa, _lib.DSA_free)
|
|
res = _lib.DSA_generate_parameters_ex(
|
|
dsa, bits, _ffi.NULL, 0, _ffi.NULL, _ffi.NULL, _ffi.NULL
|
|
)
|
|
_openssl_assert(res == 1)
|
|
|
|
_openssl_assert(_lib.DSA_generate_key(dsa) == 1)
|
|
_openssl_assert(_lib.EVP_PKEY_set1_DSA(self._pkey, dsa) == 1)
|
|
else:
|
|
raise Error("No such key type")
|
|
|
|
self._initialized = True
|
|
|
|
def check(self) -> bool:
|
|
"""
|
|
Check the consistency of an RSA private key.
|
|
|
|
This is the Python equivalent of OpenSSL's ``RSA_check_key``.
|
|
|
|
:return: ``True`` if key is consistent.
|
|
|
|
:raise OpenSSL.crypto.Error: if the key is inconsistent.
|
|
|
|
:raise TypeError: if the key is of a type which cannot be checked.
|
|
Only RSA keys can currently be checked.
|
|
"""
|
|
if self._only_public:
|
|
raise TypeError("public key only")
|
|
|
|
if _lib.EVP_PKEY_type(self.type()) != _lib.EVP_PKEY_RSA:
|
|
raise TypeError("Only RSA keys can currently be checked.")
|
|
|
|
rsa = _lib.EVP_PKEY_get1_RSA(self._pkey)
|
|
rsa = _ffi.gc(rsa, _lib.RSA_free)
|
|
result = _lib.RSA_check_key(rsa)
|
|
if result == 1:
|
|
return True
|
|
_raise_current_error()
|
|
|
|
def type(self) -> int:
|
|
"""
|
|
Returns the type of the key
|
|
|
|
:return: The type of the key.
|
|
"""
|
|
return _lib.EVP_PKEY_id(self._pkey)
|
|
|
|
def bits(self) -> int:
|
|
"""
|
|
Returns the number of bits of the key
|
|
|
|
:return: The number of bits of the key.
|
|
"""
|
|
return _lib.EVP_PKEY_bits(self._pkey)
|
|
|
|
|
|
class _EllipticCurve:
|
|
"""
|
|
A representation of a supported elliptic curve.
|
|
|
|
@cvar _curves: :py:obj:`None` until an attempt is made to load the curves.
|
|
Thereafter, a :py:type:`set` containing :py:type:`_EllipticCurve`
|
|
instances each of which represents one curve supported by the system.
|
|
@type _curves: :py:type:`NoneType` or :py:type:`set`
|
|
"""
|
|
|
|
_curves = None
|
|
|
|
def __ne__(self, other: Any) -> bool:
|
|
"""
|
|
Implement cooperation with the right-hand side argument of ``!=``.
|
|
|
|
Python 3 seems to have dropped this cooperation in this very narrow
|
|
circumstance.
|
|
"""
|
|
if isinstance(other, _EllipticCurve):
|
|
return super().__ne__(other)
|
|
return NotImplemented
|
|
|
|
@classmethod
|
|
def _load_elliptic_curves(cls, lib: Any) -> set[_EllipticCurve]:
|
|
"""
|
|
Get the curves supported by OpenSSL.
|
|
|
|
:param lib: The OpenSSL library binding object.
|
|
|
|
:return: A :py:type:`set` of ``cls`` instances giving the names of the
|
|
elliptic curves the underlying library supports.
|
|
"""
|
|
num_curves = lib.EC_get_builtin_curves(_ffi.NULL, 0)
|
|
builtin_curves = _ffi.new("EC_builtin_curve[]", num_curves)
|
|
# The return value on this call should be num_curves again. We
|
|
# could check it to make sure but if it *isn't* then.. what could
|
|
# we do? Abort the whole process, I suppose...? -exarkun
|
|
lib.EC_get_builtin_curves(builtin_curves, num_curves)
|
|
return set(cls.from_nid(lib, c.nid) for c in builtin_curves)
|
|
|
|
@classmethod
|
|
def _get_elliptic_curves(cls, lib: Any) -> set[_EllipticCurve]:
|
|
"""
|
|
Get, cache, and return the curves supported by OpenSSL.
|
|
|
|
:param lib: The OpenSSL library binding object.
|
|
|
|
:return: A :py:type:`set` of ``cls`` instances giving the names of the
|
|
elliptic curves the underlying library supports.
|
|
"""
|
|
if cls._curves is None:
|
|
cls._curves = cls._load_elliptic_curves(lib)
|
|
return cls._curves
|
|
|
|
@classmethod
|
|
def from_nid(cls, lib: Any, nid: int) -> _EllipticCurve:
|
|
"""
|
|
Instantiate a new :py:class:`_EllipticCurve` associated with the given
|
|
OpenSSL NID.
|
|
|
|
:param lib: The OpenSSL library binding object.
|
|
|
|
:param nid: The OpenSSL NID the resulting curve object will represent.
|
|
This must be a curve NID (and not, for example, a hash NID) or
|
|
subsequent operations will fail in unpredictable ways.
|
|
:type nid: :py:class:`int`
|
|
|
|
:return: The curve object.
|
|
"""
|
|
return cls(lib, nid, _ffi.string(lib.OBJ_nid2sn(nid)).decode("ascii"))
|
|
|
|
def __init__(self, lib: Any, nid: int, name: str) -> None:
|
|
"""
|
|
:param _lib: The :py:mod:`cryptography` binding instance used to
|
|
interface with OpenSSL.
|
|
|
|
:param _nid: The OpenSSL NID identifying the curve this object
|
|
represents.
|
|
:type _nid: :py:class:`int`
|
|
|
|
:param name: The OpenSSL short name identifying the curve this object
|
|
represents.
|
|
:type name: :py:class:`unicode`
|
|
"""
|
|
self._lib = lib
|
|
self._nid = nid
|
|
self.name = name
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Curve {self.name!r}>"
|
|
|
|
def _to_EC_KEY(self) -> Any:
|
|
"""
|
|
Create a new OpenSSL EC_KEY structure initialized to use this curve.
|
|
|
|
The structure is automatically garbage collected when the Python object
|
|
is garbage collected.
|
|
"""
|
|
key = self._lib.EC_KEY_new_by_curve_name(self._nid)
|
|
return _ffi.gc(key, _lib.EC_KEY_free)
|
|
|
|
|
|
def get_elliptic_curves() -> set[_EllipticCurve]:
|
|
"""
|
|
Return a set of objects representing the elliptic curves supported in the
|
|
OpenSSL build in use.
|
|
|
|
The curve objects have a :py:class:`unicode` ``name`` attribute by which
|
|
they identify themselves.
|
|
|
|
The curve objects are useful as values for the argument accepted by
|
|
:py:meth:`Context.set_tmp_ecdh` to specify which elliptical curve should be
|
|
used for ECDHE key exchange.
|
|
"""
|
|
return _EllipticCurve._get_elliptic_curves(_lib)
|
|
|
|
|
|
_get_elliptic_curves_internal = get_elliptic_curves
|
|
|
|
utils.deprecated(
|
|
get_elliptic_curves,
|
|
__name__,
|
|
(
|
|
"get_elliptic_curves is deprecated. You should use the APIs in "
|
|
"cryptography instead."
|
|
),
|
|
DeprecationWarning,
|
|
name="get_elliptic_curves",
|
|
)
|
|
|
|
|
|
def get_elliptic_curve(name: str) -> _EllipticCurve:
|
|
"""
|
|
Return a single curve object selected by name.
|
|
|
|
See :py:func:`get_elliptic_curves` for information about curve objects.
|
|
|
|
:param name: The OpenSSL short name identifying the curve object to
|
|
retrieve.
|
|
:type name: :py:class:`unicode`
|
|
|
|
If the named curve is not supported then :py:class:`ValueError` is raised.
|
|
"""
|
|
for curve in _get_elliptic_curves_internal():
|
|
if curve.name == name:
|
|
return curve
|
|
raise ValueError("unknown curve name", name)
|
|
|
|
|
|
utils.deprecated(
|
|
get_elliptic_curve,
|
|
__name__,
|
|
(
|
|
"get_elliptic_curve is deprecated. You should use the APIs in "
|
|
"cryptography instead."
|
|
),
|
|
DeprecationWarning,
|
|
name="get_elliptic_curve",
|
|
)
|
|
|
|
|
|
@functools.total_ordering
|
|
class X509Name:
|
|
"""
|
|
An X.509 Distinguished Name.
|
|
|
|
:ivar countryName: The country of the entity.
|
|
:ivar C: Alias for :py:attr:`countryName`.
|
|
|
|
:ivar stateOrProvinceName: The state or province of the entity.
|
|
:ivar ST: Alias for :py:attr:`stateOrProvinceName`.
|
|
|
|
:ivar localityName: The locality of the entity.
|
|
:ivar L: Alias for :py:attr:`localityName`.
|
|
|
|
:ivar organizationName: The organization name of the entity.
|
|
:ivar O: Alias for :py:attr:`organizationName`.
|
|
|
|
:ivar organizationalUnitName: The organizational unit of the entity.
|
|
:ivar OU: Alias for :py:attr:`organizationalUnitName`
|
|
|
|
:ivar commonName: The common name of the entity.
|
|
:ivar CN: Alias for :py:attr:`commonName`.
|
|
|
|
:ivar emailAddress: The e-mail address of the entity.
|
|
"""
|
|
|
|
def __init__(self, name: X509Name) -> None:
|
|
"""
|
|
Create a new X509Name, copying the given X509Name instance.
|
|
|
|
:param name: The name to copy.
|
|
:type name: :py:class:`X509Name`
|
|
"""
|
|
name = _lib.X509_NAME_dup(name._name)
|
|
self._name: Any = _ffi.gc(name, _lib.X509_NAME_free)
|
|
|
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
if name.startswith("_"):
|
|
return super().__setattr__(name, value)
|
|
|
|
# Note: we really do not want str subclasses here, so we do not use
|
|
# isinstance.
|
|
if type(name) is not str:
|
|
raise TypeError(
|
|
f"attribute name must be string, not "
|
|
f"'{type(value).__name__:.200}'"
|
|
)
|
|
|
|
nid = _lib.OBJ_txt2nid(_byte_string(name))
|
|
if nid == _lib.NID_undef:
|
|
try:
|
|
_raise_current_error()
|
|
except Error:
|
|
pass
|
|
raise AttributeError("No such attribute")
|
|
|
|
# If there's an old entry for this NID, remove it
|
|
for i in range(_lib.X509_NAME_entry_count(self._name)):
|
|
ent = _lib.X509_NAME_get_entry(self._name, i)
|
|
ent_obj = _lib.X509_NAME_ENTRY_get_object(ent)
|
|
ent_nid = _lib.OBJ_obj2nid(ent_obj)
|
|
if nid == ent_nid:
|
|
ent = _lib.X509_NAME_delete_entry(self._name, i)
|
|
_lib.X509_NAME_ENTRY_free(ent)
|
|
break
|
|
|
|
if isinstance(value, str):
|
|
value = value.encode("utf-8")
|
|
|
|
add_result = _lib.X509_NAME_add_entry_by_NID(
|
|
self._name, nid, _lib.MBSTRING_UTF8, value, -1, -1, 0
|
|
)
|
|
if not add_result:
|
|
_raise_current_error()
|
|
|
|
def __getattr__(self, name: str) -> str | None:
|
|
"""
|
|
Find attribute. An X509Name object has the following attributes:
|
|
countryName (alias C), stateOrProvince (alias ST), locality (alias L),
|
|
organization (alias O), organizationalUnit (alias OU), commonName
|
|
(alias CN) and more...
|
|
"""
|
|
nid = _lib.OBJ_txt2nid(_byte_string(name))
|
|
if nid == _lib.NID_undef:
|
|
# This is a bit weird. OBJ_txt2nid indicated failure, but it seems
|
|
# a lower level function, a2d_ASN1_OBJECT, also feels the need to
|
|
# push something onto the error queue. If we don't clean that up
|
|
# now, someone else will bump into it later and be quite confused.
|
|
# See lp#314814.
|
|
try:
|
|
_raise_current_error()
|
|
except Error:
|
|
pass
|
|
raise AttributeError("No such attribute")
|
|
|
|
entry_index = _lib.X509_NAME_get_index_by_NID(self._name, nid, -1)
|
|
if entry_index == -1:
|
|
return None
|
|
|
|
entry = _lib.X509_NAME_get_entry(self._name, entry_index)
|
|
data = _lib.X509_NAME_ENTRY_get_data(entry)
|
|
|
|
result_buffer = _ffi.new("unsigned char**")
|
|
data_length = _lib.ASN1_STRING_to_UTF8(result_buffer, data)
|
|
_openssl_assert(data_length >= 0)
|
|
|
|
try:
|
|
result = _ffi.buffer(result_buffer[0], data_length)[:].decode(
|
|
"utf-8"
|
|
)
|
|
finally:
|
|
# XXX untested
|
|
_lib.OPENSSL_free(result_buffer[0])
|
|
return result
|
|
|
|
def __eq__(self, other: Any) -> bool:
|
|
if not isinstance(other, X509Name):
|
|
return NotImplemented
|
|
|
|
return _lib.X509_NAME_cmp(self._name, other._name) == 0
|
|
|
|
def __lt__(self, other: Any) -> bool:
|
|
if not isinstance(other, X509Name):
|
|
return NotImplemented
|
|
|
|
return _lib.X509_NAME_cmp(self._name, other._name) < 0
|
|
|
|
def __repr__(self) -> str:
|
|
"""
|
|
String representation of an X509Name
|
|
"""
|
|
result_buffer = _ffi.new("char[]", 512)
|
|
format_result = _lib.X509_NAME_oneline(
|
|
self._name, result_buffer, len(result_buffer)
|
|
)
|
|
_openssl_assert(format_result != _ffi.NULL)
|
|
|
|
return "<X509Name object '{}'>".format(
|
|
_ffi.string(result_buffer).decode("utf-8"),
|
|
)
|
|
|
|
def hash(self) -> int:
|
|
"""
|
|
Return an integer representation of the first four bytes of the
|
|
MD5 digest of the DER representation of the name.
|
|
|
|
This is the Python equivalent of OpenSSL's ``X509_NAME_hash``.
|
|
|
|
:return: The (integer) hash of this name.
|
|
:rtype: :py:class:`int`
|
|
"""
|
|
return _lib.X509_NAME_hash(self._name)
|
|
|
|
def der(self) -> bytes:
|
|
"""
|
|
Return the DER encoding of this name.
|
|
|
|
:return: The DER encoded form of this name.
|
|
:rtype: :py:class:`bytes`
|
|
"""
|
|
result_buffer = _ffi.new("unsigned char**")
|
|
encode_result = _lib.i2d_X509_NAME(self._name, result_buffer)
|
|
_openssl_assert(encode_result >= 0)
|
|
|
|
string_result = _ffi.buffer(result_buffer[0], encode_result)[:]
|
|
_lib.OPENSSL_free(result_buffer[0])
|
|
return string_result
|
|
|
|
def get_components(self) -> list[tuple[bytes, bytes]]:
|
|
"""
|
|
Returns the components of this name, as a sequence of 2-tuples.
|
|
|
|
:return: The components of this name.
|
|
:rtype: :py:class:`list` of ``name, value`` tuples.
|
|
"""
|
|
result = []
|
|
for i in range(_lib.X509_NAME_entry_count(self._name)):
|
|
ent = _lib.X509_NAME_get_entry(self._name, i)
|
|
|
|
fname = _lib.X509_NAME_ENTRY_get_object(ent)
|
|
fval = _lib.X509_NAME_ENTRY_get_data(ent)
|
|
|
|
nid = _lib.OBJ_obj2nid(fname)
|
|
name = _lib.OBJ_nid2sn(nid)
|
|
|
|
# ffi.string does not handle strings containing NULL bytes
|
|
# (which may have been generated by old, broken software)
|
|
value = _ffi.buffer(
|
|
_lib.ASN1_STRING_get0_data(fval), _lib.ASN1_STRING_length(fval)
|
|
)[:]
|
|
result.append((_ffi.string(name), value))
|
|
|
|
return result
|
|
|
|
|
|
class X509Extension:
|
|
"""
|
|
An X.509 v3 certificate extension.
|
|
|
|
.. deprecated:: 23.3.0
|
|
Use cryptography's X509 APIs instead.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
type_name: bytes,
|
|
critical: bool,
|
|
value: bytes,
|
|
subject: X509 | None = None,
|
|
issuer: X509 | None = None,
|
|
) -> None:
|
|
"""
|
|
Initializes an X509 extension.
|
|
|
|
:param type_name: The name of the type of extension_ to create.
|
|
:type type_name: :py:data:`bytes`
|
|
|
|
:param bool critical: A flag indicating whether this is a critical
|
|
extension.
|
|
|
|
:param value: The OpenSSL textual representation of the extension's
|
|
value.
|
|
:type value: :py:data:`bytes`
|
|
|
|
:param subject: Optional X509 certificate to use as subject.
|
|
:type subject: :py:class:`X509`
|
|
|
|
:param issuer: Optional X509 certificate to use as issuer.
|
|
:type issuer: :py:class:`X509`
|
|
|
|
.. _extension: https://www.openssl.org/docs/manmaster/man5/
|
|
x509v3_config.html#STANDARD-EXTENSIONS
|
|
"""
|
|
ctx = _ffi.new("X509V3_CTX*")
|
|
|
|
# A context is necessary for any extension which uses the r2i
|
|
# conversion method. That is, X509V3_EXT_nconf may segfault if passed
|
|
# a NULL ctx. Start off by initializing most of the fields to NULL.
|
|
_lib.X509V3_set_ctx(ctx, _ffi.NULL, _ffi.NULL, _ffi.NULL, _ffi.NULL, 0)
|
|
|
|
# We have no configuration database - but perhaps we should (some
|
|
# extensions may require it).
|
|
_lib.X509V3_set_ctx_nodb(ctx)
|
|
|
|
# Initialize the subject and issuer, if appropriate. ctx is a local,
|
|
# and as far as I can tell none of the X509V3_* APIs invoked here steal
|
|
# any references, so no need to mess with reference counts or
|
|
# duplicates.
|
|
if issuer is not None:
|
|
if not isinstance(issuer, X509):
|
|
raise TypeError("issuer must be an X509 instance")
|
|
ctx.issuer_cert = issuer._x509
|
|
if subject is not None:
|
|
if not isinstance(subject, X509):
|
|
raise TypeError("subject must be an X509 instance")
|
|
ctx.subject_cert = subject._x509
|
|
|
|
if critical:
|
|
# There are other OpenSSL APIs which would let us pass in critical
|
|
# separately, but they're harder to use, and since value is already
|
|
# a pile of crappy junk smuggling a ton of utterly important
|
|
# structured data, what's the point of trying to avoid nasty stuff
|
|
# with strings? (However, X509V3_EXT_i2d in particular seems like
|
|
# it would be a better API to invoke. I do not know where to get
|
|
# the ext_struc it desires for its last parameter, though.)
|
|
value = b"critical," + value
|
|
|
|
extension = _lib.X509V3_EXT_nconf(_ffi.NULL, ctx, type_name, value)
|
|
if extension == _ffi.NULL:
|
|
_raise_current_error()
|
|
self._extension = _ffi.gc(extension, _lib.X509_EXTENSION_free)
|
|
|
|
@property
|
|
def _nid(self) -> Any:
|
|
return _lib.OBJ_obj2nid(
|
|
_lib.X509_EXTENSION_get_object(self._extension)
|
|
)
|
|
|
|
_prefixes: typing.ClassVar[dict[int, str]] = {
|
|
_lib.GEN_EMAIL: "email",
|
|
_lib.GEN_DNS: "DNS",
|
|
_lib.GEN_URI: "URI",
|
|
}
|
|
|
|
def _subjectAltNameString(self) -> str:
|
|
names = _ffi.cast(
|
|
"GENERAL_NAMES*", _lib.X509V3_EXT_d2i(self._extension)
|
|
)
|
|
|
|
names = _ffi.gc(names, _lib.GENERAL_NAMES_free)
|
|
parts = []
|
|
for i in range(_lib.sk_GENERAL_NAME_num(names)):
|
|
name = _lib.sk_GENERAL_NAME_value(names, i)
|
|
try:
|
|
label = self._prefixes[name.type]
|
|
except KeyError:
|
|
bio = _new_mem_buf()
|
|
_lib.GENERAL_NAME_print(bio, name)
|
|
parts.append(_bio_to_string(bio).decode("utf-8"))
|
|
else:
|
|
value = _ffi.buffer(name.d.ia5.data, name.d.ia5.length)[
|
|
:
|
|
].decode("utf-8")
|
|
parts.append(label + ":" + value)
|
|
return ", ".join(parts)
|
|
|
|
def __str__(self) -> str:
|
|
"""
|
|
:return: a nice text representation of the extension
|
|
"""
|
|
if _lib.NID_subject_alt_name == self._nid:
|
|
return self._subjectAltNameString()
|
|
|
|
bio = _new_mem_buf()
|
|
print_result = _lib.X509V3_EXT_print(bio, self._extension, 0, 0)
|
|
_openssl_assert(print_result != 0)
|
|
|
|
return _bio_to_string(bio).decode("utf-8")
|
|
|
|
def get_critical(self) -> bool:
|
|
"""
|
|
Returns the critical field of this X.509 extension.
|
|
|
|
:return: The critical field.
|
|
"""
|
|
return _lib.X509_EXTENSION_get_critical(self._extension)
|
|
|
|
def get_short_name(self) -> bytes:
|
|
"""
|
|
Returns the short type name of this X.509 extension.
|
|
|
|
The result is a byte string such as :py:const:`b"basicConstraints"`.
|
|
|
|
:return: The short type name.
|
|
:rtype: :py:data:`bytes`
|
|
|
|
.. versionadded:: 0.12
|
|
"""
|
|
obj = _lib.X509_EXTENSION_get_object(self._extension)
|
|
nid = _lib.OBJ_obj2nid(obj)
|
|
# OpenSSL 3.1.0 has a bug where nid2sn returns NULL for NIDs that
|
|
# previously returned UNDEF. This is a workaround for that issue.
|
|
# https://github.com/openssl/openssl/commit/908ba3ed9adbb3df90f76
|
|
buf = _lib.OBJ_nid2sn(nid)
|
|
if buf != _ffi.NULL:
|
|
return _ffi.string(buf)
|
|
else:
|
|
return b"UNDEF"
|
|
|
|
def get_data(self) -> bytes:
|
|
"""
|
|
Returns the data of the X509 extension, encoded as ASN.1.
|
|
|
|
:return: The ASN.1 encoded data of this X509 extension.
|
|
:rtype: :py:data:`bytes`
|
|
|
|
.. versionadded:: 0.12
|
|
"""
|
|
octet_result = _lib.X509_EXTENSION_get_data(self._extension)
|
|
string_result = _ffi.cast("ASN1_STRING*", octet_result)
|
|
char_result = _lib.ASN1_STRING_get0_data(string_result)
|
|
result_length = _lib.ASN1_STRING_length(string_result)
|
|
return _ffi.buffer(char_result, result_length)[:]
|
|
|
|
|
|
_X509ExtensionInternal = X509Extension
|
|
utils.deprecated(
|
|
X509Extension,
|
|
__name__,
|
|
(
|
|
"X509Extension support in pyOpenSSL is deprecated. You should use the "
|
|
"APIs in cryptography."
|
|
),
|
|
DeprecationWarning,
|
|
name="X509Extension",
|
|
)
|
|
|
|
|
|
class X509Req:
|
|
"""
|
|
An X.509 certificate signing requests.
|
|
|
|
.. deprecated:: 24.2.0
|
|
Use `cryptography.x509.CertificateSigningRequest` instead.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
req = _lib.X509_REQ_new()
|
|
self._req = _ffi.gc(req, _lib.X509_REQ_free)
|
|
# Default to version 0.
|
|
self.set_version(0)
|
|
|
|
def to_cryptography(self) -> x509.CertificateSigningRequest:
|
|
"""
|
|
Export as a ``cryptography`` certificate signing request.
|
|
|
|
:rtype: ``cryptography.x509.CertificateSigningRequest``
|
|
|
|
.. versionadded:: 17.1.0
|
|
"""
|
|
from cryptography.x509 import load_der_x509_csr
|
|
|
|
der = _dump_certificate_request_internal(FILETYPE_ASN1, self)
|
|
|
|
return load_der_x509_csr(der)
|
|
|
|
@classmethod
|
|
def from_cryptography(
|
|
cls, crypto_req: x509.CertificateSigningRequest
|
|
) -> X509Req:
|
|
"""
|
|
Construct based on a ``cryptography`` *crypto_req*.
|
|
|
|
:param crypto_req: A ``cryptography`` X.509 certificate signing request
|
|
:type crypto_req: ``cryptography.x509.CertificateSigningRequest``
|
|
|
|
:rtype: X509Req
|
|
|
|
.. versionadded:: 17.1.0
|
|
"""
|
|
if not isinstance(crypto_req, x509.CertificateSigningRequest):
|
|
raise TypeError("Must be a certificate signing request")
|
|
|
|
from cryptography.hazmat.primitives.serialization import Encoding
|
|
|
|
der = crypto_req.public_bytes(Encoding.DER)
|
|
return _load_certificate_request_internal(FILETYPE_ASN1, der)
|
|
|
|
def set_pubkey(self, pkey: PKey) -> None:
|
|
"""
|
|
Set the public key of the certificate signing request.
|
|
|
|
:param pkey: The public key to use.
|
|
:type pkey: :py:class:`PKey`
|
|
|
|
:return: ``None``
|
|
"""
|
|
set_result = _lib.X509_REQ_set_pubkey(self._req, pkey._pkey)
|
|
_openssl_assert(set_result == 1)
|
|
|
|
def get_pubkey(self) -> PKey:
|
|
"""
|
|
Get the public key of the certificate signing request.
|
|
|
|
:return: The public key.
|
|
:rtype: :py:class:`PKey`
|
|
"""
|
|
pkey = PKey.__new__(PKey)
|
|
pkey._pkey = _lib.X509_REQ_get_pubkey(self._req)
|
|
_openssl_assert(pkey._pkey != _ffi.NULL)
|
|
pkey._pkey = _ffi.gc(pkey._pkey, _lib.EVP_PKEY_free)
|
|
pkey._only_public = True
|
|
return pkey
|
|
|
|
def set_version(self, version: int) -> None:
|
|
"""
|
|
Set the version subfield (RFC 2986, section 4.1) of the certificate
|
|
request.
|
|
|
|
:param int version: The version number.
|
|
:return: ``None``
|
|
"""
|
|
if not isinstance(version, int):
|
|
raise TypeError("version must be an int")
|
|
if version != 0:
|
|
raise ValueError(
|
|
"Invalid version. The only valid version for X509Req is 0."
|
|
)
|
|
set_result = _lib.X509_REQ_set_version(self._req, version)
|
|
_openssl_assert(set_result == 1)
|
|
|
|
def get_version(self) -> int:
|
|
"""
|
|
Get the version subfield (RFC 2459, section 4.1.2.1) of the certificate
|
|
request.
|
|
|
|
:return: The value of the version subfield.
|
|
:rtype: :py:class:`int`
|
|
"""
|
|
return _lib.X509_REQ_get_version(self._req)
|
|
|
|
def get_subject(self) -> X509Name:
|
|
"""
|
|
Return the subject of this certificate signing request.
|
|
|
|
This creates a new :class:`X509Name` that wraps the underlying subject
|
|
name field on the certificate signing request. Modifying it will modify
|
|
the underlying signing request, and will have the effect of modifying
|
|
any other :class:`X509Name` that refers to this subject.
|
|
|
|
:return: The subject of this certificate signing request.
|
|
:rtype: :class:`X509Name`
|
|
"""
|
|
name = X509Name.__new__(X509Name)
|
|
name._name = _lib.X509_REQ_get_subject_name(self._req)
|
|
_openssl_assert(name._name != _ffi.NULL)
|
|
|
|
# The name is owned by the X509Req structure. As long as the X509Name
|
|
# Python object is alive, keep the X509Req Python object alive.
|
|
name._owner = self
|
|
|
|
return name
|
|
|
|
def add_extensions(
|
|
self, extensions: Iterable[_X509ExtensionInternal]
|
|
) -> None:
|
|
"""
|
|
Add extensions to the certificate signing request.
|
|
|
|
:param extensions: The X.509 extensions to add.
|
|
:type extensions: iterable of :py:class:`X509Extension`
|
|
:return: ``None``
|
|
"""
|
|
warnings.warn(
|
|
(
|
|
"This API is deprecated and will be removed in a future "
|
|
"version of pyOpenSSL. You should use pyca/cryptography's "
|
|
"X.509 APIs instead."
|
|
),
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
stack = _lib.sk_X509_EXTENSION_new_null()
|
|
_openssl_assert(stack != _ffi.NULL)
|
|
|
|
stack = _ffi.gc(stack, _lib.sk_X509_EXTENSION_free)
|
|
|
|
for ext in extensions:
|
|
if not isinstance(ext, _X509ExtensionInternal):
|
|
raise ValueError("One of the elements is not an X509Extension")
|
|
|
|
# TODO push can fail (here and elsewhere)
|
|
_lib.sk_X509_EXTENSION_push(stack, ext._extension)
|
|
|
|
add_result = _lib.X509_REQ_add_extensions(self._req, stack)
|
|
_openssl_assert(add_result == 1)
|
|
|
|
def get_extensions(self) -> list[_X509ExtensionInternal]:
|
|
"""
|
|
Get X.509 extensions in the certificate signing request.
|
|
|
|
:return: The X.509 extensions in this request.
|
|
:rtype: :py:class:`list` of :py:class:`X509Extension` objects.
|
|
|
|
.. versionadded:: 0.15
|
|
"""
|
|
warnings.warn(
|
|
(
|
|
"This API is deprecated and will be removed in a future "
|
|
"version of pyOpenSSL. You should use pyca/cryptography's "
|
|
"X.509 APIs instead."
|
|
),
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
exts = []
|
|
native_exts_obj = _lib.X509_REQ_get_extensions(self._req)
|
|
native_exts_obj = _ffi.gc(
|
|
native_exts_obj,
|
|
lambda x: _lib.sk_X509_EXTENSION_pop_free(
|
|
x,
|
|
_ffi.addressof(_lib._original_lib, "X509_EXTENSION_free"),
|
|
),
|
|
)
|
|
|
|
for i in range(_lib.sk_X509_EXTENSION_num(native_exts_obj)):
|
|
ext = _X509ExtensionInternal.__new__(_X509ExtensionInternal)
|
|
extension = _lib.X509_EXTENSION_dup(
|
|
_lib.sk_X509_EXTENSION_value(native_exts_obj, i)
|
|
)
|
|
ext._extension = _ffi.gc(extension, _lib.X509_EXTENSION_free)
|
|
exts.append(ext)
|
|
return exts
|
|
|
|
def sign(self, pkey: PKey, digest: str) -> None:
|
|
"""
|
|
Sign the certificate signing request with this key and digest type.
|
|
|
|
:param pkey: The key pair to sign with.
|
|
:type pkey: :py:class:`PKey`
|
|
:param digest: The name of the message digest to use for the signature,
|
|
e.g. :py:data:`"sha256"`.
|
|
:type digest: :py:class:`str`
|
|
:return: ``None``
|
|
"""
|
|
if pkey._only_public:
|
|
raise ValueError("Key has only public part")
|
|
|
|
if not pkey._initialized:
|
|
raise ValueError("Key is uninitialized")
|
|
|
|
digest_obj = _lib.EVP_get_digestbyname(_byte_string(digest))
|
|
if digest_obj == _ffi.NULL:
|
|
raise ValueError("No such digest method")
|
|
|
|
sign_result = _lib.X509_REQ_sign(self._req, pkey._pkey, digest_obj)
|
|
_openssl_assert(sign_result > 0)
|
|
|
|
def verify(self, pkey: PKey) -> bool:
|
|
"""
|
|
Verifies the signature on this certificate signing request.
|
|
|
|
:param PKey key: A public key.
|
|
|
|
:return: ``True`` if the signature is correct.
|
|
:rtype: bool
|
|
|
|
:raises OpenSSL.crypto.Error: If the signature is invalid or there is a
|
|
problem verifying the signature.
|
|
"""
|
|
if not isinstance(pkey, PKey):
|
|
raise TypeError("pkey must be a PKey instance")
|
|
|
|
result = _lib.X509_REQ_verify(self._req, pkey._pkey)
|
|
if result <= 0:
|
|
_raise_current_error()
|
|
|
|
return result
|
|
|
|
|
|
_X509ReqInternal = X509Req
|
|
|
|
utils.deprecated(
|
|
X509Req,
|
|
__name__,
|
|
(
|
|
"CSR support in pyOpenSSL is deprecated. You should use the APIs "
|
|
"in cryptography."
|
|
),
|
|
DeprecationWarning,
|
|
name="X509Req",
|
|
)
|
|
|
|
|
|
class X509:
|
|
"""
|
|
An X.509 certificate.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
x509 = _lib.X509_new()
|
|
_openssl_assert(x509 != _ffi.NULL)
|
|
self._x509 = _ffi.gc(x509, _lib.X509_free)
|
|
|
|
self._issuer_invalidator = _X509NameInvalidator()
|
|
self._subject_invalidator = _X509NameInvalidator()
|
|
|
|
@classmethod
|
|
def _from_raw_x509_ptr(cls, x509: Any) -> X509:
|
|
cert = cls.__new__(cls)
|
|
cert._x509 = _ffi.gc(x509, _lib.X509_free)
|
|
cert._issuer_invalidator = _X509NameInvalidator()
|
|
cert._subject_invalidator = _X509NameInvalidator()
|
|
return cert
|
|
|
|
def to_cryptography(self) -> x509.Certificate:
|
|
"""
|
|
Export as a ``cryptography`` certificate.
|
|
|
|
:rtype: ``cryptography.x509.Certificate``
|
|
|
|
.. versionadded:: 17.1.0
|
|
"""
|
|
from cryptography.x509 import load_der_x509_certificate
|
|
|
|
der = dump_certificate(FILETYPE_ASN1, self)
|
|
return load_der_x509_certificate(der)
|
|
|
|
@classmethod
|
|
def from_cryptography(cls, crypto_cert: x509.Certificate) -> X509:
|
|
"""
|
|
Construct based on a ``cryptography`` *crypto_cert*.
|
|
|
|
:param crypto_key: A ``cryptography`` X.509 certificate.
|
|
:type crypto_key: ``cryptography.x509.Certificate``
|
|
|
|
:rtype: X509
|
|
|
|
.. versionadded:: 17.1.0
|
|
"""
|
|
if not isinstance(crypto_cert, x509.Certificate):
|
|
raise TypeError("Must be a certificate")
|
|
|
|
from cryptography.hazmat.primitives.serialization import Encoding
|
|
|
|
der = crypto_cert.public_bytes(Encoding.DER)
|
|
return load_certificate(FILETYPE_ASN1, der)
|
|
|
|
def set_version(self, version: int) -> None:
|
|
"""
|
|
Set the version number of the certificate. Note that the
|
|
version value is zero-based, eg. a value of 0 is V1.
|
|
|
|
:param version: The version number of the certificate.
|
|
:type version: :py:class:`int`
|
|
|
|
:return: ``None``
|
|
"""
|
|
if not isinstance(version, int):
|
|
raise TypeError("version must be an integer")
|
|
|
|
_openssl_assert(_lib.X509_set_version(self._x509, version) == 1)
|
|
|
|
def get_version(self) -> int:
|
|
"""
|
|
Return the version number of the certificate.
|
|
|
|
:return: The version number of the certificate.
|
|
:rtype: :py:class:`int`
|
|
"""
|
|
return _lib.X509_get_version(self._x509)
|
|
|
|
def get_pubkey(self) -> PKey:
|
|
"""
|
|
Get the public key of the certificate.
|
|
|
|
:return: The public key.
|
|
:rtype: :py:class:`PKey`
|
|
"""
|
|
pkey = PKey.__new__(PKey)
|
|
pkey._pkey = _lib.X509_get_pubkey(self._x509)
|
|
if pkey._pkey == _ffi.NULL:
|
|
_raise_current_error()
|
|
pkey._pkey = _ffi.gc(pkey._pkey, _lib.EVP_PKEY_free)
|
|
pkey._only_public = True
|
|
return pkey
|
|
|
|
def set_pubkey(self, pkey: PKey) -> None:
|
|
"""
|
|
Set the public key of the certificate.
|
|
|
|
:param pkey: The public key.
|
|
:type pkey: :py:class:`PKey`
|
|
|
|
:return: :py:data:`None`
|
|
"""
|
|
if not isinstance(pkey, PKey):
|
|
raise TypeError("pkey must be a PKey instance")
|
|
|
|
set_result = _lib.X509_set_pubkey(self._x509, pkey._pkey)
|
|
_openssl_assert(set_result == 1)
|
|
|
|
def sign(self, pkey: PKey, digest: str) -> None:
|
|
"""
|
|
Sign the certificate with this key and digest type.
|
|
|
|
:param pkey: The key to sign with.
|
|
:type pkey: :py:class:`PKey`
|
|
|
|
:param digest: The name of the message digest to use.
|
|
:type digest: :py:class:`str`
|
|
|
|
:return: :py:data:`None`
|
|
"""
|
|
if not isinstance(pkey, PKey):
|
|
raise TypeError("pkey must be a PKey instance")
|
|
|
|
if pkey._only_public:
|
|
raise ValueError("Key only has public part")
|
|
|
|
if not pkey._initialized:
|
|
raise ValueError("Key is uninitialized")
|
|
|
|
evp_md = _lib.EVP_get_digestbyname(_byte_string(digest))
|
|
if evp_md == _ffi.NULL:
|
|
raise ValueError("No such digest method")
|
|
|
|
sign_result = _lib.X509_sign(self._x509, pkey._pkey, evp_md)
|
|
_openssl_assert(sign_result > 0)
|
|
|
|
def get_signature_algorithm(self) -> bytes:
|
|
"""
|
|
Return the signature algorithm used in the certificate.
|
|
|
|
:return: The name of the algorithm.
|
|
:rtype: :py:class:`bytes`
|
|
|
|
:raises ValueError: If the signature algorithm is undefined.
|
|
|
|
.. versionadded:: 0.13
|
|
"""
|
|
sig_alg = _lib.X509_get0_tbs_sigalg(self._x509)
|
|
alg = _ffi.new("ASN1_OBJECT **")
|
|
_lib.X509_ALGOR_get0(alg, _ffi.NULL, _ffi.NULL, sig_alg)
|
|
nid = _lib.OBJ_obj2nid(alg[0])
|
|
if nid == _lib.NID_undef:
|
|
raise ValueError("Undefined signature algorithm")
|
|
return _ffi.string(_lib.OBJ_nid2ln(nid))
|
|
|
|
def digest(self, digest_name: str) -> bytes:
|
|
"""
|
|
Return the digest of the X509 object.
|
|
|
|
:param digest_name: The name of the digest algorithm to use.
|
|
:type digest_name: :py:class:`str`
|
|
|
|
:return: The digest of the object, formatted as
|
|
:py:const:`b":"`-delimited hex pairs.
|
|
:rtype: :py:class:`bytes`
|
|
"""
|
|
digest = _lib.EVP_get_digestbyname(_byte_string(digest_name))
|
|
if digest == _ffi.NULL:
|
|
raise ValueError("No such digest method")
|
|
|
|
result_buffer = _ffi.new("unsigned char[]", _lib.EVP_MAX_MD_SIZE)
|
|
result_length = _ffi.new("unsigned int[]", 1)
|
|
result_length[0] = len(result_buffer)
|
|
|
|
digest_result = _lib.X509_digest(
|
|
self._x509, digest, result_buffer, result_length
|
|
)
|
|
_openssl_assert(digest_result == 1)
|
|
|
|
return b":".join(
|
|
[
|
|
b16encode(ch).upper()
|
|
for ch in _ffi.buffer(result_buffer, result_length[0])
|
|
]
|
|
)
|
|
|
|
def subject_name_hash(self) -> bytes:
|
|
"""
|
|
Return the hash of the X509 subject.
|
|
|
|
:return: The hash of the subject.
|
|
:rtype: :py:class:`bytes`
|
|
"""
|
|
return _lib.X509_subject_name_hash(self._x509)
|
|
|
|
def set_serial_number(self, serial: int) -> None:
|
|
"""
|
|
Set the serial number of the certificate.
|
|
|
|
:param serial: The new serial number.
|
|
:type serial: :py:class:`int`
|
|
|
|
:return: :py:data`None`
|
|
"""
|
|
if not isinstance(serial, int):
|
|
raise TypeError("serial must be an integer")
|
|
|
|
hex_serial = hex(serial)[2:]
|
|
hex_serial_bytes = hex_serial.encode("ascii")
|
|
|
|
bignum_serial = _ffi.new("BIGNUM**")
|
|
|
|
# BN_hex2bn stores the result in &bignum.
|
|
result = _lib.BN_hex2bn(bignum_serial, hex_serial_bytes)
|
|
_openssl_assert(result != _ffi.NULL)
|
|
|
|
asn1_serial = _lib.BN_to_ASN1_INTEGER(bignum_serial[0], _ffi.NULL)
|
|
_lib.BN_free(bignum_serial[0])
|
|
_openssl_assert(asn1_serial != _ffi.NULL)
|
|
asn1_serial = _ffi.gc(asn1_serial, _lib.ASN1_INTEGER_free)
|
|
set_result = _lib.X509_set_serialNumber(self._x509, asn1_serial)
|
|
_openssl_assert(set_result == 1)
|
|
|
|
def get_serial_number(self) -> int:
|
|
"""
|
|
Return the serial number of this certificate.
|
|
|
|
:return: The serial number.
|
|
:rtype: int
|
|
"""
|
|
asn1_serial = _lib.X509_get_serialNumber(self._x509)
|
|
bignum_serial = _lib.ASN1_INTEGER_to_BN(asn1_serial, _ffi.NULL)
|
|
try:
|
|
hex_serial = _lib.BN_bn2hex(bignum_serial)
|
|
try:
|
|
hexstring_serial = _ffi.string(hex_serial)
|
|
serial = int(hexstring_serial, 16)
|
|
return serial
|
|
finally:
|
|
_lib.OPENSSL_free(hex_serial)
|
|
finally:
|
|
_lib.BN_free(bignum_serial)
|
|
|
|
def gmtime_adj_notAfter(self, amount: int) -> None:
|
|
"""
|
|
Adjust the time stamp on which the certificate stops being valid.
|
|
|
|
:param int amount: The number of seconds by which to adjust the
|
|
timestamp.
|
|
:return: ``None``
|
|
"""
|
|
if not isinstance(amount, int):
|
|
raise TypeError("amount must be an integer")
|
|
|
|
notAfter = _lib.X509_getm_notAfter(self._x509)
|
|
_lib.X509_gmtime_adj(notAfter, amount)
|
|
|
|
def gmtime_adj_notBefore(self, amount: int) -> None:
|
|
"""
|
|
Adjust the timestamp on which the certificate starts being valid.
|
|
|
|
:param amount: The number of seconds by which to adjust the timestamp.
|
|
:return: ``None``
|
|
"""
|
|
if not isinstance(amount, int):
|
|
raise TypeError("amount must be an integer")
|
|
|
|
notBefore = _lib.X509_getm_notBefore(self._x509)
|
|
_lib.X509_gmtime_adj(notBefore, amount)
|
|
|
|
def has_expired(self) -> bool:
|
|
"""
|
|
Check whether the certificate has expired.
|
|
|
|
:return: ``True`` if the certificate has expired, ``False`` otherwise.
|
|
:rtype: bool
|
|
"""
|
|
time_bytes = self.get_notAfter()
|
|
if time_bytes is None:
|
|
raise ValueError("Unable to determine notAfter")
|
|
time_string = time_bytes.decode("utf-8")
|
|
not_after = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
|
|
|
|
UTC = datetime.timezone.utc
|
|
utcnow = datetime.datetime.now(UTC).replace(tzinfo=None)
|
|
return not_after < utcnow
|
|
|
|
def _get_boundary_time(self, which: Any) -> bytes | None:
|
|
return _get_asn1_time(which(self._x509))
|
|
|
|
def get_notBefore(self) -> bytes | None:
|
|
"""
|
|
Get the timestamp at which the certificate starts being valid.
|
|
|
|
The timestamp is formatted as an ASN.1 TIME::
|
|
|
|
YYYYMMDDhhmmssZ
|
|
|
|
:return: A timestamp string, or ``None`` if there is none.
|
|
:rtype: bytes or NoneType
|
|
"""
|
|
return self._get_boundary_time(_lib.X509_getm_notBefore)
|
|
|
|
def _set_boundary_time(
|
|
self, which: Callable[..., Any], when: bytes
|
|
) -> None:
|
|
return _set_asn1_time(which(self._x509), when)
|
|
|
|
def set_notBefore(self, when: bytes) -> None:
|
|
"""
|
|
Set the timestamp at which the certificate starts being valid.
|
|
|
|
The timestamp is formatted as an ASN.1 TIME::
|
|
|
|
YYYYMMDDhhmmssZ
|
|
|
|
:param bytes when: A timestamp string.
|
|
:return: ``None``
|
|
"""
|
|
return self._set_boundary_time(_lib.X509_getm_notBefore, when)
|
|
|
|
def get_notAfter(self) -> bytes | None:
|
|
"""
|
|
Get the timestamp at which the certificate stops being valid.
|
|
|
|
The timestamp is formatted as an ASN.1 TIME::
|
|
|
|
YYYYMMDDhhmmssZ
|
|
|
|
:return: A timestamp string, or ``None`` if there is none.
|
|
:rtype: bytes or NoneType
|
|
"""
|
|
return self._get_boundary_time(_lib.X509_getm_notAfter)
|
|
|
|
def set_notAfter(self, when: bytes) -> None:
|
|
"""
|
|
Set the timestamp at which the certificate stops being valid.
|
|
|
|
The timestamp is formatted as an ASN.1 TIME::
|
|
|
|
YYYYMMDDhhmmssZ
|
|
|
|
:param bytes when: A timestamp string.
|
|
:return: ``None``
|
|
"""
|
|
return self._set_boundary_time(_lib.X509_getm_notAfter, when)
|
|
|
|
def _get_name(self, which: Any) -> X509Name:
|
|
name = X509Name.__new__(X509Name)
|
|
name._name = which(self._x509)
|
|
_openssl_assert(name._name != _ffi.NULL)
|
|
|
|
# The name is owned by the X509 structure. As long as the X509Name
|
|
# Python object is alive, keep the X509 Python object alive.
|
|
name._owner = self
|
|
|
|
return name
|
|
|
|
def _set_name(self, which: Any, name: X509Name) -> None:
|
|
if not isinstance(name, X509Name):
|
|
raise TypeError("name must be an X509Name")
|
|
set_result = which(self._x509, name._name)
|
|
_openssl_assert(set_result == 1)
|
|
|
|
def get_issuer(self) -> X509Name:
|
|
"""
|
|
Return the issuer of this certificate.
|
|
|
|
This creates a new :class:`X509Name` that wraps the underlying issuer
|
|
name field on the certificate. Modifying it will modify the underlying
|
|
certificate, and will have the effect of modifying any other
|
|
:class:`X509Name` that refers to this issuer.
|
|
|
|
:return: The issuer of this certificate.
|
|
:rtype: :class:`X509Name`
|
|
"""
|
|
name = self._get_name(_lib.X509_get_issuer_name)
|
|
self._issuer_invalidator.add(name)
|
|
return name
|
|
|
|
def set_issuer(self, issuer: X509Name) -> None:
|
|
"""
|
|
Set the issuer of this certificate.
|
|
|
|
:param issuer: The issuer.
|
|
:type issuer: :py:class:`X509Name`
|
|
|
|
:return: ``None``
|
|
"""
|
|
self._set_name(_lib.X509_set_issuer_name, issuer)
|
|
self._issuer_invalidator.clear()
|
|
|
|
def get_subject(self) -> X509Name:
|
|
"""
|
|
Return the subject of this certificate.
|
|
|
|
This creates a new :class:`X509Name` that wraps the underlying subject
|
|
name field on the certificate. Modifying it will modify the underlying
|
|
certificate, and will have the effect of modifying any other
|
|
:class:`X509Name` that refers to this subject.
|
|
|
|
:return: The subject of this certificate.
|
|
:rtype: :class:`X509Name`
|
|
"""
|
|
name = self._get_name(_lib.X509_get_subject_name)
|
|
self._subject_invalidator.add(name)
|
|
return name
|
|
|
|
def set_subject(self, subject: X509Name) -> None:
|
|
"""
|
|
Set the subject of this certificate.
|
|
|
|
:param subject: The subject.
|
|
:type subject: :py:class:`X509Name`
|
|
|
|
:return: ``None``
|
|
"""
|
|
self._set_name(_lib.X509_set_subject_name, subject)
|
|
self._subject_invalidator.clear()
|
|
|
|
def get_extension_count(self) -> int:
|
|
"""
|
|
Get the number of extensions on this certificate.
|
|
|
|
:return: The number of extensions.
|
|
:rtype: :py:class:`int`
|
|
|
|
.. versionadded:: 0.12
|
|
"""
|
|
return _lib.X509_get_ext_count(self._x509)
|
|
|
|
def add_extensions(
|
|
self, extensions: Iterable[_X509ExtensionInternal]
|
|
) -> None:
|
|
"""
|
|
Add extensions to the certificate.
|
|
|
|
:param extensions: The extensions to add.
|
|
:type extensions: An iterable of :py:class:`X509Extension` objects.
|
|
:return: ``None``
|
|
"""
|
|
warnings.warn(
|
|
(
|
|
"This API is deprecated and will be removed in a future "
|
|
"version of pyOpenSSL. You should use pyca/cryptography's "
|
|
"X.509 APIs instead."
|
|
),
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
for ext in extensions:
|
|
if not isinstance(ext, _X509ExtensionInternal):
|
|
raise ValueError("One of the elements is not an X509Extension")
|
|
|
|
add_result = _lib.X509_add_ext(self._x509, ext._extension, -1)
|
|
_openssl_assert(add_result == 1)
|
|
|
|
def get_extension(self, index: int) -> _X509ExtensionInternal:
|
|
"""
|
|
Get a specific extension of the certificate by index.
|
|
|
|
Extensions on a certificate are kept in order. The index
|
|
parameter selects which extension will be returned.
|
|
|
|
:param int index: The index of the extension to retrieve.
|
|
:return: The extension at the specified index.
|
|
:rtype: :py:class:`X509Extension`
|
|
:raises IndexError: If the extension index was out of bounds.
|
|
|
|
.. versionadded:: 0.12
|
|
"""
|
|
warnings.warn(
|
|
(
|
|
"This API is deprecated and will be removed in a future "
|
|
"version of pyOpenSSL. You should use pyca/cryptography's "
|
|
"X.509 APIs instead."
|
|
),
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
ext = _X509ExtensionInternal.__new__(_X509ExtensionInternal)
|
|
ext._extension = _lib.X509_get_ext(self._x509, index)
|
|
if ext._extension == _ffi.NULL:
|
|
raise IndexError("extension index out of bounds")
|
|
|
|
extension = _lib.X509_EXTENSION_dup(ext._extension)
|
|
ext._extension = _ffi.gc(extension, _lib.X509_EXTENSION_free)
|
|
return ext
|
|
|
|
|
|
class X509StoreFlags:
|
|
"""
|
|
Flags for X509 verification, used to change the behavior of
|
|
:class:`X509Store`.
|
|
|
|
See `OpenSSL Verification Flags`_ for details.
|
|
|
|
.. _OpenSSL Verification Flags:
|
|
https://www.openssl.org/docs/manmaster/man3/X509_VERIFY_PARAM_set_flags.html
|
|
"""
|
|
|
|
CRL_CHECK: int = _lib.X509_V_FLAG_CRL_CHECK
|
|
CRL_CHECK_ALL: int = _lib.X509_V_FLAG_CRL_CHECK_ALL
|
|
IGNORE_CRITICAL: int = _lib.X509_V_FLAG_IGNORE_CRITICAL
|
|
X509_STRICT: int = _lib.X509_V_FLAG_X509_STRICT
|
|
ALLOW_PROXY_CERTS: int = _lib.X509_V_FLAG_ALLOW_PROXY_CERTS
|
|
POLICY_CHECK: int = _lib.X509_V_FLAG_POLICY_CHECK
|
|
EXPLICIT_POLICY: int = _lib.X509_V_FLAG_EXPLICIT_POLICY
|
|
INHIBIT_MAP: int = _lib.X509_V_FLAG_INHIBIT_MAP
|
|
CHECK_SS_SIGNATURE: int = _lib.X509_V_FLAG_CHECK_SS_SIGNATURE
|
|
PARTIAL_CHAIN: int = _lib.X509_V_FLAG_PARTIAL_CHAIN
|
|
|
|
|
|
class X509Store:
|
|
"""
|
|
An X.509 store.
|
|
|
|
An X.509 store is used to describe a context in which to verify a
|
|
certificate. A description of a context may include a set of certificates
|
|
to trust, a set of certificate revocation lists, verification flags and
|
|
more.
|
|
|
|
An X.509 store, being only a description, cannot be used by itself to
|
|
verify a certificate. To carry out the actual verification process, see
|
|
:class:`X509StoreContext`.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
store = _lib.X509_STORE_new()
|
|
self._store = _ffi.gc(store, _lib.X509_STORE_free)
|
|
|
|
def add_cert(self, cert: X509) -> None:
|
|
"""
|
|
Adds a trusted certificate to this store.
|
|
|
|
Adding a certificate with this method adds this certificate as a
|
|
*trusted* certificate.
|
|
|
|
:param X509 cert: The certificate to add to this store.
|
|
|
|
:raises TypeError: If the certificate is not an :class:`X509`.
|
|
|
|
:raises OpenSSL.crypto.Error: If OpenSSL was unhappy with your
|
|
certificate.
|
|
|
|
:return: ``None`` if the certificate was added successfully.
|
|
"""
|
|
if not isinstance(cert, X509):
|
|
raise TypeError()
|
|
|
|
res = _lib.X509_STORE_add_cert(self._store, cert._x509)
|
|
_openssl_assert(res == 1)
|
|
|
|
def add_crl(self, crl: x509.CertificateRevocationList) -> None:
|
|
"""
|
|
Add a certificate revocation list to this store.
|
|
|
|
The certificate revocation lists added to a store will only be used if
|
|
the associated flags are configured to check certificate revocation
|
|
lists.
|
|
|
|
.. versionadded:: 16.1.0
|
|
|
|
:param crl: The certificate revocation list to add to this store.
|
|
:type crl: ``cryptography.x509.CertificateRevocationList``
|
|
:return: ``None`` if the certificate revocation list was added
|
|
successfully.
|
|
"""
|
|
if isinstance(crl, x509.CertificateRevocationList):
|
|
from cryptography.hazmat.primitives.serialization import Encoding
|
|
|
|
bio = _new_mem_buf(crl.public_bytes(Encoding.DER))
|
|
openssl_crl = _lib.d2i_X509_CRL_bio(bio, _ffi.NULL)
|
|
_openssl_assert(openssl_crl != _ffi.NULL)
|
|
crl = _ffi.gc(openssl_crl, _lib.X509_CRL_free)
|
|
else:
|
|
raise TypeError(
|
|
"CRL must be of type "
|
|
"cryptography.x509.CertificateRevocationList"
|
|
)
|
|
|
|
_openssl_assert(_lib.X509_STORE_add_crl(self._store, crl) != 0)
|
|
|
|
def set_flags(self, flags: int) -> None:
|
|
"""
|
|
Set verification flags to this store.
|
|
|
|
Verification flags can be combined by oring them together.
|
|
|
|
.. note::
|
|
|
|
Setting a verification flag sometimes requires clients to add
|
|
additional information to the store, otherwise a suitable error will
|
|
be raised.
|
|
|
|
For example, in setting flags to enable CRL checking a
|
|
suitable CRL must be added to the store otherwise an error will be
|
|
raised.
|
|
|
|
.. versionadded:: 16.1.0
|
|
|
|
:param int flags: The verification flags to set on this store.
|
|
See :class:`X509StoreFlags` for available constants.
|
|
:return: ``None`` if the verification flags were successfully set.
|
|
"""
|
|
_openssl_assert(_lib.X509_STORE_set_flags(self._store, flags) != 0)
|
|
|
|
def set_time(self, vfy_time: datetime.datetime) -> None:
|
|
"""
|
|
Set the time against which the certificates are verified.
|
|
|
|
Normally the current time is used.
|
|
|
|
.. note::
|
|
|
|
For example, you can determine if a certificate was valid at a given
|
|
time.
|
|
|
|
.. versionadded:: 17.0.0
|
|
|
|
:param datetime vfy_time: The verification time to set on this store.
|
|
:return: ``None`` if the verification time was successfully set.
|
|
"""
|
|
param = _lib.X509_VERIFY_PARAM_new()
|
|
param = _ffi.gc(param, _lib.X509_VERIFY_PARAM_free)
|
|
|
|
_lib.X509_VERIFY_PARAM_set_time(
|
|
param, calendar.timegm(vfy_time.timetuple())
|
|
)
|
|
_openssl_assert(_lib.X509_STORE_set1_param(self._store, param) != 0)
|
|
|
|
def load_locations(
|
|
self, cafile: StrOrBytesPath, capath: StrOrBytesPath | None = None
|
|
) -> None:
|
|
"""
|
|
Let X509Store know where we can find trusted certificates for the
|
|
certificate chain. Note that the certificates have to be in PEM
|
|
format.
|
|
|
|
If *capath* is passed, it must be a directory prepared using the
|
|
``c_rehash`` tool included with OpenSSL. Either, but not both, of
|
|
*cafile* or *capath* may be ``None``.
|
|
|
|
.. note::
|
|
|
|
Both *cafile* and *capath* may be set simultaneously.
|
|
|
|
Call this method multiple times to add more than one location.
|
|
For example, CA certificates, and certificate revocation list bundles
|
|
may be passed in *cafile* in subsequent calls to this method.
|
|
|
|
.. versionadded:: 20.0
|
|
|
|
:param cafile: In which file we can find the certificates (``bytes`` or
|
|
``unicode``).
|
|
:param capath: In which directory we can find the certificates
|
|
(``bytes`` or ``unicode``).
|
|
|
|
:return: ``None`` if the locations were set successfully.
|
|
|
|
:raises OpenSSL.crypto.Error: If both *cafile* and *capath* is ``None``
|
|
or the locations could not be set for any reason.
|
|
|
|
"""
|
|
if cafile is None:
|
|
cafile = _ffi.NULL
|
|
else:
|
|
cafile = _path_bytes(cafile)
|
|
|
|
if capath is None:
|
|
capath = _ffi.NULL
|
|
else:
|
|
capath = _path_bytes(capath)
|
|
|
|
load_result = _lib.X509_STORE_load_locations(
|
|
self._store, cafile, capath
|
|
)
|
|
if not load_result:
|
|
_raise_current_error()
|
|
|
|
|
|
class X509StoreContextError(Exception):
|
|
"""
|
|
An exception raised when an error occurred while verifying a certificate
|
|
using `OpenSSL.X509StoreContext.verify_certificate`.
|
|
|
|
:ivar certificate: The certificate which caused verificate failure.
|
|
:type certificate: :class:`X509`
|
|
"""
|
|
|
|
def __init__(
|
|
self, message: str, errors: list[Any], certificate: X509
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.errors = errors
|
|
self.certificate = certificate
|
|
|
|
|
|
class X509StoreContext:
|
|
"""
|
|
An X.509 store context.
|
|
|
|
An X.509 store context is used to carry out the actual verification process
|
|
of a certificate in a described context. For describing such a context, see
|
|
:class:`X509Store`.
|
|
|
|
:param X509Store store: The certificates which will be trusted for the
|
|
purposes of any verifications.
|
|
:param X509 certificate: The certificate to be verified.
|
|
:param chain: List of untrusted certificates that may be used for building
|
|
the certificate chain. May be ``None``.
|
|
:type chain: :class:`list` of :class:`X509`
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
store: X509Store,
|
|
certificate: X509,
|
|
chain: Sequence[X509] | None = None,
|
|
) -> None:
|
|
self._store = store
|
|
self._cert = certificate
|
|
self._chain = self._build_certificate_stack(chain)
|
|
|
|
@staticmethod
|
|
def _build_certificate_stack(
|
|
certificates: Sequence[X509] | None,
|
|
) -> None:
|
|
def cleanup(s: Any) -> None:
|
|
# Equivalent to sk_X509_pop_free, but we don't
|
|
# currently have a CFFI binding for that available
|
|
for i in range(_lib.sk_X509_num(s)):
|
|
x = _lib.sk_X509_value(s, i)
|
|
_lib.X509_free(x)
|
|
_lib.sk_X509_free(s)
|
|
|
|
if certificates is None or len(certificates) == 0:
|
|
return _ffi.NULL
|
|
|
|
stack = _lib.sk_X509_new_null()
|
|
_openssl_assert(stack != _ffi.NULL)
|
|
stack = _ffi.gc(stack, cleanup)
|
|
|
|
for cert in certificates:
|
|
if not isinstance(cert, X509):
|
|
raise TypeError("One of the elements is not an X509 instance")
|
|
|
|
_openssl_assert(_lib.X509_up_ref(cert._x509) > 0)
|
|
if _lib.sk_X509_push(stack, cert._x509) <= 0:
|
|
_lib.X509_free(cert._x509)
|
|
_raise_current_error()
|
|
|
|
return stack
|
|
|
|
@staticmethod
|
|
def _exception_from_context(store_ctx: Any) -> X509StoreContextError:
|
|
"""
|
|
Convert an OpenSSL native context error failure into a Python
|
|
exception.
|
|
|
|
When a call to native OpenSSL X509_verify_cert fails, additional
|
|
information about the failure can be obtained from the store context.
|
|
"""
|
|
message = _ffi.string(
|
|
_lib.X509_verify_cert_error_string(
|
|
_lib.X509_STORE_CTX_get_error(store_ctx)
|
|
)
|
|
).decode("utf-8")
|
|
errors = [
|
|
_lib.X509_STORE_CTX_get_error(store_ctx),
|
|
_lib.X509_STORE_CTX_get_error_depth(store_ctx),
|
|
message,
|
|
]
|
|
# A context error should always be associated with a certificate, so we
|
|
# expect this call to never return :class:`None`.
|
|
_x509 = _lib.X509_STORE_CTX_get_current_cert(store_ctx)
|
|
_cert = _lib.X509_dup(_x509)
|
|
pycert = X509._from_raw_x509_ptr(_cert)
|
|
return X509StoreContextError(message, errors, pycert)
|
|
|
|
def _verify_certificate(self) -> Any:
|
|
"""
|
|
Verifies the certificate and runs an X509_STORE_CTX containing the
|
|
results.
|
|
|
|
:raises X509StoreContextError: If an error occurred when validating a
|
|
certificate in the context. Sets ``certificate`` attribute to
|
|
indicate which certificate caused the error.
|
|
"""
|
|
store_ctx = _lib.X509_STORE_CTX_new()
|
|
_openssl_assert(store_ctx != _ffi.NULL)
|
|
store_ctx = _ffi.gc(store_ctx, _lib.X509_STORE_CTX_free)
|
|
|
|
ret = _lib.X509_STORE_CTX_init(
|
|
store_ctx, self._store._store, self._cert._x509, self._chain
|
|
)
|
|
_openssl_assert(ret == 1)
|
|
|
|
ret = _lib.X509_verify_cert(store_ctx)
|
|
if ret <= 0:
|
|
raise self._exception_from_context(store_ctx)
|
|
|
|
return store_ctx
|
|
|
|
def set_store(self, store: X509Store) -> None:
|
|
"""
|
|
Set the context's X.509 store.
|
|
|
|
.. versionadded:: 0.15
|
|
|
|
:param X509Store store: The store description which will be used for
|
|
the purposes of any *future* verifications.
|
|
"""
|
|
self._store = store
|
|
|
|
def verify_certificate(self) -> None:
|
|
"""
|
|
Verify a certificate in a context.
|
|
|
|
.. versionadded:: 0.15
|
|
|
|
:raises X509StoreContextError: If an error occurred when validating a
|
|
certificate in the context. Sets ``certificate`` attribute to
|
|
indicate which certificate caused the error.
|
|
"""
|
|
self._verify_certificate()
|
|
|
|
def get_verified_chain(self) -> list[X509]:
|
|
"""
|
|
Verify a certificate in a context and return the complete validated
|
|
chain.
|
|
|
|
:raises X509StoreContextError: If an error occurred when validating a
|
|
certificate in the context. Sets ``certificate`` attribute to
|
|
indicate which certificate caused the error.
|
|
|
|
.. versionadded:: 20.0
|
|
"""
|
|
store_ctx = self._verify_certificate()
|
|
|
|
# Note: X509_STORE_CTX_get1_chain returns a deep copy of the chain.
|
|
cert_stack = _lib.X509_STORE_CTX_get1_chain(store_ctx)
|
|
_openssl_assert(cert_stack != _ffi.NULL)
|
|
|
|
result = []
|
|
for i in range(_lib.sk_X509_num(cert_stack)):
|
|
cert = _lib.sk_X509_value(cert_stack, i)
|
|
_openssl_assert(cert != _ffi.NULL)
|
|
pycert = X509._from_raw_x509_ptr(cert)
|
|
result.append(pycert)
|
|
|
|
# Free the stack but not the members which are freed by the X509 class.
|
|
_lib.sk_X509_free(cert_stack)
|
|
return result
|
|
|
|
|
|
def load_certificate(type: int, buffer: bytes) -> X509:
|
|
"""
|
|
Load a certificate (X509) from the string *buffer* encoded with the
|
|
type *type*.
|
|
|
|
:param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1)
|
|
|
|
:param bytes buffer: The buffer the certificate is stored in
|
|
|
|
:return: The X509 object
|
|
"""
|
|
if isinstance(buffer, str):
|
|
buffer = buffer.encode("ascii")
|
|
|
|
bio = _new_mem_buf(buffer)
|
|
|
|
if type == FILETYPE_PEM:
|
|
x509 = _lib.PEM_read_bio_X509(bio, _ffi.NULL, _ffi.NULL, _ffi.NULL)
|
|
elif type == FILETYPE_ASN1:
|
|
x509 = _lib.d2i_X509_bio(bio, _ffi.NULL)
|
|
else:
|
|
raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1")
|
|
|
|
if x509 == _ffi.NULL:
|
|
_raise_current_error()
|
|
|
|
return X509._from_raw_x509_ptr(x509)
|
|
|
|
|
|
def dump_certificate(type: int, cert: X509) -> bytes:
|
|
"""
|
|
Dump the certificate *cert* into a buffer string encoded with the type
|
|
*type*.
|
|
|
|
:param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1, or
|
|
FILETYPE_TEXT)
|
|
:param cert: The certificate to dump
|
|
:return: The buffer with the dumped certificate in
|
|
"""
|
|
bio = _new_mem_buf()
|
|
|
|
if type == FILETYPE_PEM:
|
|
result_code = _lib.PEM_write_bio_X509(bio, cert._x509)
|
|
elif type == FILETYPE_ASN1:
|
|
result_code = _lib.i2d_X509_bio(bio, cert._x509)
|
|
elif type == FILETYPE_TEXT:
|
|
result_code = _lib.X509_print_ex(bio, cert._x509, 0, 0)
|
|
else:
|
|
raise ValueError(
|
|
"type argument must be FILETYPE_PEM, FILETYPE_ASN1, or "
|
|
"FILETYPE_TEXT"
|
|
)
|
|
|
|
_openssl_assert(result_code == 1)
|
|
return _bio_to_string(bio)
|
|
|
|
|
|
def dump_publickey(type: int, pkey: PKey) -> bytes:
|
|
"""
|
|
Dump a public key to a buffer.
|
|
|
|
:param type: The file type (one of :data:`FILETYPE_PEM` or
|
|
:data:`FILETYPE_ASN1`).
|
|
:param PKey pkey: The public key to dump
|
|
:return: The buffer with the dumped key in it.
|
|
:rtype: bytes
|
|
"""
|
|
bio = _new_mem_buf()
|
|
if type == FILETYPE_PEM:
|
|
write_bio = _lib.PEM_write_bio_PUBKEY
|
|
elif type == FILETYPE_ASN1:
|
|
write_bio = _lib.i2d_PUBKEY_bio
|
|
else:
|
|
raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1")
|
|
|
|
result_code = write_bio(bio, pkey._pkey)
|
|
if result_code != 1: # pragma: no cover
|
|
_raise_current_error()
|
|
|
|
return _bio_to_string(bio)
|
|
|
|
|
|
def dump_privatekey(
|
|
type: int,
|
|
pkey: PKey,
|
|
cipher: str | None = None,
|
|
passphrase: PassphraseCallableT | None = None,
|
|
) -> bytes:
|
|
"""
|
|
Dump the private key *pkey* into a buffer string encoded with the type
|
|
*type*. Optionally (if *type* is :const:`FILETYPE_PEM`) encrypting it
|
|
using *cipher* and *passphrase*.
|
|
|
|
:param type: The file type (one of :const:`FILETYPE_PEM`,
|
|
:const:`FILETYPE_ASN1`, or :const:`FILETYPE_TEXT`)
|
|
:param PKey pkey: The PKey to dump
|
|
:param cipher: (optional) if encrypted PEM format, the cipher to use
|
|
:param passphrase: (optional) if encrypted PEM format, this can be either
|
|
the passphrase to use, or a callback for providing the passphrase.
|
|
|
|
:return: The buffer with the dumped key in
|
|
:rtype: bytes
|
|
"""
|
|
bio = _new_mem_buf()
|
|
|
|
if not isinstance(pkey, PKey):
|
|
raise TypeError("pkey must be a PKey")
|
|
|
|
if cipher is not None:
|
|
if passphrase is None:
|
|
raise TypeError(
|
|
"if a value is given for cipher "
|
|
"one must also be given for passphrase"
|
|
)
|
|
cipher_obj = _lib.EVP_get_cipherbyname(_byte_string(cipher))
|
|
if cipher_obj == _ffi.NULL:
|
|
raise ValueError("Invalid cipher name")
|
|
else:
|
|
cipher_obj = _ffi.NULL
|
|
|
|
helper = _PassphraseHelper(type, passphrase)
|
|
if type == FILETYPE_PEM:
|
|
result_code = _lib.PEM_write_bio_PrivateKey(
|
|
bio,
|
|
pkey._pkey,
|
|
cipher_obj,
|
|
_ffi.NULL,
|
|
0,
|
|
helper.callback,
|
|
helper.callback_args,
|
|
)
|
|
helper.raise_if_problem()
|
|
elif type == FILETYPE_ASN1:
|
|
result_code = _lib.i2d_PrivateKey_bio(bio, pkey._pkey)
|
|
elif type == FILETYPE_TEXT:
|
|
if _lib.EVP_PKEY_id(pkey._pkey) != _lib.EVP_PKEY_RSA:
|
|
raise TypeError("Only RSA keys are supported for FILETYPE_TEXT")
|
|
|
|
rsa = _ffi.gc(_lib.EVP_PKEY_get1_RSA(pkey._pkey), _lib.RSA_free)
|
|
result_code = _lib.RSA_print(bio, rsa, 0)
|
|
else:
|
|
raise ValueError(
|
|
"type argument must be FILETYPE_PEM, FILETYPE_ASN1, or "
|
|
"FILETYPE_TEXT"
|
|
)
|
|
|
|
_openssl_assert(result_code != 0)
|
|
|
|
return _bio_to_string(bio)
|
|
|
|
|
|
class _PassphraseHelper:
|
|
def __init__(
|
|
self,
|
|
type: int,
|
|
passphrase: PassphraseCallableT | None,
|
|
more_args: bool = False,
|
|
truncate: bool = False,
|
|
) -> None:
|
|
if type != FILETYPE_PEM and passphrase is not None:
|
|
raise ValueError(
|
|
"only FILETYPE_PEM key format supports encryption"
|
|
)
|
|
self._passphrase = passphrase
|
|
self._more_args = more_args
|
|
self._truncate = truncate
|
|
self._problems: list[Exception] = []
|
|
|
|
@property
|
|
def callback(self) -> Any:
|
|
if self._passphrase is None:
|
|
return _ffi.NULL
|
|
elif isinstance(self._passphrase, bytes) or callable(self._passphrase):
|
|
return _ffi.callback("pem_password_cb", self._read_passphrase)
|
|
else:
|
|
raise TypeError(
|
|
"Last argument must be a byte string or a callable."
|
|
)
|
|
|
|
@property
|
|
def callback_args(self) -> Any:
|
|
if self._passphrase is None:
|
|
return _ffi.NULL
|
|
elif isinstance(self._passphrase, bytes) or callable(self._passphrase):
|
|
return _ffi.NULL
|
|
else:
|
|
raise TypeError(
|
|
"Last argument must be a byte string or a callable."
|
|
)
|
|
|
|
def raise_if_problem(self, exceptionType: type[Exception] = Error) -> None:
|
|
if self._problems:
|
|
# Flush the OpenSSL error queue
|
|
try:
|
|
_exception_from_error_queue(exceptionType)
|
|
except exceptionType:
|
|
pass
|
|
|
|
raise self._problems.pop(0)
|
|
|
|
def _read_passphrase(
|
|
self, buf: Any, size: int, rwflag: Any, userdata: Any
|
|
) -> int:
|
|
try:
|
|
if callable(self._passphrase):
|
|
if self._more_args:
|
|
result = self._passphrase(size, rwflag, userdata)
|
|
else:
|
|
result = self._passphrase(rwflag)
|
|
else:
|
|
assert self._passphrase is not None
|
|
result = self._passphrase
|
|
if not isinstance(result, bytes):
|
|
raise ValueError("Bytes expected")
|
|
if len(result) > size:
|
|
if self._truncate:
|
|
result = result[:size]
|
|
else:
|
|
raise ValueError(
|
|
"passphrase returned by callback is too long"
|
|
)
|
|
for i in range(len(result)):
|
|
buf[i] = result[i : i + 1]
|
|
return len(result)
|
|
except Exception as e:
|
|
self._problems.append(e)
|
|
return 0
|
|
|
|
|
|
def load_publickey(type: int, buffer: str | bytes) -> PKey:
|
|
"""
|
|
Load a public key from a buffer.
|
|
|
|
:param type: The file type (one of :data:`FILETYPE_PEM`,
|
|
:data:`FILETYPE_ASN1`).
|
|
:param buffer: The buffer the key is stored in.
|
|
:type buffer: A Python string object, either unicode or bytestring.
|
|
:return: The PKey object.
|
|
:rtype: :class:`PKey`
|
|
"""
|
|
if isinstance(buffer, str):
|
|
buffer = buffer.encode("ascii")
|
|
|
|
bio = _new_mem_buf(buffer)
|
|
|
|
if type == FILETYPE_PEM:
|
|
evp_pkey = _lib.PEM_read_bio_PUBKEY(
|
|
bio, _ffi.NULL, _ffi.NULL, _ffi.NULL
|
|
)
|
|
elif type == FILETYPE_ASN1:
|
|
evp_pkey = _lib.d2i_PUBKEY_bio(bio, _ffi.NULL)
|
|
else:
|
|
raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1")
|
|
|
|
if evp_pkey == _ffi.NULL:
|
|
_raise_current_error()
|
|
|
|
pkey = PKey.__new__(PKey)
|
|
pkey._pkey = _ffi.gc(evp_pkey, _lib.EVP_PKEY_free)
|
|
pkey._only_public = True
|
|
return pkey
|
|
|
|
|
|
def load_privatekey(
|
|
type: int,
|
|
buffer: str | bytes,
|
|
passphrase: PassphraseCallableT | None = None,
|
|
) -> PKey:
|
|
"""
|
|
Load a private key (PKey) from the string *buffer* encoded with the type
|
|
*type*.
|
|
|
|
:param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1)
|
|
:param buffer: The buffer the key is stored in
|
|
:param passphrase: (optional) if encrypted PEM format, this can be
|
|
either the passphrase to use, or a callback for
|
|
providing the passphrase.
|
|
|
|
:return: The PKey object
|
|
"""
|
|
if isinstance(buffer, str):
|
|
buffer = buffer.encode("ascii")
|
|
|
|
bio = _new_mem_buf(buffer)
|
|
|
|
helper = _PassphraseHelper(type, passphrase)
|
|
if type == FILETYPE_PEM:
|
|
evp_pkey = _lib.PEM_read_bio_PrivateKey(
|
|
bio, _ffi.NULL, helper.callback, helper.callback_args
|
|
)
|
|
helper.raise_if_problem()
|
|
elif type == FILETYPE_ASN1:
|
|
evp_pkey = _lib.d2i_PrivateKey_bio(bio, _ffi.NULL)
|
|
else:
|
|
raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1")
|
|
|
|
if evp_pkey == _ffi.NULL:
|
|
_raise_current_error()
|
|
|
|
pkey = PKey.__new__(PKey)
|
|
pkey._pkey = _ffi.gc(evp_pkey, _lib.EVP_PKEY_free)
|
|
return pkey
|
|
|
|
|
|
def dump_certificate_request(type: int, req: X509Req) -> bytes:
|
|
"""
|
|
Dump the certificate request *req* into a buffer string encoded with the
|
|
type *type*.
|
|
|
|
:param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1)
|
|
:param req: The certificate request to dump
|
|
:return: The buffer with the dumped certificate request in
|
|
|
|
|
|
.. deprecated:: 24.2.0
|
|
Use `cryptography.x509.CertificateSigningRequest` instead.
|
|
"""
|
|
bio = _new_mem_buf()
|
|
|
|
if type == FILETYPE_PEM:
|
|
result_code = _lib.PEM_write_bio_X509_REQ(bio, req._req)
|
|
elif type == FILETYPE_ASN1:
|
|
result_code = _lib.i2d_X509_REQ_bio(bio, req._req)
|
|
elif type == FILETYPE_TEXT:
|
|
result_code = _lib.X509_REQ_print_ex(bio, req._req, 0, 0)
|
|
else:
|
|
raise ValueError(
|
|
"type argument must be FILETYPE_PEM, FILETYPE_ASN1, or "
|
|
"FILETYPE_TEXT"
|
|
)
|
|
|
|
_openssl_assert(result_code != 0)
|
|
|
|
return _bio_to_string(bio)
|
|
|
|
|
|
_dump_certificate_request_internal = dump_certificate_request
|
|
|
|
utils.deprecated(
|
|
dump_certificate_request,
|
|
__name__,
|
|
(
|
|
"CSR support in pyOpenSSL is deprecated. You should use the APIs "
|
|
"in cryptography."
|
|
),
|
|
DeprecationWarning,
|
|
name="dump_certificate_request",
|
|
)
|
|
|
|
|
|
def load_certificate_request(type: int, buffer: bytes) -> X509Req:
|
|
"""
|
|
Load a certificate request (X509Req) from the string *buffer* encoded with
|
|
the type *type*.
|
|
|
|
:param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1)
|
|
:param buffer: The buffer the certificate request is stored in
|
|
:return: The X509Req object
|
|
|
|
.. deprecated:: 24.2.0
|
|
Use `cryptography.x509.load_der_x509_csr` or
|
|
`cryptography.x509.load_pem_x509_csr` instead.
|
|
"""
|
|
if isinstance(buffer, str):
|
|
buffer = buffer.encode("ascii")
|
|
|
|
bio = _new_mem_buf(buffer)
|
|
|
|
if type == FILETYPE_PEM:
|
|
req = _lib.PEM_read_bio_X509_REQ(bio, _ffi.NULL, _ffi.NULL, _ffi.NULL)
|
|
elif type == FILETYPE_ASN1:
|
|
req = _lib.d2i_X509_REQ_bio(bio, _ffi.NULL)
|
|
else:
|
|
raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1")
|
|
|
|
_openssl_assert(req != _ffi.NULL)
|
|
|
|
x509req = _X509ReqInternal.__new__(_X509ReqInternal)
|
|
x509req._req = _ffi.gc(req, _lib.X509_REQ_free)
|
|
return x509req
|
|
|
|
|
|
_load_certificate_request_internal = load_certificate_request
|
|
|
|
utils.deprecated(
|
|
load_certificate_request,
|
|
__name__,
|
|
(
|
|
"CSR support in pyOpenSSL is deprecated. You should use the APIs "
|
|
"in cryptography."
|
|
),
|
|
DeprecationWarning,
|
|
name="load_certificate_request",
|
|
)
|