Files
open62541/plugins/crypto/openssl/create_certificate.c

425 lines
15 KiB
C

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Copyright 2021 (c) Christian von Arnim, ISW University of Stuttgart (for VDW and umati)
* Copyright 2022 (c) Wind River Systems, Inc.
*
*/
#include <open62541/plugin/create_certificate.h>
#include "securitypolicy_common.h"
#if defined(UA_ENABLE_ENCRYPTION_OPENSSL) || defined(UA_ENABLE_ENCRYPTION_LIBRESSL)
#include <openssl/pem.h>
#include <openssl/x509v3.h>
#include <openssl/err.h>
/**
* Join an array of UA_String to a single NULL-Terminated UA_String
* separated by character sep
*/
static UA_StatusCode
join_string_with_sep(const UA_String *strings, size_t stringsSize,
char sep, UA_String *out) {
if(!out)
return UA_STATUSCODE_BADINVALIDARGUMENT;
UA_String_clear(out);
size_t totalSize = stringsSize;
for(size_t iStr = 0; iStr < stringsSize; ++iStr) {
totalSize += strings[iStr].length;
}
UA_ByteString_allocBuffer(out, totalSize);
if(!out->data) {
return UA_STATUSCODE_BADOUTOFMEMORY;
}
size_t pos = 0;
for(size_t iStr = 0; iStr < stringsSize; ++iStr) {
memcpy(&out->data[pos], strings[iStr].data, strings[iStr].length);
pos += strings[iStr].length;
out->data[pos] = (UA_Byte) sep;
++pos;
}
out->data[out->length-1] = 0;
return UA_STATUSCODE_GOOD;
}
/**
* Search for a character in a string (like strchr).
* \todo Handle UTF-8
*
* \return index of the character or -1 on case of an error.
*/
static UA_Int32
UA_String_chr(const UA_String *pUaStr, char needl) {
UA_Byte byteNeedl = (UA_Byte)needl;
for(size_t i = 0; (size_t)i < pUaStr->length; ++i) {
if(pUaStr->data[i] == byteNeedl) {
return (UA_Int32) i;
}
}
return -1;
}
/* char *value cannot be const due to openssl 1.0 compatibility */
static UA_StatusCode
add_x509V3ext(const UA_Logger *logger, X509 *x509, int nid, char *value) {
X509_EXTENSION *ex;
X509V3_CTX ctx;
X509V3_set_ctx_nodb(&ctx);
X509V3_set_ctx(&ctx, x509, x509, NULL, NULL, 0);
ex = X509V3_EXT_conf_nid(NULL, &ctx, nid, value);
if(!ex)
{
#if UA_LOGLEVEL <= 300
const char * file = NULL;
int line = 0;
const char * data = NULL;
int flags = 0;
get_error_line_data(&file, &line, &data, &flags);
UA_LOG_INFO(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Internal SSL error file: %s:%d data: %s", file, line, data);
#endif
return UA_STATUSCODE_BADINTERNALERROR;
}
X509_add_ext(x509, ex, -1);
X509_EXTENSION_free(ex);
return UA_STATUSCODE_GOOD;
}
#if (OPENSSL_VERSION_NUMBER >= 0x30000000L)
/* generate the RSA key */
static EVP_PKEY * UA_RSA_Generate_Key (size_t keySizeBits){
return EVP_RSA_gen(keySizeBits);
}
#endif
UA_StatusCode
UA_CreateCertificate(const UA_Logger *logger, const UA_String *subject,
size_t subjectSize, const UA_String *subjectAltName,
size_t subjectAltNameSize, UA_CertificateFormat certFormat,
UA_KeyValueMap *params, UA_ByteString *outPrivateKey,
UA_ByteString *outCertificate) {
if(!outPrivateKey || !outCertificate || !logger || !subjectAltName || !subject ||
subjectAltNameSize == 0 || subjectSize == 0 ||
(certFormat != UA_CERTIFICATEFORMAT_DER && certFormat != UA_CERTIFICATEFORMAT_PEM))
return UA_STATUSCODE_BADINVALIDARGUMENT;
/* Use the maximum size */
UA_UInt16 keySizeBits = 4096;
/* Default to 1 year */
UA_UInt16 expiresInDays = 365;
if(params) {
const UA_UInt16 *keySizeBitsValue = (const UA_UInt16 *)UA_KeyValueMap_getScalar(
params, UA_QUALIFIEDNAME(0, "key-size-bits"), &UA_TYPES[UA_TYPES_UINT16]);
if(keySizeBitsValue)
keySizeBits = *keySizeBitsValue;
const UA_UInt16 *expiresInDaysValue = (const UA_UInt16 *)UA_KeyValueMap_getScalar(
params, UA_QUALIFIEDNAME(0, "expires-in-days"), &UA_TYPES[UA_TYPES_UINT16]);
if(expiresInDaysValue)
expiresInDays = *expiresInDaysValue;
}
UA_ByteString_init(outPrivateKey);
UA_ByteString_init(outCertificate);
UA_String fullAltSubj = UA_STRING_NULL;
UA_Int32 serial = 1;
/** \TODO: Seed Random generator
* See: (https://www.openssl.org/docs/man1.1.0/man3/RAND_add.html) */
BIO *memCert = NULL;
BIO *memPKey = NULL;
UA_StatusCode errRet = UA_STATUSCODE_GOOD;
X509 *x509 = X509_new();
#if (OPENSSL_VERSION_NUMBER >= 0x30000000L)
EVP_PKEY *pkey = UA_RSA_Generate_Key(keySizeBits);
if((pkey == NULL) || (x509 == NULL)) {
errRet = UA_STATUSCODE_BADOUTOFMEMORY;
goto cleanup;
}
#else
BIGNUM *exponent = BN_new();
EVP_PKEY *pkey = EVP_PKEY_new();
RSA *rsa = RSA_new();
if(!pkey || !x509 || !exponent || !rsa) {
errRet = UA_STATUSCODE_BADOUTOFMEMORY;
goto cleanup;
}
UA_LOG_INFO(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Generating RSA key. This may take a while.");
if(BN_set_word(exponent, RSA_F4) != 1) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting RSA exponent failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
if(RSA_generate_key_ex(rsa, (int) keySizeBits, exponent, NULL) != 1) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Generating RSA key failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
if(EVP_PKEY_assign_RSA(pkey, rsa) != 1) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Assign RSA key failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
/* rsa will be freed by pkey */
rsa = NULL;
#endif /* end of OPENSSL_VERSION_NUMBER >= 0x30000000L */
/* x509v3 has version 2
* (https://www.openssl.org/docs/man1.1.0/man3/X509_set_version.html) */
if(X509_set_version(x509, 2) != 1) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting version failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
if(ASN1_INTEGER_set(X509_get_serialNumber(x509), serial) != 1) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting serial number failed.");
/* Only memory errors are possible */
errRet = UA_STATUSCODE_BADOUTOFMEMORY;
goto cleanup;
}
if(X509_gmtime_adj(X509_get_notBefore(x509), 0) == NULL) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting 'not before' failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
if(X509_gmtime_adj(X509_get_notAfter(x509), (UA_Int64)60 * 60 * 24 * expiresInDays) ==
NULL) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting 'not before' failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
if(X509_set_pubkey(x509, pkey) != 1) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting publik key failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
X509_NAME *name = X509_get_subject_name(x509);
if(name == NULL) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Getting name failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
for(UA_UInt32 iSubject = 0; iSubject < subjectSize; ++iSubject) {
UA_Int32 sep = UA_String_chr(&subject[iSubject], '=');
char field[16];
if(sep == -1 || sep == 0 ||
((size_t) sep == (subject[iSubject].length - 1)) || sep >= 15) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Subject must contain one '=' with "
"content before and after.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
memcpy(field, subject[iSubject].data, (size_t) sep);
field[sep] = 0;
UA_Byte* pData = &subject[iSubject].data[sep + 1];
if(X509_NAME_add_entry_by_txt(
name, field, MBSTRING_ASC,
(const unsigned char *)pData,
(int) subject[iSubject].length - (int) sep - 1, -1, 0) != 1) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting subject failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
}
/* Self signed, so issuer == subject */
if(X509_set_issuer_name(x509, name) != 1) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting name failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
errRet = add_x509V3ext(logger, x509, NID_basic_constraints, "CA:FALSE");
if(errRet != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting 'Basic Constraints' failed.");
goto cleanup;
}
/* See https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 for
* possible values */
errRet = add_x509V3ext(logger, x509, NID_key_usage,
"digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment,keyCertSign");
if(errRet != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting 'Key Usage' failed.");
goto cleanup;
}
errRet = add_x509V3ext(logger, x509, NID_ext_key_usage, "serverAuth,clientAuth");
if(errRet != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting 'Extended Key Usage' failed.");
goto cleanup;
}
errRet = add_x509V3ext(logger, x509, NID_subject_key_identifier, "hash");
if(errRet != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting 'Subject Key Identifier' failed.");
goto cleanup;
}
errRet = join_string_with_sep(subjectAltName, subjectAltNameSize, ',', &fullAltSubj);
if(errRet != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Joining altSubject failed.");
goto cleanup;
}
errRet = add_x509V3ext(logger, x509, NID_subject_alt_name, (char*) fullAltSubj.data);
if(errRet != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Setting 'Subject Alternative Name' failed.");
goto cleanup;
}
if(X509_sign(x509, pkey, EVP_sha256()) == 0) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Signing failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
switch(certFormat) {
case UA_CERTIFICATEFORMAT_DER: {
unsigned char *p;
/* Private Key */
/* get length */
outPrivateKey->length = (size_t)i2d_PrivateKey(pkey, NULL);
if((int)outPrivateKey->length <= 0) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Create private DER key failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
/* allocate buffer */
UA_ByteString_allocBuffer(outPrivateKey, outPrivateKey->length);
memset(outPrivateKey->data, 0, outPrivateKey->length);
p = outPrivateKey->data;
i2d_PrivateKey(pkey, &p);
/* Certificate */
/* get length */
outCertificate->length = (size_t)i2d_X509(x509, NULL);
if((int)outCertificate->length <= 0) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Create DER-certificate failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
/* allocate buffer */
UA_ByteString_allocBuffer(outCertificate, outCertificate->length);
memset(outCertificate->data, 0, outCertificate->length);
p = outCertificate->data;
i2d_X509(x509, &p);
break;
}
case UA_CERTIFICATEFORMAT_PEM: {
/* Private Key */
memPKey = BIO_new(BIO_s_mem());
if(!memPKey) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Allocate Membuffer for PKey failed.");
errRet = UA_STATUSCODE_BADOUTOFMEMORY;
goto cleanup;
}
if(PEM_write_bio_PrivateKey(memPKey, pkey, NULL, NULL, 0, 0, NULL) != 1) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Generate PEM-PrivateKey failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
UA_ByteString tmpPem = UA_BYTESTRING_NULL;
tmpPem.length = (size_t) BIO_get_mem_data(memPKey, &tmpPem.data);
errRet = UA_ByteString_copy(&tmpPem, outPrivateKey);
if(errRet != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Copy PEM PKey failed.");
goto cleanup;
}
/* Certificate */
memCert = BIO_new(BIO_s_mem());
if(!memCert) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Allocate Membuffer for Cert failed.");
errRet = UA_STATUSCODE_BADOUTOFMEMORY;
goto cleanup;
}
if(PEM_write_bio_X509(memCert, x509) != 1) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Generate PEM-Certifcate failed.");
errRet = UA_STATUSCODE_BADINTERNALERROR;
goto cleanup;
}
tmpPem.length = (size_t) BIO_get_mem_data(memCert, &tmpPem.data);
errRet = UA_ByteString_copy(&tmpPem, outCertificate);
if(errRet != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(logger, UA_LOGCATEGORY_SECURECHANNEL,
"Create Certificate: Copy PEM Certificate failed.");
goto cleanup;
}
break;
}
}
cleanup:
UA_String_clear(&fullAltSubj);
#if (OPENSSL_VERSION_NUMBER < 0x30000000L)
RSA_free(rsa);
BN_free(exponent);
#endif
X509_free(x509);
EVP_PKEY_free(pkey);
BIO_free(memCert);
BIO_free(memPKey);
return errRet;
}
#endif