From af1abb2e8355e238187a1e771059f26555466364 Mon Sep 17 00:00:00 2001 From: Julius Pfrommer Date: Sun, 22 Sep 2024 22:03:39 +0200 Subject: [PATCH 1/5] refactor(plugins): Better cleanup of the OpenSSL CertificateVerification plugin --- plugins/crypto/openssl/ua_pki_openssl.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/crypto/openssl/ua_pki_openssl.c b/plugins/crypto/openssl/ua_pki_openssl.c index 87b91e03c1e..c741efcf820 100644 --- a/plugins/crypto/openssl/ua_pki_openssl.c +++ b/plugins/crypto/openssl/ua_pki_openssl.c @@ -118,9 +118,7 @@ UA_CertificateVerification_clear (UA_CertificateVerification * cv) { UA_CertContext_sk_free (context); UA_free (context); - cv->context = NULL; - - return; + memset(cv, 0, sizeof(UA_CertificateVerification)); } static UA_StatusCode From ee79096b6570337543dcecfe9d0bcf4fb93234d4 Mon Sep 17 00:00:00 2001 From: Julius Pfrommer Date: Sun, 22 Sep 2024 22:04:14 +0200 Subject: [PATCH 2/5] fix(plugin): Always clean up the previously configured certificate verification plugin --- plugins/ua_config_default.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/ua_config_default.c b/plugins/ua_config_default.c index c4c8f4ffec7..8ab5f547f28 100644 --- a/plugins/ua_config_default.c +++ b/plugins/ua_config_default.c @@ -184,6 +184,8 @@ setDefaultConfig(UA_ServerConfig *conf) { /* Certificate Verification that accepts every certificate. Can be * overwritten when the policy is specialized. */ + if(conf->certificateVerification.clear) + conf->certificateVerification.clear(&conf->certificateVerification); UA_CertificateVerification_AcceptAll(&conf->certificateVerification); /* * Global Node Lifecycle * */ @@ -685,6 +687,8 @@ UA_ServerConfig_setDefaultWithSecurityPolicies(UA_ServerConfig *conf, return retval; } + if(conf->certificateVerification.clear) + conf->certificateVerification.clear(&conf->certificateVerification); retval = UA_CertificateVerification_Trustlist(&conf->certificateVerification, trustList, trustListSize, issuerList, issuerListSize, @@ -758,6 +762,8 @@ UA_ClientConfig_setDefault(UA_ClientConfig *config) { /* Certificate Verification that accepts every certificate. Can be * overwritten when the policy is specialized. */ + if(config->certificateVerification.clear) + config->certificateVerification.clear(&config->certificateVerification); UA_CertificateVerification_AcceptAll(&config->certificateVerification); UA_LOG_WARNING(&config->logger, UA_LOGCATEGORY_USERLAND, "AcceptAll Certificate Verification. " @@ -816,6 +822,8 @@ UA_ClientConfig_setDefaultEncryption(UA_ClientConfig *config, if(retval != UA_STATUSCODE_GOOD) return retval; + if(config->certificateVerification.clear) + config->certificateVerification.clear(&config->certificateVerification); retval = UA_CertificateVerification_Trustlist(&config->certificateVerification, trustList, trustListSize, NULL, 0, From 19ecf3e5627ae1d0c20ce661aee8e0164b03d86c Mon Sep 17 00:00:00 2001 From: Julius Pfrommer Date: Sun, 22 Sep 2024 22:11:45 +0200 Subject: [PATCH 3/5] refactor(plugin): Manual OpenSSL certificate verification steps The previous certificate verification relied on many OpenSSL internals and a forest of feature-flags. The new code is about the same size and makes the certificate verification steps explicit. The only downside is that we no longer auto-retrieve revocation lists if a distribution point is defined in a CA certificate. But revocation is handled by a GDS in OPC UA. And we don't want our certificate verification to do synchronous HTTP calls in the background. --- plugins/crypto/openssl/ua_pki_openssl.c | 369 +++++++++++++----------- 1 file changed, 195 insertions(+), 174 deletions(-) diff --git a/plugins/crypto/openssl/ua_pki_openssl.c b/plugins/crypto/openssl/ua_pki_openssl.c index c741efcf820..dbfc94e4bad 100644 --- a/plugins/crypto/openssl/ua_pki_openssl.c +++ b/plugins/crypto/openssl/ua_pki_openssl.c @@ -390,196 +390,217 @@ UA_ReloadCertFromFolder (CertContext * ctx) { #endif /* end of __linux__ */ -static UA_StatusCode -UA_X509_Store_CTX_Error_To_UAError (int opensslErr) { - UA_StatusCode ret; - - switch (opensslErr) { - case X509_V_ERR_CERT_HAS_EXPIRED: - case X509_V_ERR_CERT_NOT_YET_VALID: - case X509_V_ERR_CRL_NOT_YET_VALID: - case X509_V_ERR_CRL_HAS_EXPIRED: - case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD: - case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD: - case X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD: - case X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD: - ret = UA_STATUSCODE_BADCERTIFICATETIMEINVALID; - break; - case X509_V_ERR_CERT_REVOKED: - ret = UA_STATUSCODE_BADCERTIFICATEREVOKED; - break; - case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT: - case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: - case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: - ret = UA_STATUSCODE_BADCERTIFICATEUNTRUSTED; - break; - case X509_V_ERR_CERT_SIGNATURE_FAILURE: - case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN: - ret = UA_STATUSCODE_BADSECURITYCHECKSFAILED; - break; - case X509_V_ERR_UNABLE_TO_GET_CRL: - ret = UA_STATUSCODE_BADCERTIFICATEREVOCATIONUNKNOWN; - break; - default: - ret = UA_STATUSCODE_BADCERTIFICATEINVALID; - break; - } - return ret; - } - -static UA_StatusCode -UA_CertificateVerification_Verify (void * verificationContext, - const UA_ByteString * certificate) { - X509_STORE_CTX* storeCtx; - X509_STORE* store; - CertContext * ctx; - UA_StatusCode ret; - int opensslRet; - X509 * certificateX509 = NULL; +static const unsigned char openssl_PEM_PRE[28] = "-----BEGIN CERTIFICATE-----"; + +/* Extract the leaf certificate from a bytestring that may contain an entire chain */ +static X509 * +openSSLLoadLeafCertificate(UA_ByteString cert, size_t *offset) { + if(cert.length <= *offset) + return NULL; + cert.length -= *offset; + cert.data += *offset; + + /* Detect DER encoding. Extract the encoding length and cut. */ + if(cert.length >= 4 && cert.data[0] == 0x30 && cert.data[1] == 0x82) { + /* The certificate length is encoded after the magic bytes */ + size_t certLen = 4; /* Magic numbers + length bytes */ + certLen += (size_t)(((uint16_t)cert.data[2]) << 8); + certLen += cert.data[3]; + if(certLen > cert.length) + return NULL; + cert.length = certLen; + *offset += certLen; + const UA_Byte *dataPtr = cert.data; + return d2i_X509(NULL, &dataPtr, (long)cert.length); + } + + /* Assume PEM encoding. Detect multiple certificates and cut. */ + if(cert.length > 27 * 4) { + const unsigned char *match = + UA_Bstrstr(openssl_PEM_PRE, 27, &cert.data[27*2], cert.length - (27*2)); + if(match) + cert.length = (uintptr_t)(match - cert.data); + } + *offset += cert.length; + + BIO *bio = BIO_new_mem_buf((void *) cert.data, (int)cert.length); + X509 *result = PEM_read_bio_X509(bio, NULL, NULL, NULL); + BIO_free(bio); + return result; +} - if (verificationContext == NULL) { - return UA_STATUSCODE_BADINTERNALERROR; - } - ctx = (CertContext *) verificationContext; - - store = X509_STORE_new(); - storeCtx = X509_STORE_CTX_new(); - - if (store == NULL || storeCtx == NULL) { - ret = UA_STATUSCODE_BADOUTOFMEMORY; - goto cleanup; - } -#ifdef __linux__ - ret = UA_ReloadCertFromFolder (ctx); - if (ret != UA_STATUSCODE_GOOD) { - goto cleanup; - } -#endif +/* The bytestring might contain an entire certificate chain. The first + * stack-element is the leaf certificate itself. The remaining ones are + * potential issuer certificates. */ +static STACK_OF(X509) * +openSSLLoadCertificateStack(const UA_ByteString cert) { + size_t offset = 0; + X509 *x509 = NULL; + STACK_OF(X509) *result = sk_X509_new_null(); + if(!result) + return NULL; + while((x509 = openSSLLoadLeafCertificate(cert, &offset))) { + sk_X509_push(result, x509); + } + return result; +} - certificateX509 = UA_OpenSSL_LoadCertificate(certificate); - if (certificateX509 == NULL) { - ret = UA_STATUSCODE_BADCERTIFICATEINVALID; - goto cleanup; - } - - X509_STORE_set_flags(store, 0); - opensslRet = X509_STORE_CTX_init (storeCtx, store, certificateX509, - ctx->skIssue); - if (opensslRet != 1) { - ret = UA_STATUSCODE_BADINTERNALERROR; - goto cleanup; - } -#if defined(OPENSSL_API_COMPAT) && OPENSSL_API_COMPAT < 0x10100000L - (void) X509_STORE_CTX_trusted_stack (storeCtx, ctx->skTrusted); -#else - (void) X509_STORE_CTX_set0_trusted_stack (storeCtx, ctx->skTrusted); -#endif +/* Return the first matching issuer candidate AFTER prev */ +static X509 * +openSSLFindNextIssuer(CertContext *ctx, STACK_OF(X509) *stack, X509 *x509, X509 *prev) { + /* First check issuers from the stack - provided in the same bytestring as + * the certificate. This can also return x509 itself. */ + do { + int size = sk_X509_num(stack); + for(int i = 0; i < size; i++) { + X509 *candidate = sk_X509_value(stack, i); + if(X509_check_issued(candidate, x509) != 0) + continue; + if(prev == NULL) + return candidate; + prev = NULL; + } + /* Switch to search in the ctx->skIssue list */ + stack = (stack != ctx->skIssue) ? ctx->skIssue : NULL; + } while(stack); + return NULL; +} - /* Set crls to ctx */ - if (sk_X509_CRL_num (ctx->skCrls) > 0) { - X509_STORE_CTX_set0_crls (storeCtx, ctx->skCrls); +static UA_Boolean +openSSLCheckRevoked(CertContext *ctx, X509 *cert) { + const ASN1_INTEGER *sn = X509_get0_serialNumber(cert); + const X509_NAME *in = X509_get_issuer_name(cert); + int size = sk_X509_CRL_num(ctx->skCrls); + for(int i = 0; i < size; i++) { + /* The crl contains a list of serial numbers from the same issuer */ + X509_CRL *crl = sk_X509_CRL_value(ctx->skCrls, i); + if(X509_NAME_cmp(in, X509_CRL_get_issuer(crl)) != 0) + continue; + STACK_OF(X509_REVOKED) *rs = X509_CRL_get_REVOKED(crl); + int rsize = sk_X509_REVOKED_num(rs); + for(int j = 0; j < rsize; j++) { + X509_REVOKED *r = sk_X509_REVOKED_value(rs, j); + if(ASN1_INTEGER_cmp(sn, X509_REVOKED_get0_serialNumber(r)) == 0) + return true; + } } + return false; +} - /* Set flag to check if the certificate has an invalid signature */ - X509_STORE_CTX_set_flags (storeCtx, X509_V_FLAG_CHECK_SS_SIGNATURE); - - if (X509_check_issued(certificateX509,certificateX509) != X509_V_OK) { - X509_STORE_CTX_set_flags (storeCtx, X509_V_FLAG_CRL_CHECK); - } +#define UA_OPENSSL_MAX_CHAIN_LENGTH 10 - /* This condition will check whether the certificate is a User certificate or a CA certificate. - * If the KU_KEY_CERT_SIGN and KU_CRL_SIGN of key_usage are set, then the certificate shall be - * condidered as CA Certificate and cannot be used to establish a connection. Refer the test case - * CTT/Security/Security Certificate Validation/029.js for more details */ - /** \todo Can the ca-parameter of X509_check_purpose can be used? */ - if(X509_check_purpose(certificateX509, X509_PURPOSE_CRL_SIGN, 0) && X509_check_ca(certificateX509)) { - return UA_STATUSCODE_BADCERTIFICATEUSENOTALLOWED; - } +static UA_StatusCode +openSSL_verifyChain(CertContext *ctx, STACK_OF(X509) *stack, X509 **old_issuers, + X509 *x509, int depth) { + /* Maxiumum chain length */ + if(depth == UA_OPENSSL_MAX_CHAIN_LENGTH) + return UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE; + + /* Verification Step: Validity Period */ + ASN1_TIME *notBefore = X509_get_notBefore(x509); + ASN1_TIME *notAfter = X509_get_notAfter(x509); + if(X509_cmp_current_time(notBefore) != -1 || X509_cmp_current_time(notAfter) != 1) + return UA_STATUSCODE_BADCERTIFICATETIMEINVALID; + + /* Verification Step: Revocation Check */ + if(openSSLCheckRevoked(ctx, x509)) + return UA_STATUSCODE_BADCERTIFICATEREVOKED; + + /* Is the certificate in the trust list? If yes, then we are done. */ + for(int i = 0; i < sk_X509_num(ctx->skTrusted); i++) { + if(X509_cmp(x509, sk_X509_value(ctx->skTrusted, i)) == 0) + return UA_STATUSCODE_GOOD; + } + + /* Return the most specific error code. BADCERTIFICATECHAININCOMPLETE is + * returned only if all possible chains are incomplete. */ + X509 *issuer = NULL; + UA_StatusCode ret = UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE; + while(ret != UA_STATUSCODE_GOOD) { + /* Find the issuer. We jump back here to find a different path if a + * subsequent check fails. */ + issuer = openSSLFindNextIssuer(ctx, stack, x509, issuer); + if(!issuer) + break; - opensslRet = X509_verify_cert (storeCtx); - if (opensslRet == 1) { - ret = UA_STATUSCODE_GOOD; + /* Detect (endless) loops of issuers */ + for(int i = 0; i < depth; i++) { + if(old_issuers[i] == issuer) + return UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE; + } + old_issuers[depth] = issuer; + + /* Verification Step: Signature */ + int opensslRet = X509_verify(x509, X509_get0_pubkey(issuer)); + if(opensslRet == -1) { + return UA_STATUSCODE_BADCERTIFICATEINVALID; /* Ill-formed signature */ + } else if(opensslRet == 0) { + ret = UA_STATUSCODE_BADCERTIFICATEINVALID; /* Wrong issuer, try again */ + continue; + } - /* Check if the not trusted certificate has a CRL file. If there is no CRL file available for the corresponding - * parent certificate then return status code UA_STATUSCODE_BADCERTIFICATEISSUERREVOCATIONUNKNOWN. Refer the test - * case CTT/Security/Security Certificate Validation/002.js */ - if (X509_check_issued(certificateX509,certificateX509) != X509_V_OK) { - /* Free X509_STORE_CTX and reuse it for certification verification */ - if (storeCtx != NULL) { - X509_STORE_CTX_free(storeCtx); - } + /* We have found the issuer certificate used for the signature. Recurse + * to the next certificate in the chain (verify the current issuer). */ + ret = openSSL_verifyChain(ctx, stack, old_issuers, issuer, depth + 1); - /* Initialised X509_STORE_CTX sructure*/ - storeCtx = X509_STORE_CTX_new(); + /* Problems where x509 != leaf are reported as "untrusted" without the + * detailed reason */ + if(ret != UA_STATUSCODE_GOOD && + ret != UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE) + ret = UA_STATUSCODE_BADCERTIFICATEUNTRUSTED; + } - /* Sets up X509_STORE_CTX structure for a subsequent verification operation */ - X509_STORE_set_flags(store, 0); - X509_STORE_CTX_init (storeCtx, store, certificateX509,ctx->skIssue); + return ret; +} - /* Set trust list to ctx */ - (void) X509_STORE_CTX_trusted_stack (storeCtx, ctx->skTrusted); +/* This follows Part 6, 6.1.3 Determining if a Certificate is trusted. + * It defines a sequence of steps for certificate verification. */ +static UA_StatusCode +UA_CertificateVerification_Verify(void *verificationContext, + const UA_ByteString *certificate) { + if(!verificationContext || !certificate) + return UA_STATUSCODE_BADINTERNALERROR; - /* Set crls to ctx */ - X509_STORE_CTX_set0_crls (storeCtx, ctx->skCrls); + UA_StatusCode ret = UA_STATUSCODE_GOOD; + CertContext *ctx = (CertContext *)verificationContext; - /* Set flags for CRL check */ - X509_STORE_CTX_set_flags (storeCtx, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL); +#ifdef __linux__ + ret = UA_ReloadCertFromFolder(ctx); + if(ret != UA_STATUSCODE_GOOD) + return ret; +#endif - opensslRet = X509_verify_cert (storeCtx); - if (opensslRet != 1) { - opensslRet = X509_STORE_CTX_get_error (storeCtx); - if (opensslRet == X509_V_ERR_UNABLE_TO_GET_CRL) { - ret = UA_STATUSCODE_BADCERTIFICATEISSUERREVOCATIONUNKNOWN; - } else { - ret = UA_X509_Store_CTX_Error_To_UAError (opensslRet); - } - } - } + /* Verification Step: Certificate Structure */ + STACK_OF(X509) *stack = openSSLLoadCertificateStack(*certificate); + if(!stack || sk_X509_num(stack) < 1) { + if(stack) + sk_X509_pop_free(stack, X509_free); + return UA_STATUSCODE_BADCERTIFICATEINVALID; + } + + /* Verification Step: Certificate Usage + * Check whether the certificate is a User certificate or a CA certificate. + * If the KU_KEY_CERT_SIGN and KU_CRL_SIGN of key_usage are set, then the + * certificate shall be condidered as CA Certificate and cannot be used to + * establish a connection. Refer the test case CTT/Security/Security + * Certificate Validation/029.js for more details */ + X509 *leaf = sk_X509_value(stack, 0); + if(X509_check_purpose(leaf, X509_PURPOSE_CRL_SIGN, 0) && X509_check_ca(leaf)) { + sk_X509_pop_free(stack, X509_free); + return UA_STATUSCODE_BADCERTIFICATEUSENOTALLOWED; } - else { - opensslRet = X509_STORE_CTX_get_error (storeCtx); - - /* Check the issued certificate of a CA that is not trusted but available */ - if(opensslRet == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN){ - int trusted_cert_len = sk_X509_num(ctx->skTrusted); - int cmpVal; - X509 *trusted_cert; - const ASN1_OCTET_STRING *trusted_cert_keyid; - const ASN1_OCTET_STRING *remote_cert_keyid; - - for (int i = 0; i < trusted_cert_len; i++) { - trusted_cert = sk_X509_value(ctx->skTrusted, i); - - /* Fetch the Subject key identifier of the certificate in trust list */ - trusted_cert_keyid = X509_get0_subject_key_id(trusted_cert); - - /* Fetch the Subject key identifier of the remote certificate */ - remote_cert_keyid = X509_get0_subject_key_id(certificateX509); - - if(trusted_cert_keyid && remote_cert_keyid) { - /* Check remote certificate is present in the trust list */ - cmpVal = ASN1_OCTET_STRING_cmp(trusted_cert_keyid, remote_cert_keyid); - if(cmpVal == 0) { - ret = UA_STATUSCODE_GOOD; - goto cleanup; - } - } - } - } - /* Return expected OPCUA error code */ - ret = UA_X509_Store_CTX_Error_To_UAError (opensslRet); - } -cleanup: - if (store != NULL) { - X509_STORE_free (store); - } - if (storeCtx != NULL) { - X509_STORE_CTX_free (storeCtx); - } - if (certificateX509 != NULL) { - X509_free (certificateX509); - } + /* These steps are performed outside of this method. + * Because we need the server or client context. + * - Security Policy + * - Host Name + * - URI */ + + /* Verification Step: Build Certificate Chain + * We perform the checks for each certificate inside. */ + X509 *old_issuers[UA_OPENSSL_MAX_CHAIN_LENGTH]; + ret = openSSL_verifyChain(ctx, stack, old_issuers, leaf, 0); + sk_X509_pop_free(stack, X509_free); return ret; } From 01086d6de8091351dbc768b731e968976baa7e23 Mon Sep 17 00:00:00 2001 From: Julius Pfrommer Date: Tue, 24 Sep 2024 20:46:26 +0200 Subject: [PATCH 4/5] refactor(plugins): Refactor mbedTLS certificate verification to include intermediate steps --- plugins/crypto/mbedtls/ua_pki_mbedtls.c | 410 +++++++++++------------- 1 file changed, 180 insertions(+), 230 deletions(-) diff --git a/plugins/crypto/mbedtls/ua_pki_mbedtls.c b/plugins/crypto/mbedtls/ua_pki_mbedtls.c index b5cb09c6321..ac3bf38814e 100644 --- a/plugins/crypto/mbedtls/ua_pki_mbedtls.c +++ b/plugins/crypto/mbedtls/ua_pki_mbedtls.c @@ -3,7 +3,7 @@ * * Copyright 2018 (c) Mark Giraud, Fraunhofer IOSB * Copyright 2019 (c) Kalycito Infotech Private Limited - * Copyright 2019 (c) Julius Pfrommer, Fraunhofer IOSB + * Copyright 2019, 2024 (c) Julius Pfrommer, Fraunhofer IOSB */ #include @@ -16,11 +16,7 @@ #include #include #include - -#define REMOTECERTIFICATETRUSTED 1 -#define ISSUERKNOWN 2 -#define DUALPARENT 3 -#define PARENTFOUND 4 +#include /* Find binary substring. Taken and adjusted from * http://tungchingkai.blogspot.com/2011/07/binary-strstr.html */ @@ -236,251 +232,204 @@ reloadCertificates(CertInfo *ci) { #endif -static UA_StatusCode -certificateVerification_verify(void *verificationContext, - const UA_ByteString *certificate) { - CertInfo *ci = (CertInfo*)verificationContext; - if(!ci) - return UA_STATUSCODE_BADINTERNALERROR; - -#ifdef __linux__ /* Reload certificates if folder paths are specified */ - UA_StatusCode certFlag = reloadCertificates(ci); - if(certFlag != UA_STATUSCODE_GOOD) { - return certFlag; - } +/* We need to access some private fields below */ +#ifndef MBEDTLS_PRIVATE +#define MBEDTLS_PRIVATE(x) x #endif - if(ci->trustListFolder.length == 0 && - ci->issuerListFolder.length == 0 && - ci->revocationListFolder.length == 0 && - ci->certificateTrustList.raw.len == 0 && - ci->certificateIssuerList.raw.len == 0 && - ci->certificateRevocationList.raw.len == 0) { - UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, - "PKI plugin unconfigured. Accepting the certificate."); - return UA_STATUSCODE_GOOD; - } - - /* Parse the certificate */ - mbedtls_x509_crt remoteCertificate; - - /* Temporary Object to parse the trustList */ - mbedtls_x509_crt *tempCert = NULL; - - /* Temporary Object to parse the revocationList */ - mbedtls_x509_crl *tempCrl = NULL; - - /* Temporary Object to identify the parent CA when there is no intermediate CA */ - mbedtls_x509_crt *parentCert = NULL; - - /* Temporary Object to identify the parent CA when there is intermediate CA */ - mbedtls_x509_crt *parentCert_2 = NULL; - - /* Flag value to identify if the issuer certificate is found */ - int issuerKnown = 0; - - /* Flag value to identify if the parent certificate found */ - int parentFound = 0; - - mbedtls_x509_crt_init(&remoteCertificate); - int mbedErr = mbedtls_x509_crt_parse(&remoteCertificate, certificate->data, - certificate->length); - if(mbedErr) { - /* char errBuff[300]; */ - /* mbedtls_strerror(mbedErr, errBuff, 300); */ - /* UA_LOG_WARNING(data->policyContext->securityPolicy->logger, UA_LOGCATEGORY_SECURITYPOLICY, */ - /* "Could not parse the remote certificate with error: %s", errBuff); */ - return UA_STATUSCODE_BADSECURITYCHECKSFAILED; - } - - /* Verify */ - mbedtls_x509_crt_profile crtProfile = { - MBEDTLS_X509_ID_FLAG(MBEDTLS_MD_SHA1) | MBEDTLS_X509_ID_FLAG(MBEDTLS_MD_SHA256), - 0xFFFFFF, 0x000000, 128 * 8 // in bits - }; // TODO: remove magic numbers - - uint32_t flags = 0; - mbedErr = mbedtls_x509_crt_verify_with_profile(&remoteCertificate, - &ci->certificateTrustList, - &ci->certificateRevocationList, - &crtProfile, NULL, &flags, NULL, NULL); - - /* Flag to check if the remote certificate is trusted or not */ - int TRUSTED = 0; - - /* Check if the remoteCertificate is present in the trustList while mbedErr value is not zero */ - if(mbedErr && !(flags & MBEDTLS_X509_BADCERT_EXPIRED) && !(flags & MBEDTLS_X509_BADCERT_FUTURE)) { - for(tempCert = &ci->certificateTrustList; tempCert != NULL; tempCert = tempCert->next) { - if(remoteCertificate.raw.len == tempCert->raw.len && - memcmp(remoteCertificate.raw.p, tempCert->raw.p, remoteCertificate.raw.len) == 0) { - TRUSTED = REMOTECERTIFICATETRUSTED; - break; - } +/* Return the first matching issuer candidate AFTER prev */ +static mbedtls_x509_crt * +mbedtlsFindNextIssuer(CertInfo *ci, mbedtls_x509_crt *stack, + mbedtls_x509_crt *cert, mbedtls_x509_crt *prev) { + char inbuf[512], snbuf[512]; + if(mbedtls_x509_dn_gets(inbuf, 512, &cert->issuer) < 0) + return NULL; + do { + for(mbedtls_x509_crt *i = stack; i; i = i->next) { + /* Compare issuer name and subject name */ + if(mbedtls_x509_dn_gets(snbuf, 512, &i->subject) < 0) + continue; + if(strncmp(inbuf, snbuf, 512) != 0) + continue; + /* Skip when the key does not match the signature */ + if(!mbedtls_pk_can_do(&i->pk, cert->MBEDTLS_PRIVATE(sig_pk))) + continue; + if(prev == NULL) + return i; + prev = NULL; /* This was the last issuer we tried to verify */ } + /* Switch from the stack that came with the cert to the ctx->skIssue list */ + stack = (stack != &ci->certificateIssuerList) ? &ci->certificateIssuerList : NULL; + } while(stack); + return NULL; +} + +static UA_Boolean +mbedtlsCheckRevoked(CertInfo *ci, mbedtls_x509_crt *cert) { + char inbuf[512], inbuf2[512]; + if(mbedtls_x509_dn_gets(inbuf, 512, &cert->issuer) < 0) + return true; + for(mbedtls_x509_crl *crl = &ci->certificateRevocationList; crl; crl = crl->next) { + /* Is the CRL for the issuer of the certificate? */ + if(mbedtls_x509_dn_gets(inbuf2, 512, &crl->issuer) < 0) + return true; + if(strncmp(inbuf, inbuf2, 512) != 0) + continue; + /* Is the serial number of the certificate contained in the CRL? */ + if(mbedtls_x509_crt_is_revoked(cert, crl) != 0) + return true; } + return false; +} - /* If the remote certificate is present in the trustList then check if the issuer certificate - * of remoteCertificate is present in issuerList */ - if(TRUSTED && mbedErr) { - mbedErr = mbedtls_x509_crt_verify_with_profile(&remoteCertificate, - &ci->certificateIssuerList, - &ci->certificateRevocationList, - &crtProfile, NULL, &flags, NULL, NULL); - - /* Check if the parent certificate has a CRL file available */ - if(!mbedErr) { - /* Flag value to identify if that there is an intermediate CA present */ - int dualParent = 0; - - /* Identify the topmost parent certificate for the remoteCertificate */ - for(parentCert = &ci->certificateIssuerList; parentCert != NULL; parentCert = parentCert->next ) { - if(memcmp(remoteCertificate.issuer_raw.p, parentCert->subject_raw.p, parentCert->subject_raw.len) == 0) { - for(parentCert_2 = &ci->certificateTrustList; parentCert_2 != NULL; parentCert_2 = parentCert_2->next) { - if(memcmp(parentCert->issuer_raw.p, parentCert_2->subject_raw.p, parentCert_2->subject_raw.len) == 0) { - dualParent = DUALPARENT; - break; - } - } - parentFound = PARENTFOUND; - } - - if(parentFound == PARENTFOUND) - break; - } +/* Verify that the public key of the issuer was used to sign the certificate */ +static UA_Boolean +mbedtlsCheckSignature(const mbedtls_x509_crt *cert, mbedtls_x509_crt *issuer) { + size_t hash_len; + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md_type_t md = cert->MBEDTLS_PRIVATE(sig_md); +#if !defined(MBEDTLS_USE_PSA_CRYPTO) + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(md); + hash_len = mbedtls_md_get_size(md_info); + if(mbedtls_md(md_info, cert->tbs.p, cert->tbs.len, hash) != 0) + return false; +#else + if(psa_hash_compute(mbedtls_md_psa_alg_from_type(md), cert->tbs.p, + cert->tbs.len, hash, sizeof(hash), &hash_len) != PSA_SUCCESS) + return false; +#endif + const mbedtls_x509_buf *sig = &cert->MBEDTLS_PRIVATE(sig); + void *sig_opts = cert->MBEDTLS_PRIVATE(sig_opts); + mbedtls_pk_type_t pktype = cert->MBEDTLS_PRIVATE(sig_pk); + return (mbedtls_pk_verify_ext(pktype, sig_opts, &issuer->pk, md, + hash, hash_len, sig->p, sig->len) == 0); +} - /* Check if there is an intermediate certificate between the topmost parent - * certificate and child certificate - * If yes the topmost parent certificate is to be checked whether it has a - * CRL file avaiable */ - if(dualParent == DUALPARENT && parentFound == PARENTFOUND) { - parentCert = parentCert_2; - } +#define UA_MBEDTLS_MAX_CHAIN_LENGTH 10 - /* If a parent certificate is found traverse the revocationList and identify - * if there is any CRL file that corresponds to the parentCertificate */ - if(parentFound == PARENTFOUND) { - tempCrl = &ci->certificateRevocationList; - while(tempCrl != NULL) { - if(tempCrl->version != 0 && - tempCrl->issuer_raw.len == parentCert->subject_raw.len && - memcmp(tempCrl->issuer_raw.p, - parentCert->subject_raw.p, - tempCrl->issuer_raw.len) == 0) { - issuerKnown = ISSUERKNOWN; - break; - } - - tempCrl = tempCrl->next; - } - - /* If the CRL file corresponding to the parent certificate is not present - * then return UA_STATUSCODE_BADCERTIFICATEISSUERREVOCATIONUNKNOWN */ - if(issuerKnown) { - flags = 0; - mbedErr = mbedtls_x509_crt_verify_with_profile(parentCert, - &ci->certificateIssuerList, - &ci->certificateRevocationList, - &crtProfile, NULL, &flags, NULL, NULL); - } else { - return UA_STATUSCODE_BADCERTIFICATEISSUERREVOCATIONUNKNOWN; - } +static UA_StatusCode +mbedtlsVerifyChain(CertInfo *ci, mbedtls_x509_crt *stack, mbedtls_x509_crt **old_issuers, + mbedtls_x509_crt *cert, int depth) { + /* Maxiumum chain length */ + if(depth == UA_MBEDTLS_MAX_CHAIN_LENGTH) + return UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE; + + /* Verification Step: Validity Period */ + if(mbedtls_x509_time_is_future(&cert->valid_from) || + mbedtls_x509_time_is_past(&cert->valid_to)) + return (depth == 0) ? UA_STATUSCODE_BADCERTIFICATETIMEINVALID : + UA_STATUSCODE_BADCERTIFICATEISSUERTIMEINVALID; + + /* Verification Step: Revocation Check */ + if(mbedtlsCheckRevoked(ci, cert)) + return (depth == 0) ? UA_STATUSCODE_BADCERTIFICATEREVOKED : + UA_STATUSCODE_BADCERTIFICATEISSUERREVOKED; + + /* Return the most specific error code. BADCERTIFICATECHAININCOMPLETE is + * returned only if all possible chains are incomplete. */ + mbedtls_x509_crt *issuer = NULL; + UA_StatusCode ret = UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE; + while(ret != UA_STATUSCODE_GOOD) { + /* Find the issuer. This can return the same certificate if it is + * self-signed (subject == issuer). We come back here to try a different + * "path" if a subsequent verification fails. */ + issuer = mbedtlsFindNextIssuer(ci, stack, cert, issuer); + if(!issuer) + break; + + /* Verification Step: Signature */ + if(!mbedtlsCheckSignature(cert, issuer)) { + ret = UA_STATUSCODE_BADCERTIFICATEINVALID; /* Wrong issuer, try again */ + continue; + } - } + /* The certificate is self-signed. We have arrived at the top of the + * chain. We check whether the certificate is trusted below. This is the + * only place where we return UA_STATUSCODE_BADCERTIFICATEUNTRUSTED. + * This sinals that the chain is complete (but can be still + * untrusted). */ + if(issuer == cert || (cert->tbs.len == issuer->tbs.len && + memcmp(cert->tbs.p, issuer->tbs.p, cert->tbs.len) == 0)) { + ret = UA_STATUSCODE_BADCERTIFICATEUNTRUSTED; + continue; + } + /* Detect (endless) loops of issuers. The last one can be skipped by the + * check for self-signed just before. */ + for(int i = 0; i < depth - 1; i++) { + if(old_issuers[i] == issuer) + return UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE; } + old_issuers[depth] = issuer; + /* We have found the issuer certificate used for the signature. Recurse + * to the next certificate in the chain (verify the current issuer). */ + ret = mbedtlsVerifyChain(ci, stack, old_issuers, issuer, depth + 1); } - else if(!mbedErr && !TRUSTED) { - /* This else if section is to identify if the parent certificate which is present in trustList - * has CRL file corresponding to it */ - - /* Identify the parent certificate of the remoteCertificate */ - for(parentCert = &ci->certificateTrustList; parentCert != NULL; parentCert = parentCert->next) { - if(memcmp(remoteCertificate.issuer_raw.p, parentCert->subject_raw.p, parentCert->subject_raw.len) == 0) { - parentFound = PARENTFOUND; - break; - } + /* The chain is complete, but we haven't yet identified a trusted + * certificate "on the way down". Can we trust this certificate? */ + if(ret == UA_STATUSCODE_BADCERTIFICATEUNTRUSTED) { + for(mbedtls_x509_crt *t = &ci->certificateTrustList; t; t = t->next) { + if(cert->tbs.len == t->tbs.len && + memcmp(cert->tbs.p, t->tbs.p, cert->tbs.len) == 0) + return UA_STATUSCODE_GOOD; } + } - /* If the parent certificate is found traverse the revocationList and identify - * if there is any CRL file that corresponds to the parentCertificate */ - if(parentFound == PARENTFOUND && - memcmp(remoteCertificate.issuer_raw.p, remoteCertificate.subject_raw.p, remoteCertificate.subject_raw.len) != 0) { - tempCrl = &ci->certificateRevocationList; - while(tempCrl != NULL) { - if(tempCrl->version != 0 && - tempCrl->issuer_raw.len == parentCert->subject_raw.len && - memcmp(tempCrl->issuer_raw.p, - parentCert->subject_raw.p, - tempCrl->issuer_raw.len) == 0) { - issuerKnown = ISSUERKNOWN; - break; - } - - tempCrl = tempCrl->next; - } - - /* If the CRL file corresponding to the parent certificate is not present - * then return UA_STATUSCODE_BADCERTIFICATEREVOCATIONUNKNOWN */ - if(issuerKnown) { - flags = 0; - mbedErr = mbedtls_x509_crt_verify_with_profile(parentCert, - &ci->certificateIssuerList, - &ci->certificateRevocationList, - &crtProfile, NULL, &flags, NULL, NULL); - } else { - return UA_STATUSCODE_BADCERTIFICATEISSUERREVOCATIONUNKNOWN; - } + return ret; +} - } +/* This follows Part 6, 6.1.3 Determining if a Certificate is trusted. + * It defines a sequence of steps for certificate verification. */ +static UA_StatusCode +certificateVerification_verify(void *verificationContext, + const UA_ByteString *certificate) { + if(!verificationContext || !certificate) + return UA_STATUSCODE_BADINTERNALERROR; - } + UA_StatusCode ret = UA_STATUSCODE_GOOD; + CertInfo *ci = (CertInfo*)verificationContext; - // TODO: Extend verification - - /* This condition will check whether the certificate is a User certificate - * or a CA certificate. If the MBEDTLS_X509_KU_KEY_CERT_SIGN and - * MBEDTLS_X509_KU_CRL_SIGN of key_usage are set, then the certificate - * shall be condidered as CA Certificate and cannot be used to establish a - * connection. Refer the test case CTT/Security/Security Certificate Validation/029.js - * for more details */ -#if MBEDTLS_VERSION_NUMBER >= 0x02060000 && MBEDTLS_VERSION_NUMBER < 0x03000000 - if((remoteCertificate.key_usage & MBEDTLS_X509_KU_KEY_CERT_SIGN) && - (remoteCertificate.key_usage & MBEDTLS_X509_KU_CRL_SIGN)) { - return UA_STATUSCODE_BADCERTIFICATEUSENOTALLOWED; - } -#else - if((remoteCertificate.private_key_usage & MBEDTLS_X509_KU_KEY_CERT_SIGN) && - (remoteCertificate.private_key_usage & MBEDTLS_X509_KU_CRL_SIGN)) { - return UA_STATUSCODE_BADCERTIFICATEUSENOTALLOWED; - } +#ifdef __linux__ /* Reload certificates if folder paths are specified */ + ret = reloadCertificates(ci); + if(ret != UA_STATUSCODE_GOOD) + return ret; #endif - - UA_StatusCode retval = UA_STATUSCODE_GOOD; - if(mbedErr) { -#if UA_LOGLEVEL <= 400 - char buff[100]; - int len = mbedtls_x509_crt_verify_info(buff, 100, "", flags); - UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_SECURITYPOLICY, - "Verifying the certificate failed with error: %.*s", len-1, buff); -#endif - if(flags & (uint32_t)MBEDTLS_X509_BADCERT_NOT_TRUSTED) { - retval = UA_STATUSCODE_BADCERTIFICATEUNTRUSTED; - } else if(flags & (uint32_t)MBEDTLS_X509_BADCERT_FUTURE || - flags & (uint32_t)MBEDTLS_X509_BADCERT_EXPIRED) { - retval = UA_STATUSCODE_BADCERTIFICATETIMEINVALID; - } else if(flags & (uint32_t)MBEDTLS_X509_BADCERT_REVOKED || - flags & (uint32_t)MBEDTLS_X509_BADCRL_EXPIRED) { - retval = UA_STATUSCODE_BADCERTIFICATEREVOKED; - } else { - retval = UA_STATUSCODE_BADSECURITYCHECKSFAILED; - } + /* Verification Step: Certificate Structure + * This parses the entire certificate chain contained in the bytestring. */ + mbedtls_x509_crt cert; + mbedtls_x509_crt_init(&cert); + int mbedErr = mbedtls_x509_crt_parse(&cert, certificate->data, + certificate->length); + if(mbedErr) + return UA_STATUSCODE_BADCERTIFICATEINVALID; + + /* Verification Step: Certificate Usage + * Check whether the certificate is a User certificate or a CA certificate. + * If the KU_KEY_CERT_SIGN and KU_CRL_SIGN of key_usage are set, then the + * certificate shall be condidered as CA Certificate and cannot be used to + * establish a connection. Refer the test case CTT/Security/Security + * Certificate Validation/029.js for more details */ + unsigned int ca_flags = MBEDTLS_X509_KU_KEY_CERT_SIGN | MBEDTLS_X509_KU_CRL_SIGN; + if(mbedtls_x509_crt_check_key_usage(&cert, ca_flags)) { + mbedtls_x509_crt_free(&cert); + return UA_STATUSCODE_BADCERTIFICATEUSENOTALLOWED; } - mbedtls_x509_crt_free(&remoteCertificate); - return retval; + /* These steps are performed outside of this method. + * Because we need the server or client context. + * - Security Policy + * - Host Name + * - URI */ + + /* Verification Step: Build Certificate Chain + * We perform the checks for each certificate inside. */ + mbedtls_x509_crt *old_issuers[UA_MBEDTLS_MAX_CHAIN_LENGTH]; + ret = mbedtlsVerifyChain(ci, &cert, old_issuers, &cert, 0); + mbedtls_x509_crt_free(&cert); + return ret; } static UA_StatusCode @@ -619,4 +568,5 @@ UA_CertificateVerification_CertFolders(UA_CertificateVerification *cv, } #endif -#endif + +#endif /* UA_ENABLE_ENCRYPTION_MBEDTLS */ From c63520c407632b4e0c2a4d5520e193cb61a28f9a Mon Sep 17 00:00:00 2001 From: Julius Pfrommer Date: Fri, 27 Sep 2024 20:46:20 +0200 Subject: [PATCH 5/5] refactor(plugins): Use the same certificate verification logic for mbedTLS and OpenSSL Walk the tree to find a complete chain of unrevoked and current certificates. The chain must end in a self-signed certificate. Then, on the "way down", check that at least one certificate in the chain is trusted. --- plugins/crypto/openssl/ua_pki_openssl.c | 61 ++++++++++++++----------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/plugins/crypto/openssl/ua_pki_openssl.c b/plugins/crypto/openssl/ua_pki_openssl.c index dbfc94e4bad..3c05aaa2777 100644 --- a/plugins/crypto/openssl/ua_pki_openssl.c +++ b/plugins/crypto/openssl/ua_pki_openssl.c @@ -491,26 +491,22 @@ openSSLCheckRevoked(CertContext *ctx, X509 *cert) { static UA_StatusCode openSSL_verifyChain(CertContext *ctx, STACK_OF(X509) *stack, X509 **old_issuers, - X509 *x509, int depth) { + X509 *cert, int depth) { /* Maxiumum chain length */ if(depth == UA_OPENSSL_MAX_CHAIN_LENGTH) return UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE; /* Verification Step: Validity Period */ - ASN1_TIME *notBefore = X509_get_notBefore(x509); - ASN1_TIME *notAfter = X509_get_notAfter(x509); + ASN1_TIME *notBefore = X509_get_notBefore(cert); + ASN1_TIME *notAfter = X509_get_notAfter(cert); if(X509_cmp_current_time(notBefore) != -1 || X509_cmp_current_time(notAfter) != 1) - return UA_STATUSCODE_BADCERTIFICATETIMEINVALID; + return (depth == 0) ? UA_STATUSCODE_BADCERTIFICATETIMEINVALID : + UA_STATUSCODE_BADCERTIFICATEISSUERTIMEINVALID; /* Verification Step: Revocation Check */ - if(openSSLCheckRevoked(ctx, x509)) - return UA_STATUSCODE_BADCERTIFICATEREVOKED; - - /* Is the certificate in the trust list? If yes, then we are done. */ - for(int i = 0; i < sk_X509_num(ctx->skTrusted); i++) { - if(X509_cmp(x509, sk_X509_value(ctx->skTrusted, i)) == 0) - return UA_STATUSCODE_GOOD; - } + if(openSSLCheckRevoked(ctx, cert)) + return (depth == 0) ? UA_STATUSCODE_BADCERTIFICATEREVOKED : + UA_STATUSCODE_BADCERTIFICATEISSUERREVOKED; /* Return the most specific error code. BADCERTIFICATECHAININCOMPLETE is * returned only if all possible chains are incomplete. */ @@ -519,19 +515,12 @@ openSSL_verifyChain(CertContext *ctx, STACK_OF(X509) *stack, X509 **old_issuers, while(ret != UA_STATUSCODE_GOOD) { /* Find the issuer. We jump back here to find a different path if a * subsequent check fails. */ - issuer = openSSLFindNextIssuer(ctx, stack, x509, issuer); + issuer = openSSLFindNextIssuer(ctx, stack, cert, issuer); if(!issuer) break; - /* Detect (endless) loops of issuers */ - for(int i = 0; i < depth; i++) { - if(old_issuers[i] == issuer) - return UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE; - } - old_issuers[depth] = issuer; - /* Verification Step: Signature */ - int opensslRet = X509_verify(x509, X509_get0_pubkey(issuer)); + int opensslRet = X509_verify(cert, X509_get0_pubkey(issuer)); if(opensslRet == -1) { return UA_STATUSCODE_BADCERTIFICATEINVALID; /* Ill-formed signature */ } else if(opensslRet == 0) { @@ -539,15 +528,35 @@ openSSL_verifyChain(CertContext *ctx, STACK_OF(X509) *stack, X509 **old_issuers, continue; } + /* The certificate is self-signed. We have arrived at the top of the + * chain. We check whether the certificate is trusted below. This is the + * only place where we return UA_STATUSCODE_BADCERTIFICATEUNTRUSTED. + * This sinals that the chain is complete (but can be still + * untrusted). */ + if(cert == issuer || X509_cmp(cert, issuer) == 0) { + ret = UA_STATUSCODE_BADCERTIFICATEUNTRUSTED; + continue; + } + + /* Detect (endless) loops of issuers. The last one can be skipped by the + * check for self-signed just before. */ + for(int i = 0; i < depth; i++) { + if(old_issuers[i] == issuer) + return UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE; + } + old_issuers[depth] = issuer; + /* We have found the issuer certificate used for the signature. Recurse * to the next certificate in the chain (verify the current issuer). */ ret = openSSL_verifyChain(ctx, stack, old_issuers, issuer, depth + 1); + } - /* Problems where x509 != leaf are reported as "untrusted" without the - * detailed reason */ - if(ret != UA_STATUSCODE_GOOD && - ret != UA_STATUSCODE_BADCERTIFICATECHAININCOMPLETE) - ret = UA_STATUSCODE_BADCERTIFICATEUNTRUSTED; + /* Is the certificate in the trust list? If yes, then we are done. */ + if(ret == UA_STATUSCODE_BADCERTIFICATEUNTRUSTED) { + for(int i = 0; i < sk_X509_num(ctx->skTrusted); i++) { + if(X509_cmp(cert, sk_X509_value(ctx->skTrusted, i)) == 0) + return UA_STATUSCODE_GOOD; + } } return ret;