Repository: pyca/cryptography (Python)

Issue:
Support encoding SCT data into an X.509 extension
Continued from #3461 since we've done everything but encoding them and it turns out that's more than a bit of a pain.


Hints:
Since this has come up again in #4820: @alex didn't you run into some significant problems with SCT creation for reasons I no longer recall but hopefully you do?

Patch:
diff --git a/src/_cffi_src/openssl/ct.py b/src/_cffi_src/openssl/ct.py
--- a/src/_cffi_src/openssl/ct.py
+++ b/src/_cffi_src/openssl/ct.py
@@ -50,13 +50,14 @@
 
 int SCT_set_source(SCT *, sct_source_t);
 
+Cryptography_STACK_OF_SCT *sk_SCT_new_null(void);
+void sk_SCT_free(Cryptography_STACK_OF_SCT *);
 int sk_SCT_num(const Cryptography_STACK_OF_SCT *);
 SCT *sk_SCT_value(const Cryptography_STACK_OF_SCT *, int);
+int sk_SCT_push(Cryptography_STACK_OF_SCT *, SCT *);
 
 void SCT_LIST_free(Cryptography_STACK_OF_SCT *);
 
-int sk_SCT_push(Cryptography_STACK_OF_SCT *, SCT *);
-Cryptography_STACK_OF_SCT *sk_SCT_new_null(void);
 SCT *SCT_new(void);
 int SCT_set1_log_id(SCT *, unsigned char *, size_t);
 void SCT_set_timestamp(SCT *, uint64_t);
@@ -101,12 +102,13 @@
 
 int (*SCT_set_source)(SCT *, sct_source_t) = NULL;
 
+Cryptography_STACK_OF_SCT *(*sk_SCT_new_null)(void) = NULL;
+void (*sk_SCT_free)(Cryptography_STACK_OF_SCT *) = NULL;
 int (*sk_SCT_num)(const Cryptography_STACK_OF_SCT *) = NULL;
 SCT *(*sk_SCT_value)(const Cryptography_STACK_OF_SCT *, int) = NULL;
+int (*sk_SCT_push)(Cryptography_STACK_OF_SCT *, SCT *) = NULL;
 
 void (*SCT_LIST_free)(Cryptography_STACK_OF_SCT *) = NULL;
-int (*sk_SCT_push)(Cryptography_STACK_OF_SCT *, SCT *) = NULL;
-Cryptography_STACK_OF_SCT *(*sk_SCT_new_null)(void) = NULL;
 SCT *(*SCT_new)(void) = NULL;
 int (*SCT_set1_log_id)(SCT *, unsigned char *, size_t) = NULL;
 void (*SCT_set_timestamp)(SCT *, uint64_t) = NULL;
diff --git a/src/cryptography/hazmat/backends/openssl/encode_asn1.py b/src/cryptography/hazmat/backends/openssl/encode_asn1.py
--- a/src/cryptography/hazmat/backends/openssl/encode_asn1.py
+++ b/src/cryptography/hazmat/backends/openssl/encode_asn1.py
@@ -604,11 +604,21 @@ def _encode_general_subtree(backend, subtrees):
             gs = backend._lib.GENERAL_SUBTREE_new()
             gs.base = _encode_general_name(backend, name)
             res = backend._lib.sk_GENERAL_SUBTREE_push(general_subtrees, gs)
-            assert res >= 1
+            backend.openssl_assert(res >= 1)
 
         return general_subtrees
 
 
+def _encode_precert_signed_certificate_timestamps(backend, scts):
+    sct_stack = backend._lib.sk_SCT_new_null()
+    backend.openssl_assert(sct_stack != backend._ffi.NULL)
+    sct_stack = backend._ffi.gc(sct_stack, backend._lib.sk_SCT_free)
+    for sct in scts:
+        res = backend._lib.sk_SCT_push(sct_stack, sct._sct)
+        backend.openssl_assert(res >= 1)
+    return sct_stack
+
+
 def _encode_nonce(backend, nonce):
     return _encode_asn1_str_gc(backend, nonce.nonce)
 
@@ -630,6 +640,9 @@ def _encode_nonce(backend, nonce):
     ExtensionOID.OCSP_NO_CHECK: _encode_ocsp_nocheck,
     ExtensionOID.NAME_CONSTRAINTS: _encode_name_constraints,
     ExtensionOID.POLICY_CONSTRAINTS: _encode_policy_constraints,
+    ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS: (
+        _encode_precert_signed_certificate_timestamps
+    ),
 }
 
 _CRL_EXTENSION_ENCODE_HANDLERS = {
diff --git a/src/cryptography/hazmat/bindings/openssl/_conditional.py b/src/cryptography/hazmat/bindings/openssl/_conditional.py
--- a/src/cryptography/hazmat/bindings/openssl/_conditional.py
+++ b/src/cryptography/hazmat/bindings/openssl/_conditional.py
@@ -100,11 +100,12 @@ def cryptography_has_sct():
         "SCT_get0_signature",
         "SCT_get_timestamp",
         "SCT_set_source",
+        "sk_SCT_new_null",
+        "sk_SCT_free",
         "sk_SCT_num",
         "sk_SCT_value",
-        "SCT_LIST_free",
         "sk_SCT_push",
-        "sk_SCT_new_null",
+        "SCT_LIST_free",
         "SCT_new",
         "SCT_set1_log_id",
         "SCT_set_timestamp",


Test patch:
diff --git a/tests/x509/test_x509_ext.py b/tests/x509/test_x509_ext.py
--- a/tests/x509/test_x509_ext.py
+++ b/tests/x509/test_x509_ext.py
@@ -5678,6 +5678,33 @@ def test_simple(self, backend):
             == x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE
         )
 
+    @pytest.mark.supported(
+        only_if=lambda backend: (backend._lib.Cryptography_HAS_SCT),
+        skip_message="Requires CT support",
+    )
+    def test_generate(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
+
+        private_key = RSA_KEY_2048.private_key(backend)
+        builder = _make_certbuilder(private_key).add_extension(
+            x509.PrecertificateSignedCertificateTimestamps([sct]),
+            critical=False,
+        )
+        cert = builder.sign(private_key, hashes.SHA256(), backend)
+        ext = cert.extensions.get_extension_for_class(
+            x509.PrecertificateSignedCertificateTimestamps
+        ).value
+        assert list(ext) == [sct]
+
     @pytest.mark.supported(
         only_if=lambda backend: backend._lib.CRYPTOGRAPHY_IS_LIBRESSL,
         skip_message="Requires LibreSSL",