-
Notifications
You must be signed in to change notification settings - Fork 422
Enable use of CRL (and more) in verify context #281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1472,18 +1472,45 @@ def get_extension(self, index): | |
|
||
class X509Store(object): | ||
""" | ||
An X509 certificate store. | ||
""" | ||
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 | ||
:py:class:`X509StoreContext`. | ||
""" | ||
|
||
CRL_CHECK = _lib.X509_V_FLAG_CRL_CHECK | ||
CRL_CHECK_ALL = _lib.X509_V_FLAG_CRL_CHECK_ALL | ||
IGNORE_CRITICAL = _lib.X509_V_FLAG_IGNORE_CRITICAL | ||
X509_STRICT = _lib.X509_V_FLAG_X509_STRICT | ||
ALLOW_PROXY_CERTS = _lib.X509_V_FLAG_ALLOW_PROXY_CERTS | ||
POLICY_CHECK = _lib.X509_V_FLAG_POLICY_CHECK | ||
EXPLICIT_POLICY = _lib.X509_V_FLAG_EXPLICIT_POLICY | ||
# FLAG_INHIBIT_ANY = _lib.X509_V_FLAG_FLAG_INHIBIT_ANY | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some flags are newer than others. How do I tell what flags are supported by my underlying OpenSSL implementation? |
||
INHIBIT_MAP = _lib.X509_V_FLAG_INHIBIT_MAP | ||
NOTIFY_POLICY = _lib.X509_V_FLAG_NOTIFY_POLICY | ||
# USE_DELTAS = _lib.X509_V_FLAG_USE_DELTAS | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not add Delta CRL support? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found that some of these flags were added in different versions of OpenSSL. I don't know how to conditionally add them if they're present. Do you have any suggestions on conditionally adding the flag if it is available in the underlying OpenSSL? |
||
CHECK_SS_SIGNATURE = _lib.X509_V_FLAG_CHECK_SS_SIGNATURE | ||
CB_ISSUER_CHECK = _lib.X509_V_FLAG_CB_ISSUER_CHECK | ||
# NO_ALT_CHAINS = _lib.X509_V_FLAG_NO_ALT_CHAINS | ||
|
||
|
||
def __init__(self): | ||
store = _lib.X509_STORE_new() | ||
self._store = _ffi.gc(store, _lib.X509_STORE_free) | ||
|
||
|
||
def add_cert(self, cert): | ||
""" | ||
Adds the certificate :py:data:`cert` to this store. | ||
Adds a trusted certificate to this store. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't entirely correct. It is also used to add intermediate certificates (at least in your tests), which are not 'trusted' unless linked to a trusted self signed root certificate. If X509_V_FLAG_PARTIAL_CHAINS is specified, it would be true, but that has other dangers. Currently the API to add 'chain' certificates to the StoreCTX that aren't trusted isn't used (_lib.X509_STORE_CTX_set_chain). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good points. I'll update the wording here later today. |
||
|
||
This is the Python equivalent of OpenSSL's ``X509_STORE_add_cert``. | ||
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 :py:class:`X509`. | ||
|
@@ -1498,9 +1525,53 @@ def add_cert(self, cert): | |
_raise_current_error() | ||
|
||
|
||
def add_crl(self, crl): | ||
""" | ||
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:: 0.16 | ||
|
||
:param CRL crl: The certificate revocation list to add to this store. | ||
:return: :py:data:`None` if the certificate revocation list was added successfully. | ||
""" | ||
if _lib.X509_STORE_add_crl(self._store, crl._crl) == 0: | ||
_raise_current_error() | ||
|
||
|
||
def set_flags(self, flags): | ||
""" | ||
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:: 0.16 | ||
|
||
:param int flags: The verification flags to set on this store. | ||
:return: :py:data:`None` if the verification flags were successfully set. | ||
""" | ||
if _lib.X509_STORE_set_flags(self._store, flags) == 0: | ||
_raise_current_error() | ||
|
||
|
||
|
||
X509StoreType = X509Store | ||
|
||
|
||
|
||
class X509StoreContextError(Exception): | ||
""" | ||
An exception raised when an error occurred while verifying a certificate | ||
|
@@ -1518,29 +1589,19 @@ class X509StoreContext(object): | |
""" | ||
An X.509 store context. | ||
|
||
An :py:class:`X509StoreContext` is used to define some of the criteria for | ||
certificate verification. The information encapsulated in this object | ||
includes, but is not limited to, a set of trusted certificates, | ||
verification parameters, and revoked certificates. | ||
|
||
.. note:: | ||
|
||
Currently, one can only set the trusted certificates on an | ||
:py:class:`X509StoreContext`. Future versions of pyOpenSSL will expose | ||
verification parameters and certificate revocation lists. | ||
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 | ||
:py:class:`X509Store`. | ||
|
||
:ivar _store_ctx: The underlying X509_STORE_CTX structure used by this | ||
instance. It is dynamically allocated and automatically garbage | ||
collected. | ||
|
||
:ivar _store: See the ``store`` ``__init__`` parameter. | ||
|
||
:ivar _cert: See the ``certificate`` ``__init__`` parameter. | ||
|
||
:param X509Store store: The certificates which will be trusted for the | ||
purposes of any verifications. | ||
|
||
:param X509 certificate: The certificate to be verified. | ||
|
||
""" | ||
|
||
def __init__(self, store, certificate): | ||
|
@@ -1598,12 +1659,12 @@ def _exception_from_context(self): | |
|
||
def set_store(self, store): | ||
""" | ||
Set the context's trust store. | ||
Set the context's X.509 store. | ||
|
||
.. versionadded:: 0.15 | ||
|
||
:param X509Store store: The certificates which will be trusted for the | ||
purposes of any *future* verifications. | ||
:param X509Store store: The store description which will be used for | ||
the purposes of any *future* verifications. | ||
""" | ||
self._store = store | ||
|
||
|
@@ -1614,11 +1675,9 @@ def verify_certificate(self): | |
|
||
.. versionadded:: 0.15 | ||
|
||
:param store_ctx: The :py:class:`X509StoreContext` to verify. | ||
|
||
:raises X509StoreContextError: If an error occured when validating a | ||
certificate in the context. Sets ``certificate`` attribute to indicate | ||
which certificate caused the error. | ||
certificate in the context. Sets ``certificate`` attribute to indicate | ||
which certificate caused the error. | ||
""" | ||
# Always re-initialize the store context in case | ||
# :py:meth:`verify_certificate` is called multiple times. | ||
|
@@ -1958,9 +2017,6 @@ class CRL(object): | |
A certificate revocation list. | ||
""" | ||
def __init__(self): | ||
""" | ||
Create a new empty certificate revocation list. | ||
""" | ||
crl = _lib.X509_CRL_new() | ||
self._crl = _ffi.gc(crl, _lib.X509_CRL_free) | ||
|
||
|
@@ -1995,9 +2051,7 @@ def add_revoked(self, revoked): | |
means it's okay to mutate it after adding: it won't affect | ||
this CRL. | ||
|
||
:param revoked: The new revocation. | ||
:type revoked: :class:`Revoked` | ||
|
||
:param Revoked revoked: The new revocation. | ||
:return: :py:const:`None` | ||
""" | ||
copy = _X509_REVOKED_dup(revoked._revoked) | ||
|
@@ -2011,27 +2065,123 @@ def add_revoked(self, revoked): | |
_raise_current_error() | ||
|
||
|
||
def export(self, cert, key, type=FILETYPE_PEM, days=100, | ||
digest=_UNSPECIFIED): | ||
def get_issuer(self): | ||
""" | ||
Export a CRL as a string. | ||
Get the CRL's issuer. | ||
|
||
:param cert: The certificate used to sign the CRL. | ||
:type cert: :py:class:`X509` | ||
.. versionadded:: 0.16 | ||
|
||
:param key: The key used to sign the CRL. | ||
:type key: :py:class:`PKey` | ||
:return: :py:class:`X509Name` | ||
""" | ||
_issuer = _lib.X509_NAME_dup(_lib.X509_CRL_get_issuer(self._crl)) | ||
if _issuer == _ffi.NULL: | ||
_raise_current_error() | ||
_issuer = _ffi.gc(_issuer, _lib.X509_NAME_free) | ||
issuer = X509Name.__new__(X509Name) | ||
issuer._name = _issuer | ||
return issuer | ||
|
||
:param type: The export format, either :py:data:`FILETYPE_PEM`, | ||
:py:data:`FILETYPE_ASN1`, or :py:data:`FILETYPE_TEXT`. | ||
|
||
:param int days: The number of days until the next update of this CRL. | ||
def set_version(self, version): | ||
""" | ||
Set the CRL version. | ||
|
||
.. versionadded:: 0.16 | ||
|
||
:param int version: The version of the CRL. | ||
:return: :py:const:`None` | ||
""" | ||
if _lib.X509_CRL_set_version(self._crl, version) == 0: | ||
_raise_current_error() | ||
|
||
|
||
def _set_boundary_time(self, which, when): | ||
return _set_asn1_time(which(self._crl), when) | ||
|
||
|
||
def set_lastUpdate(self, when): | ||
""" | ||
Set when the CRL was last updated. | ||
|
||
The timestamp is formatted as an ASN.1 GENERALIZEDTIME:: | ||
|
||
YYYYMMDDhhmmssZ | ||
YYYYMMDDhhmmss+hhmm | ||
YYYYMMDDhhmmss-hhmm | ||
|
||
.. versionadded:: 0.16 | ||
|
||
:param bytes when: A timestamp string. | ||
:return: :py:const:`None` | ||
""" | ||
return self._set_boundary_time(_lib.X509_CRL_get_lastUpdate, when) | ||
|
||
|
||
def set_nextUpdate(self, when): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any good reason why this does not take a datetime.datetime object instead of a string? Same for set_lastUpdate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only "good" reason I have is that other API in this module use this format. I'd insist that we keep it consistent, but I would also be happy to change the other API to use a datetime if the maintainers are okay with a backwards incompatible API change. |
||
""" | ||
Set when the CRL will next be udpated. | ||
|
||
The timestamp is formatted as an ASN.1 GENERALIZEDTIME:: | ||
|
||
YYYYMMDDhhmmssZ | ||
YYYYMMDDhhmmss+hhmm | ||
YYYYMMDDhhmmss-hhmm | ||
|
||
.. versionadded:: 0.16 | ||
|
||
:param bytes when: A timestamp string. | ||
:return: :py:const:`None` | ||
""" | ||
return self._set_boundary_time(_lib.X509_CRL_get_nextUpdate, when) | ||
|
||
|
||
def sign(self, issuer_cert, issuer_key, digest='sha1'): | ||
""" | ||
Sign the CRL. | ||
|
||
Signing a CRL enables clients to associate the CRL itself with an | ||
issuer. Before a CRL is meaningful to other OpenSSL functions, it must | ||
be signed by an issuer. | ||
|
||
This method implicitly sets the issuer's name based on the issuer | ||
certificate and private key used to sign the CRL. | ||
|
||
.. versionadded:: 0.16 | ||
|
||
:param X509 issuer_cert: The issuer's certificate. | ||
:param PKey issuer_key: The issuer's private key. | ||
:param str digest: The digest method to sign the CRL with. | ||
""" | ||
digest_obj = _lib.EVP_get_digestbyname(digest) | ||
if digest_obj == _ffi.NULL: | ||
raise ValueError("No such digest method") | ||
_lib.X509_CRL_set_issuer_name(self._crl, _lib.X509_get_subject_name(issuer_cert._x509)) | ||
_lib.X509_CRL_sort(self._crl) | ||
sign_result = _lib.X509_CRL_sign(self._crl, issuer_key._pkey, digest_obj) | ||
if not sign_result: | ||
_raise_current_error() | ||
|
||
|
||
def export(self, cert, key, type=FILETYPE_PEM, days=100, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is bad but I cannot fix it to use the new functions added in this PR without changing the public API of pyOpenSSL. I would appreciate some guidance from the maintainers on ways they want to handle public API changes. E.g., do we do semantic versioning, do we just do bust clients, something else? This same problem is present in #258. The solution is simple, but it requires a public API change or the additional of new methods. |
||
digest=_UNSPECIFIED): | ||
""" | ||
Export the CRL as a string. | ||
|
||
:param X509 cert: The certificate used to sign the CRL. | ||
:param PKey key: The key used to sign the CRL. | ||
:param int type: The export format, either :py:data:`FILETYPE_PEM`, | ||
:py:data:`FILETYPE_ASN1`, or :py:data:`FILETYPE_TEXT`. | ||
:param int days: The number of days until the next update of this CRL. | ||
:param bytes digest: The name of the message digest to use (eg | ||
``b"sha1"``). | ||
|
||
:return: :py:data:`bytes` | ||
""" | ||
|
||
# TODO: fix this function to use functionality added in version 0.16. | ||
# Doing this without changing the public API is tricky. Checking if | ||
# lastUpdate, nextUpdate, issuer or signing has happened is hard to do | ||
# without generating a segmentation fault. | ||
|
||
if not isinstance(cert, X509): | ||
raise TypeError("cert must be an X509 instance") | ||
if not isinstance(key, PKey): | ||
|
@@ -2090,6 +2240,8 @@ def export(self, cert, key, type=FILETYPE_PEM, days=100, | |
_raise_current_error() | ||
|
||
return _bio_to_string(bio) | ||
|
||
|
||
CRLType = CRL | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not all of the flags below are tested. What do folks prefer I do here: 1) expose all of the flags below and let people use them even though they're not tested or 2) only expose the flags that have been specifically tested?