Skip to content

Commit

Permalink
Refs #3461 -- parse SCTs from x.509 extension (#3480)
Browse files Browse the repository at this point in the history
* Stub API for SCTs, feedback wanted

* grr, flake8

* finish up the __init__

* Initial implementation and tests

* write a test. it fails because computer

* get the tests passing and fix some TODOs

* changelog entry

* This can go now

* Put a skip in this test

* grump

* Removed unreachable code

* moved changelog to the correct section

* Use the deocrator for expressing requirements

* This needs f for the right entry_type

* coverage

* syntax error

* tests for coverage

* better sct eq tests

* docs

* technically correct, the most useless kind of correct

* typo and more details

* bug

* drop __eq__
  • Loading branch information
alex authored and reaperhulk committed Jun 4, 2017
1 parent 140ec5d commit 6a0718f
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 6 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Changelog
and
:class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
in favor of ``verify``.
* Added support for parsing
:class:`~cryptography.x509.certificate_transparency.SignedCertificateTimestamp`
objects from X.509 certificate extensions.

1.9 - 2017-05-29
~~~~~~~~~~~~~~~~
Expand Down
6 changes: 3 additions & 3 deletions docs/x509/certificate-transparency.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ issued.

.. class:: SignedCertificateTimestamp

.. versionadded:: 1.9
.. versionadded:: 2.0

SignedCertificateTimestamps (SCTs) are small cryptographically signed
assertions that the specified certificate has been submitted to a
Expand Down Expand Up @@ -53,7 +53,7 @@ issued.

.. class:: Version

.. versionadded:: 1.9
.. versionadded:: 2.0

An enumeration for SignedCertificateTimestamp versions.

Expand All @@ -63,7 +63,7 @@ issued.

.. class:: LogEntryType

.. versionadded:: 1.9
.. versionadded:: 2.0

An enumeration for SignedCertificateTimestamp log entry types.

Expand Down
26 changes: 26 additions & 0 deletions docs/x509/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1814,6 +1814,32 @@ X.509 Extensions
:returns: A list of values extracted from the matched general names.


.. class:: PrecertificateSignedCertificateTimestamps(scts)

.. versionadded:: 2.0

This extension contains
:class:`~cryptography.x509.certificate_transparency.SignedCertificateTimestamp`
instances which were issued for the pre-certificate corresponding to this
certificate. These can be used to verify that the certificate is included
in a public Certificate Transparency log.

It is an iterable containing one or more
:class:`~cryptography.x509.certificate_transparency.SignedCertificateTimestamp`
objects.

:param list scts: A ``list`` of
:class:`~cryptography.x509.certificate_transparency.SignedCertificateTimestamp`
objects.

.. attribute:: oid

:type: :class:`ObjectIdentifier`

Returns
:attr:`~cryptography.x509.oid.ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS`.


.. class:: AuthorityInformationAccess(descriptions)

.. versionadded:: 0.9
Expand Down
18 changes: 18 additions & 0 deletions src/cryptography/hazmat/backends/openssl/decode_asn1.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,21 @@ def _decode_inhibit_any_policy(backend, asn1_int):
return x509.InhibitAnyPolicy(skip_certs)


def _decode_precert_signed_certificate_timestamps(backend, asn1_scts):
from cryptography.hazmat.backends.openssl.x509 import (
_SignedCertificateTimestamp
)
asn1_scts = backend._ffi.cast("Cryptography_STACK_OF_SCT *", asn1_scts)
asn1_scts = backend._ffi.gc(asn1_scts, backend._lib.SCT_LIST_free)

scts = []
for i in range(backend._lib.sk_SCT_num(asn1_scts)):
sct = backend._lib.sk_SCT_value(asn1_scts, i)

scts.append(_SignedCertificateTimestamp(backend, asn1_scts, sct))
return x509.PrecertificateSignedCertificateTimestamps(scts)


# CRLReason ::= ENUMERATED {
# unspecified (0),
# keyCompromise (1),
Expand Down Expand Up @@ -751,6 +766,9 @@ def _parse_asn1_generalized_time(backend, generalized_time):
ExtensionOID.ISSUER_ALTERNATIVE_NAME: _decode_issuer_alt_name,
ExtensionOID.NAME_CONSTRAINTS: _decode_name_constraints,
ExtensionOID.POLICY_CONSTRAINTS: _decode_policy_constraints,
ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS: (
_decode_precert_signed_certificate_timestamps
),
}

