Skip to content

Commit

Permalink
refactor(plugin): Manual OpenSSL certificate verification steps
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jpfr committed Sep 22, 2024
1 parent e1c4286 commit 9ac78af
Showing 1 changed file with 195 additions and 174 deletions.
369 changes: 195 additions & 174 deletions plugins/crypto/openssl/ua_pki_openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down

0 comments on commit 9ac78af

Please sign in to comment.