mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
The current implementation drops the reason why a download of a CRL failed. This is changed to the reporting of the error triggered by the last CRL download attempt. This mainly helps to check for the download error during CI testing and prevent connectivity issues to be the reason of a test failure. Signed-off-by: Steffen Eiden <seiden@linux.ibm.com> Reviewed-by: Marc Hartmayer <mhartmay@linux.ibm.com> Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
1653 lines
47 KiB
C
1653 lines
47 KiB
C
/*
|
|
* Certificate functions and definitions.
|
|
*
|
|
* Copyright IBM Corp. 2022
|
|
*
|
|
* s390-tools is free software; you can redistribute it and/or modify
|
|
* it under the terms of the MIT license. See LICENSE for details.
|
|
*/
|
|
|
|
/* Must be included before any other header */
|
|
#include "config.h"
|
|
|
|
#include <openssl/pem.h>
|
|
|
|
#include "libpv/cert.h"
|
|
#include "libpv/crypto.h"
|
|
#include "libpv/curl.h"
|
|
|
|
/* Used for the caching of the downloaded CRLs */
|
|
static GHashTable *cached_crls;
|
|
|
|
void pv_cert_init(void)
|
|
{
|
|
if (!cached_crls)
|
|
cached_crls = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
|
|
(GDestroyNotify)X509_CRL_free);
|
|
}
|
|
|
|
void pv_cert_cleanup(void)
|
|
{
|
|
g_clear_pointer(&cached_crls, g_hash_table_destroy);
|
|
}
|
|
|
|
PvX509WithPath *pv_x509_with_path_new(X509 *cert, const char *path)
|
|
{
|
|
g_autoptr(PvX509WithPath) ret = g_new(PvX509WithPath, 1);
|
|
|
|
g_assert(cert && path);
|
|
|
|
if (X509_up_ref(cert) != 1)
|
|
g_abort();
|
|
ret->cert = cert;
|
|
ret->path = g_strdup(path);
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
void pv_x509_with_path_free(PvX509WithPath *cert)
|
|
{
|
|
if (!cert)
|
|
return;
|
|
|
|
X509_free(cert->cert);
|
|
g_free(cert->path);
|
|
g_free(cert);
|
|
}
|
|
|
|
PvX509Pair *pv_x509_pair_new_take(X509 **cert, STACK_OF_X509_CRL **crls)
|
|
{
|
|
g_autoptr(PvX509Pair) ret = g_new0(PvX509Pair, 1);
|
|
|
|
g_assert(cert);
|
|
g_assert(crls);
|
|
|
|
ret->cert = g_steal_pointer(cert);
|
|
ret->crls = g_steal_pointer(crls);
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
void pv_x509_pair_free(PvX509Pair *pair)
|
|
{
|
|
if (!pair)
|
|
return;
|
|
|
|
sk_X509_CRL_pop_free(pair->crls, X509_CRL_free);
|
|
X509_free(pair->cert);
|
|
g_free(pair);
|
|
}
|
|
|
|
void STACK_OF_DIST_POINT_free(STACK_OF_DIST_POINT *stack)
|
|
{
|
|
if (!stack)
|
|
return;
|
|
|
|
sk_DIST_POINT_pop_free(stack, DIST_POINT_free);
|
|
}
|
|
|
|
void STACK_OF_X509_free(STACK_OF_X509 *stack)
|
|
{
|
|
if (!stack)
|
|
return;
|
|
|
|
sk_X509_pop_free(stack, X509_free);
|
|
}
|
|
|
|
void STACK_OF_X509_CRL_free(STACK_OF_X509_CRL *stack)
|
|
{
|
|
if (!stack)
|
|
return;
|
|
|
|
sk_X509_CRL_pop_free(stack, X509_CRL_free);
|
|
}
|
|
|
|
static gboolean certificate_uses_elliptic_curve(EVP_PKEY *key, int nid, GError **error)
|
|
{
|
|
g_autoptr(EC_KEY) ec = NULL;
|
|
int rc;
|
|
|
|
g_assert(key);
|
|
|
|
if (EVP_PKEY_id(key) != EVP_PKEY_EC) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INVALID_PARM, _("No EC key found"));
|
|
return FALSE;
|
|
}
|
|
|
|
ec = EVP_PKEY_get1_EC_KEY(key);
|
|
if (!ec) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INVALID_PARM, _("No EC key found"));
|
|
return FALSE;
|
|
}
|
|
|
|
if (EC_KEY_check_key(ec) != 1) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INVALID_PARM, _("Invalid EC key"));
|
|
return FALSE;
|
|
}
|
|
|
|
rc = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec));
|
|
if (rc != nid) {
|
|
/* maybe the NID is unset */
|
|
if (rc == 0) {
|
|
g_autoptr(EC_GROUP) grp = EC_GROUP_new_by_curve_name(nid);
|
|
const EC_POINT *pub = EC_KEY_get0_public_key(ec);
|
|
g_autoptr(BN_CTX) ctx = BN_CTX_new();
|
|
|
|
if (EC_POINT_is_on_curve(grp, pub, ctx) != 1) {
|
|
g_set_error_literal(error, PV_CERT_ERROR,
|
|
PV_CERT_ERROR_INVALID_PARM,
|
|
_("Invalid EC curve"));
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
/* NID was set but doesn't match with the expected NID
|
|
*/
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INVALID_PARM,
|
|
_("Wrong NID used: '%d'"),
|
|
EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)));
|
|
return FALSE;
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
EVP_PKEY *pv_x509_get_ec_pubkey(X509 *cert, int nid, GError **error)
|
|
{
|
|
g_autoptr(EVP_PKEY) ret = NULL;
|
|
|
|
ret = X509_get_pubkey(cert);
|
|
if (!ret) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INVALID_PARM,
|
|
_("Failed to get public key from host-key document"));
|
|
return NULL;
|
|
}
|
|
|
|
if (!certificate_uses_elliptic_curve(ret, nid, error)) {
|
|
g_prefix_error(error, _("Host-key document does not use an elliptic EC curve"));
|
|
return NULL;
|
|
}
|
|
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
GSList *pv_get_ec_pubkeys(PvCertWithPathList *certs_with_path, int nid, GError **error)
|
|
{
|
|
g_autoslist(EVP_PKEY) ret = NULL;
|
|
|
|
for (GSList *iterator = certs_with_path; iterator; iterator = iterator->next) {
|
|
const PvX509WithPath *cert_with_path = iterator->data;
|
|
g_autoptr(EVP_PKEY) host_key = NULL;
|
|
X509 *cert = cert_with_path->cert;
|
|
|
|
host_key = pv_x509_get_ec_pubkey(cert, nid, error);
|
|
if (!host_key)
|
|
return NULL;
|
|
|
|
ret = g_slist_append(ret, g_steal_pointer(&host_key));
|
|
}
|
|
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
PvCertWithPathList *pv_load_certificates(char **cert_paths, GError **error)
|
|
{
|
|
g_autoslist(PvX509WithPath) ret = NULL;
|
|
|
|
for (char **iterator = cert_paths; iterator != NULL && *iterator != NULL; iterator++) {
|
|
const char *cert_path = *iterator;
|
|
g_autoptr(X509) cert = NULL;
|
|
|
|
g_assert(cert_path);
|
|
|
|
cert = pv_load_first_cert_from_file(cert_path, error);
|
|
if (!cert)
|
|
return NULL;
|
|
|
|
ret = g_slist_append(ret, pv_x509_with_path_new(cert, cert_path));
|
|
}
|
|
if (!ret) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_READ_CERTIFICATE,
|
|
_("no certificates specified"));
|
|
return NULL;
|
|
}
|
|
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
X509 *pv_load_first_cert_from_file(const char *path, GError **error)
|
|
{
|
|
g_autoptr(BIO) bio = BIO_new_file(path, "r");
|
|
g_autoptr(X509) cert = NULL;
|
|
|
|
if (!bio) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_READ_CERTIFICATE,
|
|
_("unable to read certificate: '%s'"), path);
|
|
return NULL;
|
|
}
|
|
|
|
cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
|
if (cert)
|
|
return g_steal_pointer(&cert);
|
|
ERR_clear_error();
|
|
if (pv_BIO_reset(bio) < 0) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_READ_CERTIFICATE,
|
|
_("unable to load certificate: '%s'"), path);
|
|
return NULL;
|
|
}
|
|
|
|
/* maybe the certificate is stored in DER format */
|
|
cert = d2i_X509_bio(bio, NULL);
|
|
if (cert)
|
|
return g_steal_pointer(&cert);
|
|
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_READ_CERTIFICATE,
|
|
_("unable to load certificate: '%s'"), path);
|
|
return NULL;
|
|
}
|
|
|
|
static X509_CRL *load_crl_from_bio(BIO *bio)
|
|
{
|
|
g_autoptr(X509_CRL) crl = PEM_read_bio_X509_CRL(bio, NULL, NULL, NULL);
|
|
if (crl)
|
|
return g_steal_pointer(&crl);
|
|
ERR_clear_error();
|
|
if (pv_BIO_reset(bio) < 0)
|
|
return NULL;
|
|
|
|
/* maybe the CRL is stored in DER format */
|
|
crl = d2i_X509_CRL_bio(bio, NULL);
|
|
if (crl)
|
|
return g_steal_pointer(&crl);
|
|
return NULL;
|
|
}
|
|
|
|
/* This function reads in only the first CRL and ignores all other. This is only
|
|
* relevant for the PEM file format.
|
|
*/
|
|
X509_CRL *pv_load_first_crl_from_file(const char *path, GError **error)
|
|
{
|
|
g_autoptr(BIO) bio = BIO_new_file(path, "r");
|
|
g_autoptr(X509_CRL) crl = NULL;
|
|
|
|
if (!bio) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_READ_CRL,
|
|
_("unable to read CRL: '%s'"), path);
|
|
return NULL;
|
|
}
|
|
|
|
crl = load_crl_from_bio(bio);
|
|
if (!crl) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_READ_CRL,
|
|
_("unable to load CRL: '%s'"), path);
|
|
return NULL;
|
|
}
|
|
return g_steal_pointer(&crl);
|
|
}
|
|
|
|
static char *pv_X509_NAME_oneline(const X509_NAME *name)
|
|
{
|
|
g_autoptr(BIO) key_bio = BIO_new(BIO_s_mem());
|
|
g_autofree char *ret = NULL;
|
|
char *key = NULL;
|
|
long len;
|
|
|
|
if (X509_NAME_print_ex(key_bio, name, 0, XN_FLAG_RFC2253) == -1) {
|
|
g_warning(_("Cannot receive X509-NAME from CRL: %s"), pv_get_openssl_error());
|
|
return NULL;
|
|
}
|
|
|
|
len = BIO_get_mem_data(key_bio, &key);
|
|
if (len < 0) {
|
|
g_warning(_("Cannot receive X509-NAME from CRL"));
|
|
return NULL;
|
|
}
|
|
|
|
ret = g_malloc0((size_t)len + 1);
|
|
memcpy(ret, key, (size_t)len);
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
static gboolean cache_crl(const X509_NAME *name, X509_CRL *crl)
|
|
{
|
|
g_autofree char *key = NULL;
|
|
|
|
g_assert(name);
|
|
|
|
key = pv_X509_NAME_oneline(name);
|
|
if (!key) {
|
|
g_warning(_("Cannot receive X509-NAME from CRL"));
|
|
return FALSE;
|
|
}
|
|
if (X509_CRL_up_ref(crl) != 1)
|
|
g_abort();
|
|
return g_hash_table_insert(cached_crls, g_steal_pointer(&key), crl);
|
|
}
|
|
|
|
/* Caller is responsible for free'ing */
|
|
static X509_CRL *lookup_crl(const X509_NAME *name)
|
|
{
|
|
g_autoptr(X509_CRL) crl = NULL;
|
|
g_autofree char *key = NULL;
|
|
|
|
g_assert(name);
|
|
|
|
key = pv_X509_NAME_oneline(name);
|
|
if (!key)
|
|
return NULL;
|
|
crl = g_hash_table_lookup(cached_crls, key);
|
|
if (crl) {
|
|
if (X509_CRL_up_ref(crl) != 1)
|
|
g_abort();
|
|
return g_steal_pointer(&crl);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Returns empty stack if no CRL downloaded. */
|
|
static STACK_OF_X509_CRL *crls_download_cb(const X509_STORE_CTX *ctx, const X509_NAME *nm)
|
|
{
|
|
g_autoptr(STACK_OF_X509_CRL) crls = NULL;
|
|
g_autoptr(X509_CRL) crl = NULL;
|
|
/* must not be free'd */
|
|
X509 *cert = NULL;
|
|
|
|
crls = sk_X509_CRL_new_null();
|
|
if (!crls)
|
|
g_abort();
|
|
cert = pv_X509_STORE_CTX_get_current_cert(ctx);
|
|
if (!cert)
|
|
return g_steal_pointer(&crls);
|
|
g_assert(X509_NAME_cmp(X509_get_issuer_name(cert), nm) == 0);
|
|
crl = lookup_crl(nm);
|
|
if (!crl) {
|
|
/* ignore error */
|
|
crl = pv_load_first_crl_by_cert(cert, NULL);
|
|
if (!crl)
|
|
return g_steal_pointer(&crls);
|
|
g_assert_true(cache_crl(nm, crl));
|
|
}
|
|
if (sk_X509_CRL_push(crls, g_steal_pointer(&crl)) == 0)
|
|
g_abort();
|
|
return g_steal_pointer(&crls);
|
|
}
|
|
|
|
/* Downloaded CRLs have a higher precedence than the CRLs specified on the
|
|
* command line.
|
|
*/
|
|
static STACK_OF_X509_CRL *crls_cb(const X509_STORE_CTX *ctx, const X509_NAME *nm)
|
|
{
|
|
g_autoptr(STACK_OF_X509_CRL) crls = crls_download_cb(ctx, nm);
|
|
|
|
if (sk_X509_CRL_num(crls) > 0)
|
|
return g_steal_pointer(&crls);
|
|
return pv_X509_STORE_CTX_get1_crls(ctx, nm);
|
|
}
|
|
|
|
/* Set up CRL lookup with download support */
|
|
void pv_store_setup_crl_download(X509_STORE *st)
|
|
{
|
|
pv_X509_STORE_set_lookup_crls(st, crls_cb);
|
|
}
|
|
|
|
static X509_CRL *GByteArray_to_X509_CRL(const GByteArray *data)
|
|
{
|
|
g_autoptr(X509_CRL) ret = NULL;
|
|
g_autoptr(BIO) bio = NULL;
|
|
|
|
g_assert(data);
|
|
|
|
if (data->len > INT_MAX)
|
|
return NULL;
|
|
|
|
bio = BIO_new_mem_buf(data->data, (int)data->len);
|
|
if (!bio)
|
|
g_abort();
|
|
|
|
ret = load_crl_from_bio(bio);
|
|
if (!ret)
|
|
return NULL;
|
|
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
static int load_crl_from_web(const char *url, X509_CRL **crl, GError **error)
|
|
{
|
|
g_autoptr(X509_CRL) tmp_crl = NULL;
|
|
g_autoptr(GByteArray) data = NULL;
|
|
g_assert(crl);
|
|
|
|
data = curl_download(url, CRL_DOWNLOAD_TIMEOUT_MS, CRL_DOWNLOAD_MAX_SIZE, error);
|
|
if (!data) {
|
|
g_prefix_error(error, _("unable to download CRL: "));
|
|
return -1;
|
|
}
|
|
tmp_crl = GByteArray_to_X509_CRL(data);
|
|
if (!tmp_crl) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_CRL_DOWNLOAD_FAILED,
|
|
_("unable to load CRL from '%s'"), url);
|
|
return -1;
|
|
}
|
|
*crl = g_steal_pointer(&tmp_crl);
|
|
return 0;
|
|
}
|
|
|
|
/* Get the first http[s] URL from a DIST_POINT */
|
|
static const char *get_first_dp_url(DIST_POINT *dp)
|
|
{
|
|
GENERAL_NAMES *general_names;
|
|
|
|
g_assert(dp);
|
|
|
|
if (!dp->distpoint || dp->distpoint->type != 0)
|
|
return NULL;
|
|
|
|
general_names = dp->distpoint->name.fullname;
|
|
for (int i = 0; i < sk_GENERAL_NAME_num(general_names); i++) {
|
|
GENERAL_NAME *name = sk_GENERAL_NAME_value(general_names, i);
|
|
g_autofree const char *uri_str = NULL;
|
|
ASN1_STRING *uri_asn1;
|
|
const char *uri_data;
|
|
int uri_data_len;
|
|
int type;
|
|
|
|
uri_asn1 = GENERAL_NAME_get0_value(name, &type);
|
|
if (type != GEN_URI)
|
|
continue;
|
|
uri_data_len = ASN1_STRING_length(uri_asn1);
|
|
if (uri_data_len < 0)
|
|
continue;
|
|
uri_data = (const char *)ASN1_STRING_get0_data(uri_asn1);
|
|
/* Make sure that uri_str is null-terminated as in general it
|
|
* cannot be assumed that @uri_data is null-terminated.
|
|
*/
|
|
uri_str = g_strndup(uri_data, (size_t)uri_data_len);
|
|
if (g_str_has_prefix(uri_str, "http://"))
|
|
return uri_data;
|
|
if (g_str_has_prefix(uri_str, "https://"))
|
|
return uri_data;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Download a CRL using the URI specified in the distribution @crldp */
|
|
static X509_CRL *load_crl_by_dist_point(DIST_POINT *crldp, GError **error)
|
|
{
|
|
const char *uri = get_first_dp_url(crldp);
|
|
g_autoptr(X509_CRL) crl = NULL;
|
|
|
|
if (!uri) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INTERNAL,
|
|
_("no valid URL specified in distribution point"));
|
|
return NULL;
|
|
}
|
|
|
|
if (load_crl_from_web(uri, &crl, error) < 0)
|
|
return NULL;
|
|
|
|
return g_steal_pointer(&crl);
|
|
}
|
|
|
|
/* This function returns the first X509_CRL found from the CRL distribution
|
|
* points specified in @cert. This function could be optimized by filtering
|
|
* duplicate certificates and/or filtering duplicated URIs.
|
|
*/
|
|
X509_CRL *pv_load_first_crl_by_cert(X509 *cert, GError **error)
|
|
{
|
|
g_autoptr(STACK_OF_DIST_POINT) crldps = NULL;
|
|
g_autoptr(GError) last_error = NULL;
|
|
g_autoptr(X509_CRL) ret = NULL;
|
|
int dist_points_cnt;
|
|
|
|
g_assert(cert);
|
|
|
|
crldps = X509_get_ext_d2i(cert, NID_crl_distribution_points, NULL, NULL);
|
|
if (!crldps || sk_DIST_POINT_num(crldps) == 0) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_NO_CRLDP,
|
|
_("no distribution point found"));
|
|
return NULL;
|
|
}
|
|
|
|
dist_points_cnt = sk_DIST_POINT_num(crldps);
|
|
for (int i = 0; i < dist_points_cnt; i++) {
|
|
DIST_POINT *crldp = sk_DIST_POINT_value(crldps, i);
|
|
g_assert(crldp);
|
|
|
|
g_clear_error(&last_error);
|
|
ret = load_crl_by_dist_point(crldp, &last_error);
|
|
if (ret)
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
/* relabel error */
|
|
if (last_error)
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_FAILED_DOWNLOAD_CRL,
|
|
"%s", last_error->message);
|
|
else
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_FAILED_DOWNLOAD_CRL,
|
|
_("failed to download CRL"));
|
|
return NULL;
|
|
}
|
|
|
|
STACK_OF_X509_CRL *pv_try_load_crls_by_certs(GSList *certs_with_path)
|
|
{
|
|
g_autoptr(STACK_OF_X509_CRL) ret = sk_X509_CRL_new_null();
|
|
if (!ret)
|
|
g_abort();
|
|
|
|
for (GSList *iterator = certs_with_path; iterator; iterator = iterator->next) {
|
|
PvX509WithPath *cert_with_path = iterator->data;
|
|
X509 *cert = cert_with_path->cert;
|
|
g_autoptr(X509_CRL) crl = NULL;
|
|
g_assert(cert);
|
|
/* ignore error */
|
|
crl = pv_load_first_crl_by_cert(cert, NULL);
|
|
if (!crl)
|
|
continue;
|
|
if (sk_X509_CRL_push(ret, g_steal_pointer(&crl)) == 0)
|
|
g_abort();
|
|
}
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
#define DEFINE_GSLIST_MAP(t2, t1) \
|
|
typedef t1 *(*g_slist_map_func_##t2##_##t1)(const t2 *x, GError **error); \
|
|
G_GNUC_UNUSED static GSList *g_slist_map_##t2##_##t1( \
|
|
const GSList *list, g_slist_map_func_##t2##_##t1 func, GError **error) \
|
|
{ \
|
|
g_autoslist(t1) ret = NULL; \
|
|
for (const GSList *iterator = list; iterator; iterator = iterator->next) { \
|
|
const t2 *value = iterator->data; \
|
|
t1 *new_value = NULL; \
|
|
g_assert(value); \
|
|
new_value = func(value, error); \
|
|
if (!new_value) \
|
|
return NULL; \
|
|
ret = g_slist_append(ret, g_steal_pointer(&new_value)); \
|
|
} \
|
|
return g_steal_pointer(&ret); \
|
|
}
|
|
|
|
#define DEFINE_GSLIST_TO_STACK(t1) \
|
|
G_GNUC_UNUSED static STACK_OF(t1) * g_slist_to_stack_of_##t1(GSList **list) \
|
|
{ \
|
|
g_assert(list); \
|
|
g_autoptr(STACK_OF_##t1) ret = sk_##t1##_new_null(); \
|
|
if (!ret) \
|
|
g_abort(); \
|
|
for (GSList *iterator = *list; iterator; iterator = iterator->next) { \
|
|
if (sk_##t1##_push(ret, g_steal_pointer(&iterator->data)) == 0) \
|
|
g_abort(); \
|
|
} \
|
|
g_clear_pointer(list, g_slist_free); \
|
|
return g_steal_pointer(&ret); \
|
|
}
|
|
|
|
DEFINE_GSLIST_MAP(PvX509WithPath, X509)
|
|
DEFINE_GSLIST_TO_STACK(X509)
|
|
|
|
static X509 *pv_x509_with_path_get_cert(const PvX509WithPath *cert_with_path,
|
|
G_GNUC_UNUSED GError **error)
|
|
{
|
|
g_autoptr(X509) cert = NULL;
|
|
|
|
g_assert(cert_with_path && cert_with_path->cert);
|
|
|
|
cert = cert_with_path->cert;
|
|
if (X509_up_ref(cert) != 1)
|
|
g_abort();
|
|
return g_steal_pointer(&cert);
|
|
}
|
|
|
|
/* @crl_paths is allowed to be NULL */
|
|
static int load_crls_to_store(X509_STORE *store, char **crl_paths, gboolean err_out_empty_crls,
|
|
GError **error)
|
|
{
|
|
for (char **iterator = crl_paths; iterator != NULL && *iterator != NULL; iterator++) {
|
|
X509_LOOKUP *lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
|
|
const char *crl_path = *iterator;
|
|
int count;
|
|
|
|
g_assert(crl_path);
|
|
|
|
if (!lookup) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INTERNAL,
|
|
_("X509 store initialization failed"));
|
|
return -1;
|
|
}
|
|
|
|
/* support *.pem files containing multiple CRLs */
|
|
count = X509_load_crl_file(lookup, crl_path, X509_FILETYPE_PEM);
|
|
if (count > 0)
|
|
continue;
|
|
|
|
count = X509_load_crl_file(lookup, crl_path, X509_FILETYPE_ASN1);
|
|
if (count == 1)
|
|
continue;
|
|
|
|
if (err_out_empty_crls) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_LOAD_CRL,
|
|
_("unable to load CRL from: '%s'"), crl_path);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
X509_STORE *pv_store_setup(char *root_ca_path, char **crl_paths, char **cert_with_crl_paths,
|
|
GError **error)
|
|
{
|
|
g_autoptr(X509_STORE) store = X509_STORE_new();
|
|
if (!store)
|
|
g_abort();
|
|
|
|
/* if @root_ca_path != NULL use the specified root CA only, otherwise use the
|
|
* default root CAs found on the system
|
|
*/
|
|
if (root_ca_path) {
|
|
X509_LOOKUP *lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
|
|
int count;
|
|
|
|
if (!lookup) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INTERNAL,
|
|
_("X509 store initialization failed"));
|
|
return NULL;
|
|
}
|
|
|
|
/* only the PEM format allows embedded CRLs so we've to
|
|
* check for it only here and not in case of ASN1
|
|
*/
|
|
count = X509_load_cert_file(lookup, root_ca_path, X509_FILETYPE_PEM);
|
|
if (count > 0) {
|
|
/* Out of security reasons that it can be easily
|
|
* overseen that there are multiple certificates located
|
|
* in a PEM-file we raise an error
|
|
*/
|
|
if (count > 1) {
|
|
g_set_error(
|
|
error, PV_CERT_ERROR, PV_CERT_ERROR_LOAD_ROOT_CA,
|
|
_("multiple certificates in one PEM file is not supported: '%s'"),
|
|
root_ca_path);
|
|
return NULL;
|
|
}
|
|
|
|
/* PEM format so it's possible there are CRLs embedded
|
|
*/
|
|
(void)X509_load_crl_file(lookup, root_ca_path, X509_FILETYPE_PEM);
|
|
} else {
|
|
/* Maybe the root CA is stored in ASN1 format */
|
|
count = X509_load_cert_file(lookup, root_ca_path, X509_FILETYPE_ASN1);
|
|
if (count != 1) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_LOAD_ROOT_CA,
|
|
_("failed to load root certificate from '%s'"),
|
|
root_ca_path);
|
|
return NULL;
|
|
}
|
|
}
|
|
} else {
|
|
/* Load certificates into @store from the hardcoded OpenSSL
|
|
* default paths
|
|
*/
|
|
if (X509_STORE_set_default_paths(store) != 1) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_LOAD_DEFAULT_CA,
|
|
_("failed to load system root certificates"));
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
/* Error out if a CRL file was provided that has not at least one CRL*/
|
|
if (load_crls_to_store(store, crl_paths, TRUE, error) < 0)
|
|
return NULL;
|
|
|
|
/* Try to load CRLs from the provided untrusted certificates */
|
|
if (load_crls_to_store(store, cert_with_crl_paths, FALSE, error) < 0)
|
|
return NULL;
|
|
|
|
return g_steal_pointer(&store);
|
|
}
|
|
|
|
STACK_OF_X509 *pv_get_x509_stack(const GSList *x509_with_path_list)
|
|
{
|
|
g_autoslist(X509) certs = NULL;
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
certs = g_slist_map_PvX509WithPath_X509(x509_with_path_list, pv_x509_with_path_get_cert,
|
|
&error);
|
|
g_assert_no_error(error);
|
|
return g_slist_to_stack_of_X509(&certs);
|
|
}
|
|
|
|
int pv_init_store_ctx(X509_STORE_CTX *ctx, X509_STORE *trusted, STACK_OF_X509 *chain,
|
|
GError **error)
|
|
{
|
|
pv_wrapped_g_assert(ctx);
|
|
pv_wrapped_g_assert(trusted);
|
|
pv_wrapped_g_assert(chain);
|
|
|
|
if (X509_STORE_CTX_init(ctx, trusted, NULL, chain) != 1) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INTERNAL,
|
|
_("X509 store initialization failed: %s"),
|
|
X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx)));
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
X509_STORE_CTX *pv_create_store_ctx(X509_STORE *trusted, STACK_OF_X509 *chain, GError **error)
|
|
{
|
|
g_autoptr(X509_STORE_CTX) ctx = X509_STORE_CTX_new();
|
|
|
|
pv_wrapped_g_assert(trusted);
|
|
pv_wrapped_g_assert(chain);
|
|
|
|
if (!ctx)
|
|
return NULL;
|
|
|
|
if (pv_init_store_ctx(ctx, trusted, chain, error) < 0)
|
|
return NULL;
|
|
|
|
return g_steal_pointer(&ctx);
|
|
}
|
|
|
|
int pv_store_set_verify_param(X509_STORE *store, GError **error)
|
|
{
|
|
g_autoptr(X509_VERIFY_PARAM) param = NULL;
|
|
unsigned long flags = X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL |
|
|
X509_V_FLAG_TRUSTED_FIRST | X509_V_FLAG_CHECK_SS_SIGNATURE |
|
|
X509_V_FLAG_X509_STRICT | X509_V_FLAG_POLICY_CHECK;
|
|
|
|
/* Create a X509_VERIFY_PARAM structure, which specifies which checks
|
|
* should be done by the certificate verification operation
|
|
*/
|
|
param = X509_VERIFY_PARAM_new();
|
|
if (!param)
|
|
g_abort();
|
|
|
|
/* The maximum depth level of the chain of trust for the verification of
|
|
* the IBM Z signing key is 2, i.e. IBM Z signing key -> intermediate CA
|
|
* -> root CA
|
|
*/
|
|
X509_VERIFY_PARAM_set_depth(param, 2);
|
|
|
|
/* Set minimum allowed security level to at least 112 bits. */
|
|
X509_VERIFY_PARAM_set_auth_level(param, PV_CERTS_SECURITY_LEVEL);
|
|
|
|
/* Set verification purpose to 'Any Purpose' and specify that the
|
|
* associated trust setting of the default purpose should be used.
|
|
*/
|
|
if (X509_VERIFY_PARAM_set_purpose(param, X509_PURPOSE_ANY | X509_TRUST_DEFAULT) != 1)
|
|
goto error;
|
|
|
|
/* Each certificate from the chain of trust must be checked against a
|
|
* CRL to see if it has been revoked. In addition, use trusted
|
|
* certificates first mode, check signature of the last certificate,
|
|
* strict mode, and verify the policies.
|
|
*/
|
|
if (X509_VERIFY_PARAM_set_flags(param, flags) != 1)
|
|
goto error;
|
|
|
|
if (X509_STORE_set1_param(store, param) != 1)
|
|
goto error;
|
|
|
|
return 0;
|
|
|
|
error:
|
|
g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL,
|
|
_("X509 store initialization failed"));
|
|
return -1;
|
|
}
|
|
|
|
static int x509_name_entry_get0_data(X509_NAME_ENTRY *entry, const uint8_t **data, size_t *data_len)
|
|
{
|
|
const ASN1_STRING *asn1_str;
|
|
int tmp_data_len;
|
|
|
|
g_assert(data);
|
|
g_assert(data_len);
|
|
|
|
asn1_str = X509_NAME_ENTRY_get_data(entry);
|
|
if (!asn1_str)
|
|
return -1;
|
|
|
|
tmp_data_len = ASN1_STRING_length(asn1_str);
|
|
if (tmp_data_len < 0)
|
|
return -1;
|
|
|
|
*data = ASN1_STRING_get0_data(asn1_str);
|
|
*data_len = (size_t)tmp_data_len;
|
|
return 0;
|
|
}
|
|
|
|
/* The caller must not free *data! */
|
|
static int x509_name_get0_data_by_NID(X509_NAME *name, int nid, const uint8_t **data,
|
|
size_t *data_len)
|
|
{
|
|
X509_NAME_ENTRY *entry = NULL;
|
|
int lastpos = -1;
|
|
|
|
lastpos = X509_NAME_get_index_by_NID(name, nid, lastpos);
|
|
if (lastpos == -1)
|
|
return -1;
|
|
|
|
entry = X509_NAME_get_entry(name, lastpos);
|
|
if (!entry)
|
|
return -1;
|
|
|
|
if (x509_name_entry_get0_data(entry, data, data_len) < 0)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* @y must be a NULL-terminated string */
|
|
static gboolean x509_name_data_by_nid_equal(X509_NAME *name, int nid, const char *y)
|
|
{
|
|
const uint8_t *data = NULL;
|
|
size_t y_len = strlen(y);
|
|
size_t data_len;
|
|
|
|
if (x509_name_get0_data_by_NID(name, nid, &data, &data_len) < 0)
|
|
return FALSE;
|
|
|
|
if (data_len != y_len)
|
|
return FALSE;
|
|
|
|
return memcmp(data, y, data_len) == 0;
|
|
}
|
|
|
|
/* Checks whether the subject of @cert is a IBM signing key subject. For this we
|
|
* must check that the subject is equal to: 'C = US, ST = New York, L =
|
|
* Poughkeepsie, O = International Business Machines Corporation, CN =
|
|
* International Business Machines Corporation' and the organization unit (OUT)
|
|
* must end with the suffix ' Key Signing Service'.
|
|
*/
|
|
static gboolean has_ibm_signing_subject(X509 *cert)
|
|
{
|
|
X509_NAME *subject = X509_get_subject_name(cert);
|
|
/* X509_NAME_entry_count is safe to be used with NULL */
|
|
int entry_count = X509_NAME_entry_count(subject);
|
|
g_autofree char *data_str = NULL;
|
|
const uint8_t *data;
|
|
size_t data_len;
|
|
|
|
if (entry_count != PV_IMB_Z_SUBJECT_ENTRY_COUNT)
|
|
return FALSE;
|
|
|
|
if (!x509_name_data_by_nid_equal(subject, NID_countryName, PV_IBM_Z_SUBJECT_COUNTRY_NAME))
|
|
return FALSE;
|
|
|
|
if (!x509_name_data_by_nid_equal(subject, NID_stateOrProvinceName, PV_IBM_Z_SUBJECT_STATE))
|
|
return FALSE;
|
|
|
|
if (!x509_name_data_by_nid_equal(subject, NID_localityName, PV_IBM_Z_SUBJECT_LOCALITY_NAME))
|
|
return FALSE;
|
|
|
|
if (!x509_name_data_by_nid_equal(subject, NID_organizationName,
|
|
PV_IBM_Z_SUBJECT_ORGANIZATION_NAME))
|
|
return FALSE;
|
|
|
|
if (!x509_name_data_by_nid_equal(subject, NID_commonName, PV_IBM_Z_SUBJECT_COMMON_NAME))
|
|
return FALSE;
|
|
|
|
if (x509_name_get0_data_by_NID(subject, NID_organizationalUnitName, &data, &data_len) < 0)
|
|
return FALSE;
|
|
|
|
/* Make sure that data_str is null-terminated as in general it cannot be
|
|
* assumed that @data is null-terminated.
|
|
*/
|
|
data_str = g_strndup((const char *)data, data_len);
|
|
if (!g_str_has_suffix(data_str, PV_IBM_Z_SUBJECT_ORGANIZATIONAL_UNIT_NAME_SUFFIX))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Return a list of all IBM Z signing key certificates in @certs and remove them
|
|
* from the chain. Return empty stack if no IBM Z signing key is found.
|
|
*/
|
|
STACK_OF_X509 *pv_remove_ibm_signing_certs(STACK_OF_X509 *certs)
|
|
{
|
|
g_autoptr(STACK_OF_X509) ret = sk_X509_new_null();
|
|
|
|
for (int i = 0; i < sk_X509_num(certs); i++) {
|
|
X509 *cert = sk_X509_value(certs, i);
|
|
|
|
g_assert(cert);
|
|
|
|
if (!has_ibm_signing_subject(cert))
|
|
continue;
|
|
|
|
/* Remove this certificate from the list and change i-- as the
|
|
* array has changed - this is not beautiful, but right now the
|
|
* easiest solution I came up with.
|
|
*/
|
|
if (sk_X509_delete(certs, i--) != cert)
|
|
g_abort();
|
|
|
|
if (sk_X509_push(ret, g_steal_pointer(&cert)) == 0)
|
|
g_abort();
|
|
}
|
|
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
static X509_NAME *x509_name_reorder_attributes(const X509_NAME *name, const int nids[],
|
|
size_t nids_len)
|
|
{
|
|
int entry_count = X509_NAME_entry_count(name);
|
|
g_autoptr(X509_NAME) ret = NULL;
|
|
|
|
if (entry_count < 0)
|
|
return NULL;
|
|
|
|
if (nids_len != (size_t)entry_count)
|
|
return NULL;
|
|
|
|
ret = X509_NAME_new();
|
|
if (!ret)
|
|
g_abort();
|
|
|
|
for (size_t i = 0; i < nids_len; i++) {
|
|
const X509_NAME_ENTRY *entry = NULL;
|
|
int nid = nids[i];
|
|
int lastpos = -1;
|
|
|
|
lastpos = X509_NAME_get_index_by_NID((X509_NAME *)name, nid, lastpos);
|
|
if (lastpos == -1)
|
|
return NULL;
|
|
|
|
entry = X509_NAME_get_entry(name, lastpos);
|
|
if (!entry)
|
|
return NULL;
|
|
|
|
if (X509_NAME_add_entry(ret, entry, -1, 0) != 1)
|
|
return NULL;
|
|
}
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
X509_NAME *pv_c2b_name(const X509_NAME *name)
|
|
{
|
|
int nids[] = { NID_countryName, NID_organizationName, NID_organizationalUnitName,
|
|
NID_localityName, NID_stateOrProvinceName, NID_commonName };
|
|
g_autoptr(X509_NAME) broken_name = NULL;
|
|
|
|
g_assert(name);
|
|
|
|
/* Try to reorder the attributes */
|
|
broken_name = x509_name_reorder_attributes(name, nids, G_N_ELEMENTS(nids));
|
|
if (broken_name)
|
|
return g_steal_pointer(&broken_name);
|
|
return X509_NAME_dup((X509_NAME *)name);
|
|
}
|
|
|
|
static int security_level_to_bits(int level)
|
|
{
|
|
static int security_bits[] = { 0, 80, 112, 128, 192, 256 };
|
|
|
|
g_assert(level > 0 && level < (int)G_N_ELEMENTS(security_bits));
|
|
|
|
return security_bits[level];
|
|
}
|
|
|
|
/* returns
|
|
* 0 when the certificate is valid,
|
|
* -1 when not yet valid,
|
|
* 1 when expired
|
|
*/
|
|
static int check_validity_period(const ASN1_TIME *not_before, const ASN1_TIME *not_after)
|
|
{
|
|
if (X509_cmp_current_time(not_before) != -1)
|
|
return -1;
|
|
|
|
if (X509_cmp_current_time(not_after) != 1)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static gboolean own_X509_NAME_ENTRY_equal(const X509_NAME_ENTRY *x, const X509_NAME_ENTRY *y)
|
|
{
|
|
const ASN1_OBJECT *x_obj = X509_NAME_ENTRY_get_object(x);
|
|
const ASN1_STRING *x_data = X509_NAME_ENTRY_get_data(x);
|
|
const ASN1_OBJECT *y_obj = X509_NAME_ENTRY_get_object(y);
|
|
const ASN1_STRING *y_data = X509_NAME_ENTRY_get_data(y);
|
|
int x_len = ASN1_STRING_length(x_data);
|
|
int y_len = ASN1_STRING_length(y_data);
|
|
|
|
if (x_len < 0 || x_len != y_len)
|
|
return FALSE;
|
|
|
|
/* ASN1_STRING_cmp(x_data, y_data) == 0 doesn't work because it also
|
|
* compares the type, which is sometimes different.
|
|
*/
|
|
return OBJ_cmp(x_obj, y_obj) == 0 &&
|
|
memcmp(ASN1_STRING_get0_data(x_data), ASN1_STRING_get0_data(y_data),
|
|
(unsigned long)x_len) == 0;
|
|
}
|
|
|
|
static gboolean own_X509_NAME_equal(const X509_NAME *x, const X509_NAME *y)
|
|
{
|
|
int x_count = X509_NAME_entry_count(x);
|
|
int y_count = X509_NAME_entry_count(y);
|
|
|
|
if (x != y && (!x || !y))
|
|
return FALSE;
|
|
|
|
if (x_count != y_count)
|
|
return FALSE;
|
|
|
|
for (int i = 0; i < x_count; i++) {
|
|
const X509_NAME_ENTRY *entry_i = X509_NAME_get_entry(x, i);
|
|
gboolean entry_found = FALSE;
|
|
|
|
for (int j = 0; j < y_count; j++) {
|
|
const X509_NAME_ENTRY *entry_j = X509_NAME_get_entry(y, j);
|
|
|
|
if (own_X509_NAME_ENTRY_equal(entry_i, entry_j)) {
|
|
entry_found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!entry_found)
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/* Verify that the used public key algorithm matches the subject signature
|
|
* algorithm
|
|
*/
|
|
static int check_signature_algo_match(const EVP_PKEY *pkey, const X509 *subject, GError **error)
|
|
{
|
|
int pkey_nid;
|
|
|
|
if (!pkey) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_NO_PUBLIC_KEY, _("no public key"));
|
|
return -1;
|
|
}
|
|
|
|
if (OBJ_find_sigid_algs(X509_get_signature_nid(subject), NULL, &pkey_nid) != 1) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INVALID_SIGNATURE_ALGORITHM,
|
|
_("unsupported signature algorithm"));
|
|
return -1;
|
|
}
|
|
|
|
if (EVP_PKEY_type(pkey_nid) != EVP_PKEY_base_id(pkey)) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_SIGNATURE_ALGORITHM_MISMATCH,
|
|
_("signature algorithm mismatch"));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* It's almost the same as X509_check_issed from OpenSSL does except that we
|
|
* don't check the key usage of the potential issuer. This means we check:
|
|
* 1. issuer_name(cert) == subject_name(issuer)
|
|
* 2. Check whether the akid(cert) (if available) matches the issuer skid
|
|
* 3. Check that the cert algrithm matches the subject algorithm
|
|
* 4. Verify the signature of certificate @cert is using the public key of
|
|
* @issuer.
|
|
*/
|
|
static int check_host_key_issued(X509 *cert, X509 *issuer, GError **error)
|
|
{
|
|
const X509_NAME *issuer_subject = X509_get_subject_name(issuer);
|
|
const X509_NAME *cert_issuer = X509_get_issuer_name(cert);
|
|
g_autoptr(AUTHORITY_KEYID) akid = NULL;
|
|
|
|
/* We cannot use X509_NAME_cmp() because it considers the order of the
|
|
* X509_NAME_Entries.
|
|
*/
|
|
if (!own_X509_NAME_equal(issuer_subject, cert_issuer)) {
|
|
g_autofree char *issuer_subject_str = pv_X509_NAME_oneline(issuer_subject);
|
|
g_autofree char *cert_issuer_str = pv_X509_NAME_oneline(cert_issuer);
|
|
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_CERT_SUBJECT_ISSUER_MISMATCH,
|
|
_("Subject issuer mismatch:\n'%s'\n'%s'"), issuer_subject_str,
|
|
cert_issuer_str);
|
|
return -1;
|
|
}
|
|
|
|
akid = X509_get_ext_d2i(cert, NID_authority_key_identifier, NULL, NULL);
|
|
if (akid && X509_check_akid(issuer, akid) != X509_V_OK) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_SKID_AKID_MISMATCH,
|
|
_("AKID mismatch"));
|
|
return -1;
|
|
}
|
|
|
|
if (check_signature_algo_match(X509_get0_pubkey(issuer), cert, error) < 0)
|
|
return -1;
|
|
|
|
if (X509_verify(cert, X509_get0_pubkey(issuer)) != 1) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_CERT_SIGNATURE_INVALID,
|
|
_("Signature verification failed"));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static gboolean is_cert_revoked(X509 *cert, X509_CRL *crl)
|
|
{
|
|
X509_REVOKED *revoked = NULL;
|
|
int rc;
|
|
|
|
if (!cert || !crl)
|
|
g_abort();
|
|
|
|
rc = X509_CRL_get0_by_serial(crl, &revoked, (ASN1_INTEGER *)X509_get0_serialNumber(cert));
|
|
if (rc == 0)
|
|
return FALSE;
|
|
|
|
if (revoked)
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/* Assumptions are that the issuer_crt and issuer_crl is a trusted IBM Z
|
|
* signing certificate/revocation list. This function verifies a host-key
|
|
* document. To do so multiple steps are required:
|
|
*
|
|
* 1. issuer(host_key) == subject(issuer_crt)
|
|
* 2. Signature verification
|
|
* 3. @host_key must not be expired
|
|
* 4. @host_key must not be revoked
|
|
*/
|
|
int pv_verify_host_key(X509 *host_key, GSList *issuer_pairs, int verify_flags, int level,
|
|
GError **error)
|
|
{
|
|
const int exp_security_bits = security_level_to_bits(level);
|
|
EVP_PKEY *pkey;
|
|
gboolean successfully_checked = FALSE;
|
|
int pkey_security_bits;
|
|
|
|
g_assert(host_key);
|
|
pkey = X509_get0_pubkey(host_key);
|
|
|
|
if (!pkey) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INTERNAL,
|
|
_("failed to retrieve public key"));
|
|
return -1;
|
|
}
|
|
|
|
/* check key level, if necessary */
|
|
pkey_security_bits = EVP_PKEY_security_bits(pkey);
|
|
if (exp_security_bits > 0 && pkey_security_bits < exp_security_bits) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_VERIFICATION_FAILED,
|
|
_("not enough bits of security (%d, %d expected)"), pkey_security_bits,
|
|
exp_security_bits);
|
|
return -1;
|
|
}
|
|
|
|
if (!(verify_flags & X509_V_FLAG_NO_CHECK_TIME)) {
|
|
const ASN1_TIME *last = X509_get0_notBefore(host_key);
|
|
const ASN1_TIME *next = X509_get0_notAfter(host_key);
|
|
|
|
if (!last || !next || check_validity_period(last, next)) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INVALID_VALIDITY_PERIOD,
|
|
_("validity period is not valid"));
|
|
return -1;
|
|
}
|
|
} else {
|
|
verify_flags &= ~X509_V_FLAG_NO_CHECK_TIME;
|
|
}
|
|
|
|
/* Verify that the host_key was issued by a certificate and that it
|
|
* wasn't revoked.
|
|
*/
|
|
for (GSList *iterator = issuer_pairs; iterator; iterator = iterator->next) {
|
|
const PvX509Pair *pair = iterator->data;
|
|
STACK_OF_X509_CRL *issuer_crls = NULL;
|
|
X509 *issuer_cert = NULL;
|
|
|
|
g_assert(pair);
|
|
|
|
issuer_cert = pair->cert;
|
|
issuer_crls = pair->crls;
|
|
|
|
g_assert(issuer_cert);
|
|
|
|
/* Verify that the issuer(host_key) == subject(issuer_cert) and
|
|
* that the signature is valid
|
|
*/
|
|
if (check_host_key_issued(host_key, issuer_cert, NULL) < 0)
|
|
continue;
|
|
|
|
/* Check against CRL */
|
|
if (verify_flags & X509_V_FLAG_CRL_CHECK) {
|
|
gboolean crl_checked = FALSE;
|
|
|
|
verify_flags &= ~X509_V_FLAG_CRL_CHECK;
|
|
for (int i = 0; i < sk_X509_CRL_num(issuer_crls); i++) {
|
|
X509_CRL *issuer_crl = sk_X509_CRL_value(issuer_crls, i);
|
|
|
|
g_assert(issuer_crl);
|
|
|
|
if (is_cert_revoked(host_key, issuer_crl)) {
|
|
g_set_error(error, PV_CERT_ERROR,
|
|
PV_CERT_ERROR_CERT_REVOKED,
|
|
_("certificate revoked"));
|
|
return -1;
|
|
}
|
|
|
|
crl_checked = TRUE;
|
|
}
|
|
|
|
if (!crl_checked) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_NO_CRL,
|
|
_("no valid CRL found"));
|
|
return -1;
|
|
}
|
|
successfully_checked = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!successfully_checked) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_NO_ISSUER_IBM_Z_FOUND,
|
|
_("no IBM Z signing key that issued this host-key document found"));
|
|
return -1;
|
|
}
|
|
|
|
/* were some unsupported flags specified? */
|
|
g_assert(verify_flags == 0);
|
|
return 0;
|
|
}
|
|
|
|
int pv_verify_cert(X509_STORE_CTX *ctx, X509 *cert, GError **error)
|
|
{
|
|
int rc;
|
|
|
|
pv_wrapped_g_assert(cert);
|
|
pv_wrapped_g_assert(ctx);
|
|
|
|
X509_STORE_CTX_set_cert(ctx, cert);
|
|
rc = X509_verify_cert(ctx);
|
|
if (rc != 1) {
|
|
X509 *tmp_cert = NULL;
|
|
|
|
tmp_cert = pv_X509_STORE_CTX_get_current_cert(ctx);
|
|
if (tmp_cert) {
|
|
g_autofree char *subj_name =
|
|
pv_X509_NAME_oneline(X509_get_subject_name(tmp_cert));
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_VERIFICATION_FAILED,
|
|
_("failed to verify certificate '%s': %s"), subj_name,
|
|
X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx)));
|
|
} else {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_VERIFICATION_FAILED,
|
|
_("failed to verify certificate: %s"),
|
|
X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx)));
|
|
}
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Verify that: subject(issuer) == issuer(crl) and SKID(issuer) == AKID(crl) */
|
|
static int check_crl_issuer(X509_CRL *crl, X509 *issuer, GError **error)
|
|
{
|
|
const X509_NAME *crl_issuer = X509_CRL_get_issuer(crl);
|
|
const X509_NAME *issuer_subject = X509_get_subject_name(issuer);
|
|
AUTHORITY_KEYID *akid = NULL;
|
|
|
|
if (!own_X509_NAME_equal(issuer_subject, crl_issuer)) {
|
|
g_autofree char *issuer_subject_str = pv_X509_NAME_oneline(issuer_subject);
|
|
g_autofree char *crl_issuer_str = pv_X509_NAME_oneline(crl_issuer);
|
|
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_CRL_SUBJECT_ISSUER_MISMATCH,
|
|
_("issuer mismatch:\n%s\n%s"), issuer_subject_str, crl_issuer_str);
|
|
return -1;
|
|
}
|
|
|
|
/* If AKID(@crl) is specified it must match with SKID(@issuer) */
|
|
akid = X509_CRL_get_ext_d2i(crl, NID_authority_key_identifier, NULL, NULL);
|
|
if (akid && X509_check_akid(issuer, akid) != X509_V_OK) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_SKID_AKID_MISMATCH,
|
|
_("AKID mismatch"));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int pv_verify_crl(X509_CRL *crl, X509 *cert, int verify_flags, GError **error)
|
|
{
|
|
EVP_PKEY *pkey = X509_get0_pubkey(cert);
|
|
|
|
g_assert(crl);
|
|
|
|
if (!pkey) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INTERNAL,
|
|
_("failed to retrieve public key from the certificate"));
|
|
return -1;
|
|
}
|
|
|
|
/* check that the @crl issuer matches with the subject name of @cert*/
|
|
if (check_crl_issuer(crl, cert, error) < 0)
|
|
return -1;
|
|
|
|
/* verify the validity period of the CRL */
|
|
if (!(verify_flags & X509_V_FLAG_NO_CHECK_TIME)) {
|
|
const ASN1_TIME *last = X509_CRL_get0_lastUpdate(crl);
|
|
const ASN1_TIME *next = X509_CRL_get0_nextUpdate(crl);
|
|
|
|
if (!last || !next || check_validity_period(last, next)) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INVALID_VALIDITY_PERIOD,
|
|
_("validity period is not valid"));
|
|
return -1;
|
|
}
|
|
} else {
|
|
verify_flags &= ~X509_V_FLAG_NO_CHECK_TIME;
|
|
}
|
|
|
|
/* verify the signature */
|
|
if (X509_CRL_verify(crl, pkey) != 1) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_CRL_SIGNATURE_INVALID,
|
|
_("signature is not valid"));
|
|
return -1;
|
|
}
|
|
g_assert(verify_flags == 0);
|
|
return 0;
|
|
}
|
|
|
|
int pv_check_chain_parameters(const STACK_OF_X509 *chain, GError **error)
|
|
{
|
|
const X509_NAME *ca_x509_subject = NULL;
|
|
g_autofree char *ca_subject = NULL;
|
|
int len = sk_X509_num(chain);
|
|
X509 *ca = NULL;
|
|
|
|
if (len < 2) {
|
|
g_set_error(
|
|
error, PV_CERT_ERROR, PV_CERT_ERROR_INTERNAL,
|
|
_("there must be at least one root and one leaf certificate in the chain of trust"));
|
|
return -1;
|
|
}
|
|
|
|
/* get the root certificate of the chain of trust */
|
|
ca = sk_X509_value(chain, len - 1);
|
|
if (!ca) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_INTERNAL,
|
|
_("no root certificate found"));
|
|
return -1;
|
|
}
|
|
|
|
ca_x509_subject = X509_get_subject_name(ca);
|
|
if (!ca_x509_subject) {
|
|
g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL,
|
|
_("subject of the root CA cannot be retrieved"));
|
|
return -1;
|
|
}
|
|
|
|
ca_subject = pv_X509_NAME_oneline(ca_x509_subject);
|
|
if (!ca_subject) {
|
|
g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL,
|
|
_("subject name of the root CA cannot be retrieved"));
|
|
return -1;
|
|
}
|
|
g_info(_("Root CA used: '%s'"), ca_subject);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Given a certificate @cert try to find valid revocation lists in @ctx. If no
|
|
* valid CRL was found NULL is returned.
|
|
*/
|
|
STACK_OF_X509_CRL *pv_store_ctx_find_valid_crls(X509_STORE_CTX *ctx, X509 *cert, GError **error)
|
|
{
|
|
g_autoptr(STACK_OF_X509_CRL) ret = NULL;
|
|
const int verify_flags = 0;
|
|
X509_NAME *subject = NULL;
|
|
|
|
pv_wrapped_g_assert(ctx);
|
|
pv_wrapped_g_assert(cert);
|
|
|
|
subject = X509_get_subject_name(cert);
|
|
if (!subject) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_MALFORMED_CERTIFICATE,
|
|
_("certificate is malformed"));
|
|
return NULL;
|
|
}
|
|
|
|
ret = pv_X509_STORE_CTX_get1_crls(ctx, subject);
|
|
if (!ret) {
|
|
/* Workaround to fix the mismatch between issuer name of the
|
|
* IBM Z signing CRLs and the IBM Z signing key subject name.
|
|
*/
|
|
g_autoptr(X509_NAME) broken_subject = pv_c2b_name(subject);
|
|
|
|
ret = pv_X509_STORE_CTX_get1_crls(ctx, broken_subject);
|
|
if (!ret) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_NO_CRL, _("no CRL found"));
|
|
g_info("ERROR: %s", (*error)->message);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
/* Filter out non-valid CRLs for @cert */
|
|
for (int i = 0; i < sk_X509_CRL_num(ret); i++) {
|
|
X509_CRL *crl = sk_X509_CRL_value(ret, i);
|
|
|
|
g_assert(crl);
|
|
|
|
/* If @crl is not valid remove it from the array and log a
|
|
* warning.
|
|
*/
|
|
if (pv_verify_crl(crl, cert, verify_flags, error) < 0) {
|
|
g_assert(error);
|
|
g_warning(_("CRL is not valid: %s"), (*error)->message);
|
|
g_clear_error(error);
|
|
|
|
/* Remove this certificate from the list and change i-- as the
|
|
* array has changed - this is not beautfiul, but right now the
|
|
* easiest solution I came up with
|
|
*/
|
|
if (sk_X509_CRL_delete(ret, i--) != crl)
|
|
g_abort();
|
|
|
|
g_clear_pointer(&crl, X509_CRL_free);
|
|
}
|
|
}
|
|
|
|
if (sk_X509_CRL_num(ret) < 1) {
|
|
g_set_error(error, PV_CERT_ERROR, PV_CERT_ERROR_NO_CRL, _("no valid CRL found"));
|
|
return NULL;
|
|
}
|
|
return g_steal_pointer(&ret);
|
|
}
|
|
|
|
/*
|
|
* Finds the IBM signing key in the stack.
|
|
* Error out, if there is not exactly one IBM signing key.
|
|
*/
|
|
static STACK_OF_X509 *get_ibm_signing_certs(STACK_OF_X509 *certs, GError **error)
|
|
{
|
|
g_autoptr(STACK_OF_X509) ibm_signing_certs = NULL;
|
|
int ibm_signing_certs_count;
|
|
|
|
/* Find all IBM Z signing keys and remove them from the chain as we
|
|
* have to verify that they're valid. The last step of the chain of
|
|
* trust verification must be done manually, as the IBM Z signing keys
|
|
* are not marked as (intermediate) CA and therefore the standard
|
|
* `X509_verify_cert` function of OpenSSL cannot be used to verify the
|
|
* actual host-key documents.
|
|
*/
|
|
ibm_signing_certs = pv_remove_ibm_signing_certs(certs);
|
|
ibm_signing_certs_count = sk_X509_num(ibm_signing_certs);
|
|
if (ibm_signing_certs_count < 1) {
|
|
g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_NO_IBM_Z_SIGNING_KEY,
|
|
_("Specify at least one IBM Z signing key"));
|
|
return NULL;
|
|
} else if (ibm_signing_certs_count > 1) {
|
|
g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_NO_IBM_Z_SIGNING_KEY,
|
|
_("Specify only one IBM Z signing key"));
|
|
return NULL;
|
|
}
|
|
g_assert(ibm_signing_certs_count == 1);
|
|
|
|
return g_steal_pointer(&ibm_signing_certs);
|
|
}
|
|
|
|
static gboolean download_crls(X509_STORE *trusted, PvCertWithPathList *host_key_certs_with_path,
|
|
GError **error)
|
|
{
|
|
g_autoptr(STACK_OF_X509_CRL) downloaded_ibm_signing_crls = NULL;
|
|
|
|
/* Set up the download routine for the lookup of CRLs. */
|
|
pv_store_setup_crl_download(trusted);
|
|
|
|
/* Try to download the CRLs of the IBM Z signing certificates
|
|
* specified in the host-key documents. Ignore download errors
|
|
* as it's still possible that a CRL is specified via command
|
|
* line.
|
|
*/
|
|
downloaded_ibm_signing_crls = pv_try_load_crls_by_certs(host_key_certs_with_path);
|
|
|
|
/* Add the downloaded CRLs to the store so they can be used for
|
|
* the verification later.
|
|
*/
|
|
for (int i = 0; i < sk_X509_CRL_num(downloaded_ibm_signing_crls); i++) {
|
|
X509_CRL *crl = sk_X509_CRL_value(downloaded_ibm_signing_crls, i);
|
|
|
|
if (X509_STORE_add_crl(trusted, crl) != 1) {
|
|
g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL,
|
|
_("failed to load CRL"));
|
|
return FALSE;
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean pv_verify_host_key_doc(PvCertWithPathList *host_key_certs_with_path, X509_STORE *trusted,
|
|
STACK_OF_X509 *untrusted_certs, gboolean online, GError **error)
|
|
{
|
|
g_autoslist(PvX509Pair) ibm_z_pairs = NULL;
|
|
g_autoptr(STACK_OF_X509) ibm_signing_certs = NULL;
|
|
g_autoptr(X509_STORE_CTX) ctx = NULL;
|
|
|
|
pv_wrapped_g_assert(host_key_certs_with_path);
|
|
pv_wrapped_g_assert(trusted);
|
|
pv_wrapped_g_assert(untrusted_certs);
|
|
|
|
if (online && !download_crls(trusted, host_key_certs_with_path, error))
|
|
return -1;
|
|
|
|
/* Find all IBM Z signing keys and remove them from the chain as we
|
|
* have to verify that they're valid. The last step of the chain of
|
|
* trust verification must be done manually, as the IBM Z signing keys
|
|
* are not marked as (intermediate) CA and therefore the standard
|
|
* `X509_verify_cert` function of OpenSSL cannot be used to verify the
|
|
* actual host-key documents.
|
|
*/
|
|
ibm_signing_certs = get_ibm_signing_certs(untrusted_certs, error);
|
|
if (!ibm_signing_certs)
|
|
return -1;
|
|
|
|
if (pv_store_set_verify_param(trusted, error) < 0)
|
|
return -1;
|
|
|
|
ctx = pv_create_store_ctx(trusted, untrusted_certs, error);
|
|
if (!ctx)
|
|
return -1;
|
|
/*
|
|
* Get all IBM-signing-[key,crls] pairs.
|
|
* NOTE: Currently there is only one signing-key allowed
|
|
*/
|
|
for (int i = 0; i < sk_X509_num(ibm_signing_certs); i++) {
|
|
g_autoptr(X509) ibm_signing_cert = sk_X509_pop(ibm_signing_certs);
|
|
g_autoptr(STACK_OF_X509_CRL) ibm_signing_crls = NULL;
|
|
PvX509Pair *ibm_z_pair = NULL;
|
|
|
|
/*
|
|
* Get CRLs for the IBM signing cert
|
|
*/
|
|
ibm_signing_crls = pv_store_ctx_find_valid_crls(ctx, ibm_signing_cert, error);
|
|
if (!ibm_signing_crls) {
|
|
g_prefix_error(error, _("IBM Z signing key: "));
|
|
return -1;
|
|
}
|
|
|
|
/* build the pair and add it to the list */
|
|
ibm_z_pair = pv_x509_pair_new_take(&ibm_signing_cert, &ibm_signing_crls);
|
|
g_assert(!ibm_signing_cert);
|
|
g_assert(!ibm_signing_crls);
|
|
ibm_z_pairs = g_slist_append(ibm_z_pairs, ibm_z_pair);
|
|
}
|
|
|
|
/* Verify host-key documents by using the IBM Z signing
|
|
* certificates and the corresponding certificate revocation
|
|
* lists.
|
|
*/
|
|
for (GSList *iterator = host_key_certs_with_path; iterator; iterator = iterator->next) {
|
|
PvX509WithPath *host_key_with_path = iterator->data;
|
|
const char *host_key_path = host_key_with_path->path;
|
|
X509 *host_key = host_key_with_path->cert;
|
|
int flags = X509_V_FLAG_CRL_CHECK;
|
|
|
|
if (pv_verify_host_key(host_key, ibm_z_pairs, flags, PV_CERTS_SECURITY_LEVEL,
|
|
error) < 0) {
|
|
g_prefix_error(error, "'%s': ", host_key_path);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/* Verify that all IBM Z signing keys are trustable.
|
|
* For this we must check:
|
|
*
|
|
* 1. Can a chain of trust be established ending in a root CA
|
|
* 2. Is the correct root CA used? It has either to be the
|
|
* System CA or the root CA specified via command line.
|
|
*/
|
|
for (GSList *iterator = ibm_z_pairs; iterator; iterator = iterator->next) {
|
|
const PvX509Pair *ibm_z_pair = iterator->data;
|
|
|
|
if (pv_verify_cert(ctx, ibm_z_pair->cert, error) < 0)
|
|
return -1;
|
|
if (pv_check_chain_parameters(X509_STORE_CTX_get0_chain(ctx), error) < 0)
|
|
return -1;
|
|
/* re-init ctx for the next verification */
|
|
X509_STORE_CTX_cleanup(ctx);
|
|
if (pv_init_store_ctx(ctx, trusted, untrusted_certs, error) != 0)
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int pv_verify_host_key_docs_by_path(char **host_key_paths, char *optional_root_ca_path,
|
|
char **crl_paths, char **untrusted_cert_paths, gboolean online,
|
|
GError **error)
|
|
{
|
|
g_autoslist(PvX509WithPath) untrusted_certs_with_path = NULL, host_key_certs = NULL;
|
|
g_autoptr(STACK_OF_X509) untrusted_certs = NULL;
|
|
g_autoptr(X509_STORE) trusted = NULL;
|
|
|
|
pv_wrapped_g_assert(host_key_paths);
|
|
pv_wrapped_g_assert(untrusted_cert_paths);
|
|
|
|
/* Load trusted root CAs of the system if and only if @root_ca_path is
|
|
* NULL, otherwise use the root CA specified by @root_ca_path.
|
|
*/
|
|
trusted = pv_store_setup(optional_root_ca_path, crl_paths, untrusted_cert_paths, error);
|
|
if (!trusted)
|
|
return -1;
|
|
|
|
/* Load all untrusted certificates (e.g. IBM Z signing key and
|
|
* intermediate CA) that are required to establish a chain of
|
|
* trust starting from the host-key document up to the root CA (if not
|
|
* otherwise specified that can be one of the system wide installed
|
|
* root CAs, e.g. DigiCert).
|
|
*/
|
|
untrusted_certs_with_path = pv_load_certificates(untrusted_cert_paths, error);
|
|
if (!untrusted_certs_with_path)
|
|
return -1;
|
|
/* Convert to STACK_OF(X509) */
|
|
untrusted_certs = pv_get_x509_stack(untrusted_certs_with_path);
|
|
|
|
host_key_certs = pv_load_certificates(host_key_paths, error);
|
|
if (!host_key_certs)
|
|
return -1;
|
|
|
|
return pv_verify_host_key_doc(host_key_certs, trusted, untrusted_certs, online, error);
|
|
}
|