_REVOKED_EXTENSION_HANDLERS = {
Expand Down
41 changes: 41 additions & 0 deletions src/cryptography/hazmat/backends/openssl/x509.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import absolute_import, division, print_function

import datetime
import operator
import warnings

Expand Down Expand Up @@ -433,3 +434,43 @@ def is_signature_valid(self):
return False

return True


@utils.register_interface(
x509.certificate_transparency.SignedCertificateTimestamp
)
class _SignedCertificateTimestamp(object):
def __init__(self, backend, sct_list, sct):
self._backend = backend
# Keep the SCT_LIST that this SCT came from alive.
self._sct_list = sct_list
self._sct = sct

@property
def version(self):
version = self._backend._lib.SCT_get_version(self._sct)
assert version == self._backend._lib.SCT_VERSION_V1
return x509.certificate_transparency.Version.v1

@property
def log_id(self):
out = self._backend._ffi.new("unsigned char **")
log_id_length = self._backend._lib.SCT_get0_log_id(self._sct, out)
assert log_id_length >= 0
return self._backend._ffi.buffer(out[0], log_id_length)[:]

@property
def timestamp(self):
timestamp = self._backend._lib.SCT_get_timestamp(self._sct)
milliseconds = timestamp % 1000
return datetime.datetime.utcfromtimestamp(
timestamp // 1000
).replace(microsecond=milliseconds * 1000)

@property
def entry_type(self):
entry_type = self._backend._lib.SCT_get_log_entry_type(self._sct)
# We currently only support loading SCTs from the X.509 extension, so
# we only have precerts.
assert entry_type == self._backend._lib.CT_LOG_ENTRY_TYPE_PRECERT
return x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE
7 changes: 4 additions & 3 deletions src/cryptography/x509/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
ExtensionNotFound, ExtensionType, Extensions, GeneralNames,
InhibitAnyPolicy, InvalidityDate, IssuerAlternativeName, KeyUsage,
NameConstraints, NoticeReference, OCSPNoCheck, PolicyConstraints,
PolicyInformation, ReasonFlags, SubjectAlternativeName,
SubjectKeyIdentifier, UnrecognizedExtension, UnsupportedExtension,
UserNotice
PolicyInformation, PrecertificateSignedCertificateTimestamps, ReasonFlags,
SubjectAlternativeName, SubjectKeyIdentifier, UnrecognizedExtension,
UnsupportedExtension, UserNotice
)
from cryptography.x509.general_name import (
DNSName, DirectoryName, GeneralName, IPAddress, OtherName, RFC822Name,
Expand Down Expand Up @@ -185,4 +185,5 @@
"InvalidityDate",
"UnrecognizedExtension",
"PolicyConstraints",
"PrecertificateSignedCertificateTimestamps",
]
36 changes: 36 additions & 0 deletions src/cryptography/x509/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
from cryptography.hazmat.primitives import constant_time, serialization
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from cryptography.x509.certificate_transparency import (
SignedCertificateTimestamp
)
from cryptography.x509.general_name import GeneralName, IPAddress, OtherName
from cryptography.x509.name import RelativeDistinguishedName
from cryptography.x509.oid import (
Expand Down Expand Up @@ -1151,6 +1154,39 @@ def __hash__(self):
invalidity_date = utils.read_only_property("_invalidity_date")


@utils.register_interface(ExtensionType)
class PrecertificateSignedCertificateTimestamps(object):
oid = ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS

def __init__(self, signed_certificate_timestamps):
signed_certificate_timestamps = list(signed_certificate_timestamps)
if not all(
isinstance(sct, SignedCertificateTimestamp)
for sct in signed_certificate_timestamps
):
raise TypeError(
"Every item in the signed_certificate_timestamps list must be "
"a SignedCertificateTimestamp"
)
self._signed_certificate_timestamps = signed_certificate_timestamps

def __iter__(self):
return iter(self._signed_certificate_timestamps)

def __len__(self):
return len(self._signed_certificate_timestamps)

def __getitem__(self, idx):
return self._signed_certificate_timestamps[idx]

def __repr__(self):
return (
"<PrecertificateSignedCertificateTimestamps({0})>".format(
list(self)
)
)


@utils.register_interface(ExtensionType)
class UnrecognizedExtension(object):
def __init__(self, oid, value):
Expand Down
42 changes: 42 additions & 0 deletions tests/test_x509_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -3666,6 +3666,48 @@ def test_nocheck(self, backend):
assert iap.skip_certs == 5


@pytest.mark.requires_backend_interface(interface=RSABackend)
@pytest.mark.requires_backend_interface(interface=X509Backend)
@pytest.mark.supported(
only_if=lambda backend: backend._lib.CRYPTOGRAPHY_OPENSSL_110F_OR_GREATER,
skip_message="Requires OpenSSL 1.1.0f+",
)
class TestPrecertificateSignedCertificateTimestampsExtension(object):
def test_init(self):
with pytest.raises(TypeError):
x509.PrecertificateSignedCertificateTimestamps([object()])

def test_repr(self):
assert repr(x509.PrecertificateSignedCertificateTimestamps([])) == (
"<PrecertificateSignedCertificateTimestamps([])>"
)

def test_simple(self, backend):
cert = _load_cert(
os.path.join("x509", "badssl-sct.pem"),
x509.load_pem_x509_certificate,
backend
)
scts = cert.extensions.get_extension_for_class(
x509.PrecertificateSignedCertificateTimestamps
).value
assert len(scts) == 1
[sct] = scts
assert scts[0] == sct
assert sct.version == x509.certificate_transparency.Version.v1
assert sct.log_id == (
b"\xa7\xceJNb\x07\xe0\xad\xde\xe5\xfd\xaaK\x1f\x86v\x87g\xb5\xd0"
b"\x02\xa5]G1\x0e~g\n\x95\xea\xb2"
)
assert sct.timestamp == datetime.datetime(
2016, 11, 17, 1, 56, 25, 396000
)
assert (
sct.entry_type ==
x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE
)


@pytest.mark.requires_backend_interface(interface=RSABackend)
@pytest.mark.requires_backend_interface(interface=X509Backend)
class TestInvalidExtension(object):
Expand Down

0 comments on commit 6a0718f

Please sign in to comment.