From ccb24b5f1fc4d59f388f24bab80459a599e7e6f7 Mon Sep 17 00:00:00 2001 From: Gabriel Cruz <8129788+gmelodie@users.noreply.github.com> Date: Tue, 6 May 2025 15:56:51 -0300 Subject: [PATCH] feat(cert): add certificate signing request (CSR) generation (#1355) --- libp2p/transports/tls/certificate.c | 129 ++++++++++++++++++++++ libp2p/transports/tls/certificate.h | 19 ++++ libp2p/transports/tls/certificate.nim | 4 +- libp2p/transports/tls/certificate_ffi.nim | 4 + tests/transports/tls/testcertificate.nim | 53 ++++++++- 5 files changed, 206 insertions(+), 3 deletions(-) diff --git a/libp2p/transports/tls/certificate.c b/libp2p/transports/tls/certificate.c index fe35d58e5..b4acb7cdf 100644 --- a/libp2p/transports/tls/certificate.c +++ b/libp2p/transports/tls/certificate.c @@ -959,4 +959,133 @@ void cert_free_key(cert_key_t key) { struct cert_key_s *k = (struct cert_key_s *)key; EVP_PKEY_free(k->pkey); free(k); +} + +// Function to check if a Common Name is correct +// each label should have <= 63 characters +// the whole CN should have <= 253 characters +cert_error_t check_cn(const char *cn) { + cert_error_t ret_code = CERT_SUCCESS; + + if (!cn || strlen(cn) == 0) { + return CERT_ERROR_CN_EMPTY; + } + if (strlen(cn) > 253) { + return CERT_ERROR_CN_TOO_LONG; + } + + char *cn_copy = strdup(cn); + char *cn_copy_orig = cn_copy; + + // trim trailing dot if any before checking + size_t len = strlen(cn_copy); + if (len > 0 && cn_copy[len - 1] == '.') { + cn_copy[len - 1] = '\0'; + } + + char *label; + char *last = NULL; + char *ptr = cn_copy; + + while ((label = strtok(ptr, ".")) != NULL) { + if (last && last + strlen(last) + 1 != label) { + // empty label (e.g., "example..com") + ret_code = CERT_ERROR_CN_EMPTY_LABEL; + break; + } + if (strlen(label) > 63) { + ret_code = CERT_ERROR_CN_LABEL_TOO_LONG; + break; + } + last = label; + ptr = NULL; + } + + free(cn_copy_orig); + return ret_code; +} + +cert_error_t cert_signing_req(const char *cn, cert_key_t key, cert_buffer **csr_buffer) { + cert_error_t ret_code = CERT_SUCCESS; + X509_REQ *x509_req = NULL; + X509_NAME *name = NULL; + X509_EXTENSION *ext = NULL; + X509V3_CTX ctx; + STACK_OF(X509_EXTENSION) *exts = NULL; + unsigned char *der = NULL; + size_t der_len = 0; + + ret_code = check_cn(cn); + if (ret_code != CERT_SUCCESS) { + goto cleanup; + } + + if (!key || !(key->pkey)) { + ret_code = CERT_ERROR_NO_PUBKEY; + goto cleanup; + } + EVP_PKEY *pkey = key->pkey; + + x509_req = X509_REQ_new(); + if (!x509_req) { + ret_code = CERT_ERROR_X509_REQ_GEN; + goto cleanup; + } + + if (!X509_REQ_set_pubkey(x509_req, pkey)) { + ret_code = CERT_ERROR_PUBKEY_SET; + goto cleanup; + } + + // Build SAN extension + X509V3_set_ctx(&ctx, NULL, NULL, x509_req, NULL, 0); + char san_str[258]; // max of 253 from cn + 4 "DNS:" + \0 + snprintf(san_str, sizeof(san_str), "DNS:%s", cn); + + ext = X509V3_EXT_conf_nid(NULL, &ctx, NID_subject_alt_name, san_str); + if (!ext) { + ret_code = CERT_ERROR_X509_SAN; + goto cleanup; + } + + exts = sk_X509_EXTENSION_new_null(); + if (!exts || !sk_X509_EXTENSION_push(exts, ext)) { + ret_code = CERT_ERROR_X509_SAN; + goto cleanup; + } + + if (!X509_REQ_add_extensions(x509_req, exts)) { + ret_code = CERT_ERROR_X509_SAN; + goto cleanup; + } + + if (!X509_REQ_sign(x509_req, pkey, EVP_sha256())) { + ret_code = CERT_ERROR_SIGN; + goto cleanup; + } + + der_len = i2d_X509_REQ(x509_req, &der); + if (der_len < 0) { + ret_code = CERT_ERROR_X509_REQ_DER; + goto cleanup; + } + + ret_code = init_cert_buffer(csr_buffer, der, der_len); + if (ret_code < 0) { + goto cleanup; + } + +cleanup: + if (exts) + sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free); + if (x509_req) + X509_REQ_free(x509_req); + if (der) + OPENSSL_free(der); + if (ret_code != CERT_SUCCESS && csr_buffer) { + cert_free_buffer(*csr_buffer); + *csr_buffer = NULL; + } + + return ret_code; } \ No newline at end of file diff --git a/libp2p/transports/tls/certificate.h b/libp2p/transports/tls/certificate.h index ca5892056..cf0416bc7 100644 --- a/libp2p/transports/tls/certificate.h +++ b/libp2p/transports/tls/certificate.h @@ -54,6 +54,14 @@ typedef int32_t cert_error_t; #define CERT_ERROR_PUBKEY_DER_CONV -41 #define CERT_ERROR_INIT_KEYGEN -42 #define CERT_ERROR_SET_CURVE -43 +#define CERT_ERROR_X509_REQ_GEN -44 +#define CERT_ERROR_X509_REQ_DER -45 +#define CERT_ERROR_NO_PUBKEY -46 +#define CERT_ERROR_X509_SAN -47 +#define CERT_ERROR_CN_TOO_LONG -48 +#define CERT_ERROR_CN_LABEL_TOO_LONG -49 +#define CERT_ERROR_CN_EMPTY_LABEL -50 +#define CERT_ERROR_CN_EMPTY -51 typedef enum { CERT_FORMAT_DER = 0, CERT_FORMAT_PEM = 1 } cert_format_t; @@ -184,4 +192,15 @@ void cert_free_key(cert_key_t key); */ void cert_free_buffer(cert_buffer *buffer); +/** + * Create a X.509 certificate request + * + * @param cn Domain for which we're requesting the certificate + * @param key Public key of the requesting client + * @param csr_buffer Pointer to the buffer that will be set to the CSR in DER format + * + * @return CERT_SUCCESS on successful execution, an error code otherwise + */ +cert_error_t cert_signing_req(const char *cn, cert_key_t key, cert_buffer **csr_buffer); + #endif /* LIBP2P_CERT_H */ \ No newline at end of file diff --git a/libp2p/transports/tls/certificate.nim b/libp2p/transports/tls/certificate.nim index 30539b459..9494b2013 100644 --- a/libp2p/transports/tls/certificate.nim +++ b/libp2p/transports/tls/certificate.nim @@ -55,10 +55,10 @@ type EncodingFormat* = enum proc cert_format_t(self: EncodingFormat): cert_format_t = if self == EncodingFormat.DER: CERT_FORMAT_DER else: CERT_FORMAT_PEM -proc toCertBuffer(self: seq[uint8]): cert_buffer = +proc toCertBuffer*(self: seq[uint8]): cert_buffer = cert_buffer(data: self[0].unsafeAddr, length: self.len.csize_t) -proc toSeq(self: ptr cert_buffer): seq[byte] = +proc toSeq*(self: ptr cert_buffer): seq[byte] = toOpenArray(cast[ptr UncheckedArray[byte]](self.data), 0, self.length.int - 1).toSeq() # Initialize entropy and DRBG contexts at the module level diff --git a/libp2p/transports/tls/certificate_ffi.nim b/libp2p/transports/tls/certificate_ffi.nim index 1743f1de9..50677c583 100644 --- a/libp2p/transports/tls/certificate_ffi.nim +++ b/libp2p/transports/tls/certificate_ffi.nim @@ -79,3 +79,7 @@ proc cert_free_buffer*( proc cert_free_parsed*( cert: ptr cert_parsed ): void {.cdecl, importc: "cert_free_parsed".} + +proc cert_signing_req*( + cn: cstring, key: cert_key_t, csr_buffer: ptr ptr cert_buffer +): cert_error_t {.cdecl, importc: "cert_signing_req".} diff --git a/tests/transports/tls/testcertificate.nim b/tests/transports/tls/testcertificate.nim index 95c846b24..7e8855d39 100644 --- a/tests/transports/tls/testcertificate.nim +++ b/tests/transports/tls/testcertificate.nim @@ -1,7 +1,8 @@ import unittest2 -import times +import times, base64 import ../../../libp2p/transports/tls/certificate +import ../../../libp2p/transports/tls/certificate_ffi import ../../../libp2p/crypto/crypto import ../../../libp2p/peerid @@ -107,6 +108,56 @@ suite "Test vectors": # should not verify check not cert.verify() + test "CSR generation": + var certKey: cert_key_t + var certCtx: cert_context_t + var derCSR: ptr cert_buffer = nil + + check cert_init_drbg("seed".cstring, 4, certCtx.addr) == CERT_SUCCESS + check cert_generate_key(certCtx, certKey.addr) == CERT_SUCCESS + + check cert_signing_req("my.domain.string".cstring, certKey, derCSR.addr) == + CERT_SUCCESS + + check cert_signing_req( + "my.big.domain.string.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaa".cstring, + # 253 characters, no labels longer than 63, okay + certKey, + derCSR.addr, + ) == CERT_SUCCESS + + check cert_signing_req( + "my.domain.".cstring, # domain ending in ".", okay + certKey, + derCSR.addr, + ) == CERT_SUCCESS + + check cert_signing_req( + "my.big.domain.string.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".cstring, + # 254 characters, too long + certKey, + derCSR.addr, + ) == -48 # CERT_ERROR_CN_TOO_LONG + + check cert_signing_req( + "my.big.domain.string.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".cstring, + # 64 character label, too long + certKey, + derCSR.addr, + ) == -49 # CERT_ERROR_CN_LABEL_TOO_LONG + + check cert_signing_req( + "my..empty.label.domain".cstring, # domain with empty label + certKey, + derCSR.addr, + ) == -50 # CERT_ERROR_CN_EMPTY_LABEL + + check cert_signing_req( + "".cstring, # domain with empty cn + certKey, + derCSR.addr, + ) == -51 # CERT_ERROR_CN_EMPTY + suite "utilities test": test "parseCertTime": var dt = parseCertTime("Mar 19 11:54:31 2025 GMT")