mirror of
https://github.com/open62541/open62541.git
synced 2026-10-22 00:16:25 +08:00
425 lines
15 KiB
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
|