diff --git a/Makefile b/Makefile index af66561e..e46955c2 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ include common.mak # BASELIB_DIRS = libutil libseckey LIB_DIRS = libvtoc libzds libdasd libvmdump libccw libvmcp libekmfweb \ - libkmipclient libcpumf libap + libkmipclient libcpumf libap libpv TOOL_DIRS = zipl zdump fdasd dasdfmt dasdview tunedasd \ tape390 osasnmpd qetharp ip_watcher qethconf scripts zconf \ vmconvert vmcp man mon_tools dasdinfo vmur cpuplugd ipl_tools \ diff --git a/common.mak b/common.mak index 8113560f..1296f248 100644 --- a/common.mak +++ b/common.mak @@ -387,6 +387,10 @@ $(rootdir)/libap/libap.a: $(rootdir)/libap $(MAKE) -C $(rootdir)/libap/ libap.a .PHONY: $(rootdir)/libap +$(rootdir)/libpv/libpv.a: $(rootdir)/libpv + $(MAKE) -C $(rootdir)/libpv libpv.a +.PHONY: $(rootdir)/libpv + $(rootdir)/zipl/boot/data.o: $(MAKE) -C $(rootdir)/zipl/boot/ data.o diff --git a/include/libpv/cert.h b/include/libpv/cert.h new file mode 100644 index 00000000..3d2ccf0c --- /dev/null +++ b/include/libpv/cert.h @@ -0,0 +1,438 @@ +/* + * 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. + */ +#ifndef LIBPV_CERT_H +#define LIBPV_CERT_H + +#include +#include + +#include "libpv/common.h" + +#define PV_IBM_Z_SUBJECT_COMMON_NAME "International Business Machines Corporation" +#define PV_IBM_Z_SUBJECT_COUNTRY_NAME "US" +#define PV_IBM_Z_SUBJECT_LOCALITY_NAME "Poughkeepsie" +#define PV_IBM_Z_SUBJECT_ORGANIZATIONAL_UNIT_NAME_SUFFIX "Key Signing Service" +#define PV_IBM_Z_SUBJECT_ORGANIZATION_NAME "International Business Machines Corporation" +#define PV_IBM_Z_SUBJECT_STATE "New York" +#define PV_IMB_Z_SUBJECT_ENTRY_COUNT 6 + +/* Minimum security level for the keys/certificates used to establish a chain of + * trust (see https://www.openssl.org/docs/man1.1.1/man3/X509_VERIFY_PARAM_set_auth_level.html + * for details). + */ +#define PV_CERTS_SECURITY_LEVEL 2 + +/** pv_cert_init: + * + * Should not be called by user. + * Use pv_init() instead which + * calls this function during creation. + * + * Sets up data structures for caching CRLs. + */ +void pv_cert_init(void); + +/** pv_cert_cleanup: + * + * Should not be called by user. + * Use pv_cleanup() instead which + * calls this function during creation. + * + * Cleans up data structures for caching CRLs. + */ +void pv_cert_cleanup(void); + +#define PV_CERT_ERROR g_quark_from_static_string("pv-cert-error-quark") +typedef enum { + PV_CERT_ERROR_CERT_REVOKED, + PV_CERT_ERROR_CERT_SIGNATURE_INVALID, + PV_CERT_ERROR_CERT_SUBJECT_ISSUER_MISMATCH, + PV_CERT_ERROR_CRL_DOWNLOAD_FAILED, + PV_CERT_ERROR_CRL_SIGNATURE_INVALID, + PV_CERT_ERROR_CRL_SUBJECT_ISSUER_MISMATCH, + PV_CERT_ERROR_FAILED_DOWNLOAD_CRL, + PV_CERT_ERROR_INTERNAL, + PV_CERT_ERROR_INVALID_PARM, + PV_CERT_ERROR_INVALID_SIGNATURE_ALGORITHM, + PV_CERT_ERROR_INVALID_VALIDITY_PERIOD, + PV_CERT_ERROR_LOAD_CRL, + PV_CERT_ERROR_LOAD_DEFAULT_CA, + PV_CERT_ERROR_LOAD_ROOT_CA, + PV_CERT_ERROR_MALFORMED_CERTIFICATE, + PV_CERT_ERROR_MALFORMED_ROOT_CA, + PV_CERT_ERROR_NO_CRL, + PV_CERT_ERROR_NO_CRLDP, + PV_CERT_ERROR_NO_ISSUER_IBM_Z_FOUND, + PV_CERT_ERROR_NO_PUBLIC_KEY, + PV_CERT_ERROR_READ_CERTIFICATE, + PV_CERT_ERROR_READ_CRL, + PV_CERT_ERROR_SIGNATURE_ALGORITHM_MISMATCH, + PV_CERT_ERROR_SKID_AKID_MISMATCH, + PV_CERT_ERROR_VERIFICATION_FAILED, + PV_CERT_ERROR_WRONG_CA_USED, +} PvCertErrors; + +/** PvX509WithPath - X509 certificate associated with a path + */ +typedef struct { + X509 *cert; + char *path; +} PvX509WithPath; + +/** pv_x509_with_path_new: + * + * @cert: X509 certificate + * @path: Path of that X509 certificate + * + * Returns: (nullable) (transfer full): new X509 with path + */ +PvX509WithPath *pv_x509_with_path_new(X509 *cert, const char *path); + +/** pv_x509_with_path_free: + * + * Frees the path and the PvX509WithPath; Decreases the refcount of the X509 + */ +void pv_x509_with_path_free(PvX509WithPath *cert); + +typedef STACK_OF(DIST_POINT) STACK_OF_DIST_POINT; +typedef STACK_OF(X509) STACK_OF_X509; +typedef STACK_OF(X509_CRL) STACK_OF_X509_CRL; +typedef GSList PvCertWithPathList; + +typedef struct { + X509 *cert; + STACK_OF_X509_CRL *crls; +} PvX509Pair; + +/** pv_x509_pair_new_take: + * @cert: ptr to X509 + * @crls: ptr to CRLs + * + * Takes a X509 and the associated CRLs and builds a pair. + * Both, *cert and *crls will be NULL afterwards, and owned by the pair. + * + * Returns: (nullable) (transfer full): New PvX509Pair + */ +PvX509Pair *pv_x509_pair_new_take(X509 **cert, STACK_OF_X509_CRL **crls); + +/** pv_x509_pair_free: + * + * Decreases the refcount of the X509 and crls. + * Frees the PvX509Pair. + */ +void pv_x509_pair_free(PvX509Pair *pair); + +void STACK_OF_DIST_POINT_free(STACK_OF_DIST_POINT *stack); +void STACK_OF_X509_free(STACK_OF_X509 *stack); +void STACK_OF_X509_CRL_free(STACK_OF_X509_CRL *stack); + +/** pv_x509_from_pem_der_data: + * + * @data: GBytes containing the cert in PEM format + * @error: return location for a #GError + * + * Returns: (nullable) (transfer full): X509 cert + */ +X509 *pv_x509_from_pem_der_data(GBytes *data, GError **error); + +/** pv_x509_get_ec_pubkey: + * + * @cert: X509 to extract elliptic curve pubkey from + * @nid: numerical identifier of the expected curve + * @error: return location for a #GError + * + * Returns: (nullable) (transfer full): corresponding pupkey for the given certificate + */ +EVP_PKEY *pv_x509_get_ec_pubkey(X509 *cert, int nid, GError **error); + +/** pv_get_ec_pubkeys: + * + * @certs_with_path: List of PvX509WithPath + * @nid: numerical identifier of the expected curve + * @error: return location for a #GError + * + * Returns: (nullable) (transfer full): List of corresponding public keys for the given certificate + */ +GSList *pv_get_ec_pubkeys(PvCertWithPathList *certs_with_path, int nid, GError **error); + +/* pv_load_certificates: + * + * @cert_paths: list of cert paths. + * @error: return location for a #GError + * + * @cert_paths must contain at least one element, otherwise an error is + * reported. + * + * Returns: (nullable) (transfer full): List of PvX509WithPath corresponding to the given paths + */ +PvCertWithPathList *pv_load_certificates(char **cert_paths, GError **error); + +/* pv_load_first_cert_from_file: + * + * @path: location of the x509 + * @error: return location for a #GError + * + * This function reads in only the first certificate and ignores all other. This + * is only relevant for the PEM file format. For the host-key document and the + * root CA this behavior is expected. + * + * Returns: (nullable) (transfer full): PvX509WithPath corresponding to the given path + */ +X509 *pv_load_first_cert_from_file(const char *path, GError **error); + +/* pv_load_first_crl_from_file: + * + * @path: location of the x509 CRL + * @error: return location for a #GError + * + * This function reads in only the first CRL and ignores all other. This + * is only relevant for the PEM file format. + * + * Returns: (nullable) (transfer full): X509_CRL corresponding to the given path + */ +X509_CRL *pv_load_first_crl_from_file(const char *path, GError **error); + +/** pv_store_setup_crl_download: + * + * @st: X509_STORE + */ +void pv_store_setup_crl_download(X509_STORE *st); + +/** pv_load_first_crl_by_cert: + * @cert: X509 to specify the download location. + * @error: return location for a #GError + * + * This function returns the first X509_CRL found from the CRL distribution + * points specified in @cert. + * + * Returns: (nullable) (transfer full): x509 CRL corresponding to the given X509 + */ +X509_CRL *pv_load_first_crl_by_cert(X509 *cert, GError **error); + +/** pv_try_load_crls_by_certs: + * + * @certs_with_path: List of PvX509WithPath + * + * Returns: (nullable) (transfer full): Stack of CRLs corresponding to the given X509 + */ +STACK_OF_X509_CRL *pv_try_load_crls_by_certs(PvCertWithPathList *certs_with_path); + +/** pv_store_setup: + * + * @root_ca_path: Location of the rootCA or NULL if SystemRoot CA shall be used + * @crl_paths: List of CRL paths or NULL + * @cert_with_crl_paths: List of (untrusted) X509 paths + * @error: return location for a #GError + * + * The untrusted certs need to be verified before actually verifying a Host Key Document. + * + * Returns: (nullable) (transfer full): X509_store with given input data. + * + */ +X509_STORE *pv_store_setup(char *root_ca_path, char **crl_paths, char **cert_with_crl_paths, + GError **error); + +/** pv_get_x509_stack: + * + * x509_with_path_list: list of PvX509WithPath + * + * Returns: (nullable) (transfer full): Stack of X509 corresponding to the given x509 with path + */ +STACK_OF_X509 *pv_get_x509_stack(const GSList *x509_with_path_list); + +/** pv_init_store_ctx: + * + * @ctx: a uninitialized Store CTX + * @trusted: X509_STORE with a trusted rootCA + * @chain: untrusted X509s + * @error: return location for a #GError + * + * Can be called multiple times on the same context if X509_STORE_CTX_cleanup(ctx) + * was called before. + * + * Returns: + * 0 on success + * -1 in failure + */ +int pv_init_store_ctx(X509_STORE_CTX *ctx, X509_STORE *trusted, STACK_OF_X509 *chain, + GError **error) PV_NONNULL(1, 2, 3); + +/** pv_init_store_ctx: + * + * @trusted: X509_STORE with a trusted rootCA + * @chain: untrusted X509s + * @error: return location for a #GError + * + * Returns: (nullable) (transfer full): X509_STORE_CTX setup with the input data + */ +X509_STORE_CTX *pv_create_store_ctx(X509_STORE *trusted, STACK_OF_X509 *chain, GError **error) + PV_NONNULL(1, 2); +/** pv_remove_ibm_signing_certs: + * + * @certs: Stack of X509s + * + * Returns: (transfer full): + * List of all IBM Z signing key certificates in @certs and remove them + * from the chain. + * Empty stack if no IBM Z signing key is found. + */ +STACK_OF_X509 *pv_remove_ibm_signing_certs(STACK_OF_X509 *certs); + +/** pv_c2b_name: + * + * Workaround to fix the mismatch between issuer name of the + * IBM Z signing CRLs and the IBM Z signing key subject name. + * + * In RFC 5280 the attributes of a (subject/issuer) name is not mandatory + * ordered. The problem is that our certificates are not consistent in the order + * (see https://tools.ietf.org/html/rfc5280#section-4.1.2.4 for details). + * + * This function tries to reorder the name attributes such that + * further OpenSSL calls can work with it. The caller is + * responsible to free the returned value. + */ +X509_NAME *pv_c2b_name(const X509_NAME *name); + +/** pv_verify_host_key: + * + * @host_key: X509 to be verified + * @issuer_pairs: IBM signing key X509+CRLs Pairs used for verification + * @level: Security level. see PV_CERTS_SECURITY_LEVEL + * @error: return location for a #GError + * + * Returns: + * 0 if Host key could be verified with one of the IBM signing keys + * -1 if no IBM signing key could verify the authenticity of the given host key + * + */ +int pv_verify_host_key(X509 *host_key, GSList *issuer_pairs, int verify_flags, int level, + GError **error); + +/** pv_verify_cert: + * + * @ctx: trusted store ctx used for verification + * @cert: X509 to be verified + * @error: return location for a #GError + * + * Cannot be used to verify host keys with IBM signing keys, as IBM signing + * keys are no intermediate CAs. Use pv_verify_host_key() instead. + * + * Returns: + * 0 if @cert could be verified + * -1 if @cert could not be verified + */ +int pv_verify_cert(X509_STORE_CTX *ctx, X509 *cert, GError **error) PV_NONNULL(1, 2); + +/** pv_check_crl_valid_for_cert: + * + * @crl: CRL to be verified + * @cert: Cert that probably issued the given CRL + * @verify_flags: X509 Verification flags (X509_V_FLAG_) + * @error: return location for a #GError + * + * Verify whether a revocation list @crl is valid and is issued by @cert. For + * this multiple steps must be done: + * + * 1. verify issuer of the CRL matches with the suject name of @cert + * 2. verify the validity period of the CRL + * 3. verify the signature of the CRL + * + * Important: This function does not verify whether @cert is allowed to issue a + * CRL. + * + * Returns: + * 0 if @crl is valid and issued by @cert + * -1 otherwise + */ +int pv_verify_crl(X509_CRL *crl, X509 *cert, int verify_flags, GError **error); + +/** pv_check_chain_parameters: + * + * @chain: chain of trust to be validated + * @error: return location for a #GError + * + * Verifies that chain has at least a RootCA ans intermediate CA + * and logs the used ROD CA subject + * + * Returns: + * 0 @chain is valid + * -1 otherwise + */ +int pv_check_chain_parameters(const STACK_OF_X509 *chain, GError **error); + +/** pv_store_set_verify_param: + * + * @store: X509_STORE to set parameters + * @error: return location for a #GError + * + * Returns: + * 0 on success + * -1 on failure + */ +int pv_store_set_verify_param(X509_STORE *store, GError **error); + +/** pv_store_ctx_find_valid_crls: + * + * @ctx: STORE_CTX for searching CRLs + * @cert: X509 to match CRLs aggainst + * @error: return location for a #GError + * + * Returns: (nullable) (transfer full): STACK of CRLs related to given @crl fin @ctx + */ +STACK_OF_X509_CRL *pv_store_ctx_find_valid_crls(X509_STORE_CTX *ctx, X509 *cert, GError **error) + PV_NONNULL(1, 2); + +/** pv_verify_host_key_doc: + * + * @host_key_certs_with_path: X509s to be verified + * @trusted. X509_STORE with a rusted RootCA + * @untrusted_certs: STACK OF untrusted X509s + * @online: true if CRLs shall be downloaded + * @error: return location for a #GError + * + * Returns: + * 0 if all given HKDs could be verified using the chain of trust. + * -1 otherwise + */ +int pv_verify_host_key_doc(PvCertWithPathList *host_key_certs_with_path, X509_STORE *trusted, + STACK_OF_X509 *untrusted_certs, gboolean online, GError **error) + PV_NONNULL(1, 2, 3); + +/** pv_verify_host_key_docs_by_path: + * + * @host_key_paths: locations of X509 to be verified + * @optional_root_ca_path: rootCA location or NULL if Default shall be used + * @optional_crl_paths: locations of CRLs or NULL + * @untrusted_cert_paths: locations of IntermediateCAs including the IBM signing key + * @online: true if CRLs shall be downloaded + * @error: return location for a #GError + * + * Returns: + * 0 if all given HKDs could be verfied using the chain of trust. + * -1 otherwise + */ +int pv_verify_host_key_docs_by_path(char **host_key_paths, char *optional_root_ca_path, + char **optional_crl_paths, char **untrusted_cert_paths, + gboolean online, GError **error) PV_NONNULL(1, 4); + +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(AUTHORITY_KEYID, AUTHORITY_KEYID_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(PvX509WithPath, pv_x509_with_path_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(STACK_OF_DIST_POINT, STACK_OF_DIST_POINT_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(STACK_OF_X509, STACK_OF_X509_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(STACK_OF_X509_CRL, STACK_OF_X509_CRL_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(X509, X509_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(X509_CRL, X509_CRL_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(X509_LOOKUP, X509_LOOKUP_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(X509_NAME, X509_NAME_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(X509_VERIFY_PARAM, X509_VERIFY_PARAM_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(PvX509Pair, pv_x509_pair_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(X509_STORE, X509_STORE_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(X509_STORE_CTX, X509_STORE_CTX_free) + +#endif /* LIBPV_CERT_H */ diff --git a/include/libpv/common.h b/include/libpv/common.h new file mode 100644 index 00000000..bfc6c4e4 --- /dev/null +++ b/include/libpv/common.h @@ -0,0 +1,35 @@ +/* + * Libpv common 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. + * + */ +#ifndef LIBPV_COMMON_H +#define LIBPV_COMMON_H + +/* must be included before any (other) glib header to verify that + * the glib version is supported + */ +#include "libpv/glib-helper.h" + +#include + +#include "libpv/openssl-compat.h" +#include "libpv/macros.h" + +/** pv_init: + * + * Must be called before any libpv call. + */ +int pv_init(void); + +/** pv_cleanup: + * + * Must be called when done with using libpv. + */ +void pv_cleanup(void); + +#endif /* LIBPV_COMMON_H */ diff --git a/include/libpv/crypto.h b/include/libpv/crypto.h new file mode 100644 index 00000000..24c987fa --- /dev/null +++ b/include/libpv/crypto.h @@ -0,0 +1,200 @@ +/* + * General cryptography helper 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. + */ +#ifndef LIBPV_CRYPTO_H +#define LIBPV_CRYPTO_H + +#include +#include +#include +#include + +#include "libpv/common.h" + +typedef struct pv_cipher_parms { + const EVP_CIPHER *cipher; + size_t tag_size; + GBytes *key; + union { + GBytes *iv; + GBytes *tweak; + }; +} PvCipherParms; + +typedef union { + struct { + uint8_t x[80]; + uint8_t y[80]; + }; + uint8_t data[160]; +} PvEcdhPubKey; +G_STATIC_ASSERT(sizeof(PvEcdhPubKey) == 160); + +typedef GSList PvEvpKeyList; + +enum PvCryptoMode { + PV_ENCRYPT, + PV_DECRYPT, +}; + +/** pv_get_openssl_error: + * + * Returns: (transfer full): String representing the error. + */ +const char *pv_get_openssl_error(void); + +/** + * pv_BIO_reset: + * @b: BIO to reset + * + * Resets a BIO to its initial state. + * + * Returns: 0 in case of success, -1 otherwise. + */ +int pv_BIO_reset(BIO *b); + +/** + * pv_generate_rand_data: + * @size: number of generated random bytes using a crypographically secure pseudo random generator + * @error: return location for a #GError + * + * Creates a new #GBytes with @size random bytes using a cryptographically + * secure pseudo random generator. + * + * Returns: (nullable) (transfer full): a new #GBytes, or %NULL in case of an error + */ +GBytes *pv_generate_rand_data(size_t size, GError **error); + +/** + * pv_generate_key: + * @cipher: specifies the OpenSSL cipher for which a cryptographically secure key should be generated + * @error: return location for a #GError + * + * Creates a random key for @cipher using a cryptographically secure pseudo + * random generator. + * + * Returns: (nullable) (transfer full): a new #GBytes, or %NULL in case of an error + */ +GBytes *pv_generate_key(const EVP_CIPHER *cipher, GError **error) PV_NONNULL(1); + +/** + * pv_generate_iv: + * @cipher: specifies the OpenSSL cipher for which a cryptographically secure IV should be generated + * @error: return location for a #GError + * + * Creates a random IV for @cipher using a cryptographically secure pseudo + * random generator. + * + * Returns: (nullable) (transfer full): a new #GBytes, or %NULL in case of an error + */ +GBytes *pv_generate_iv(const EVP_CIPHER *cipher, GError **error) PV_NONNULL(1); + +/* Symmetric en/decryption functions */ + +/** + * pv_gcm_encrypt: + * @plain: data to encrypt + * @aad: (optional): additional data that should be authenticated with the key + * @parms: + * @cipher: (out): location to store the ciphertext + * @tag: (out): location to store the generated GCM tag + * @error: return location for a #GError + * + * Encrypts the @plain data and authenticates @aad data. + * + * Returns: number of bytes, or -1 in case of an error + */ +int64_t pv_gcm_encrypt(GBytes *plain, GBytes *aad, const PvCipherParms *parms, GBytes **cipher, + GBytes **tag, GError **error) PV_NONNULL(1, 3, 4, 5); + +/** + * pv_gcm_decrypt: + * @cipher: ciphertext to decrypt + * @aad: (optional): additional date to authenticate + * @tag: the GCM tag + * @parms: + * @plain: (out): location to store the decrypted data + * @error: return location for a #GError + * + * Decrypts the @cipher data and authenticates the @aad data. + * + * Returns: number of bytes, or -1 in case of an error + */ +int64_t pv_gcm_decrypt(GBytes *cipher, GBytes *aad, GBytes *tag, const PvCipherParms *parms, + GBytes **plain, GError **error) PV_NONNULL(1, 3, 4, 5); + +/** pv_hkdf_extract_and_expand: + * @derived_key_len: size of the output key + * @key: input key + * @salt: salt for the extraction + * @info: infor for the expansion + * @md: EVP mode of operation + * @error: return location for a #GError + * + * Performs a RFC 5869 HKDF. + * + * Returns: (nullable) (transfer full): Result of RFC 5869 HKDF + * + */ +GBytes *pv_hkdf_extract_and_expand(size_t derived_key_len, GBytes *key, GBytes *salt, GBytes *info, + const EVP_MD *md, GError **error) PV_NONNULL(2, 3, 4, 5); + +/** pv_generate_ec_key: + * + * @nid: Numerical identifier of the curve + * @error: return location for a #GError + * + * Returns: (nullable) (transfer full): new random key based on the given curve + */ +EVP_PKEY *pv_generate_ec_key(int nid, GError **error); + +/** pv_evp_pkey_to_ecdh_pub_key: + * + * @key: input key in EVP_PKEY format + * @error: return location for a #GError + * + * Returns: the public part of the input @key in ECDH format. + */ +PvEcdhPubKey *pv_evp_pkey_to_ecdh_pub_key(EVP_PKEY *key, GError **error) PV_NONNULL(1); + +/** pv_derive_exchange_key: + * @cust: Customer Key + * @host: Host key + * @error: return location for a #GError + * + * Returns: (nullable) (transfer full): Shared Secret of @cust and @host + */ +GBytes *pv_derive_exchange_key(EVP_PKEY *cust, EVP_PKEY *host, GError **error) PV_NONNULL(1, 2); + +GQuark pv_crypto_error_quark(void); +#define PV_CRYPTO_ERROR pv_crypto_error_quark() +typedef enum { + PV_CRYPTO_ERROR_DERIVE, + PV_CRYPTO_ERROR_HKDF_FAIL, + PV_CRYPTO_ERROR_INTERNAL, + PV_CRYPTO_ERROR_INVALID_KEY_SIZE, + PV_CRYPTO_ERROR_KEYGENERATION, + PV_CRYPTO_ERROR_RANDOMIZATION, + PV_CRYPTO_ERROR_READ_FILE, + PV_CRYPTO_ERROR_NO_IBM_Z_SIGNING_KEY, + PV_CRYPTO_ERROR_NO_MATCH_TAG, +} PvCryptoErrors; + +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(ASN1_INTEGER, ASN1_INTEGER_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(ASN1_OCTET_STRING, ASN1_OCTET_STRING_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(BIO, BIO_free_all) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(BIGNUM, BN_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(BN_CTX, BN_CTX_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(EC_GROUP, EC_GROUP_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(EC_KEY, EC_KEY_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(EC_POINT, EC_POINT_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(EVP_CIPHER_CTX, EVP_CIPHER_CTX_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(EVP_PKEY, EVP_PKEY_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(EVP_PKEY_CTX, EVP_PKEY_CTX_free) + +#endif /* LIBPV_CRYPTO_H */ diff --git a/include/libpv/curl.h b/include/libpv/curl.h new file mode 100644 index 00000000..f6089221 --- /dev/null +++ b/include/libpv/curl.h @@ -0,0 +1,53 @@ +/* + * Libcurl utils + * + * 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. + */ +#ifndef LIBPV_CURL_H +#define LIBPV_CURL_H + +#include + +#include "libpv/common.h" + +#define CRL_DOWNLOAD_TIMEOUT_MS 3000 +#define CRL_DOWNLOAD_MAX_SIZE 0x100000 + +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(CURL, curl_easy_cleanup) + +/** curl_download: + * @url: URL to specify location of data + * @timeout_ms: time to wait until fail + * @max_size: Maximum size of the downloaded data + * @error: return location for a GError + * + * Returns: (nullable) (transfer full): Downloaded data as #GByteArray + */ +GByteArray *curl_download(const char *url, long timeout_ms, uint max_size, GError **err); + +/** pv_curl_init: + * + * Should not be called by user. + * Use pv_init() instead which + * calls this function during creation. + */ +int pv_curl_init(void); + +/** pv_curl_cleanup: + * + * Should not be called by user. + * Use pv_cleanup() instead which + * calls this function during creation. + */ +void pv_curl_cleanup(void); + +#define PV_CURL_ERROR g_quark_from_static_string("pv-curl-error-quark") +typedef enum { + PV_CURL_ERROR_CURL_INIT_FAILED, + PV_CURL_ERROR_DOWNLOAD_FAILED, +} PvCurlErrors; + +#endif /* LIBPV_CURL_H */ diff --git a/include/libpv/glib-helper.h b/include/libpv/glib-helper.h new file mode 100644 index 00000000..6dcd8532 --- /dev/null +++ b/include/libpv/glib-helper.h @@ -0,0 +1,117 @@ +/* + * Glib convenience functions + * Shall be used instead of manually including glib. + * + * 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. + */ +#ifndef LIBPV_GLIB_HELPER_H +#define LIBPV_GLIB_HELPER_H + +#if defined(GLIB_VERSION_MIN_REQUIRED) +#if GLIB_VERSION_MIN_REQUIRED < GLIB_VERSION_2_56 +#error "GLIB_VERSION must be at least 2.56" +#endif +#else +#define GLIB_VERSION_MIN_REQUIRED GLIB_VERSION_2_56 +#endif + +#include +#include +#include + +#include "libpv/macros.h" + +#ifdef __clang__ +#define WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(...) \ + DO_PRAGMA(clang diagnostic push) \ + DO_PRAGMA(clang diagnostic ignored "-Wunused-function") \ + G_DEFINE_AUTOPTR_CLEANUP_FUNC(__VA_ARGS__) \ + DO_PRAGMA(clang diagnostic pop) +#else +#define WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(...) G_DEFINE_AUTOPTR_CLEANUP_FUNC(__VA_ARGS__) +#endif + +#define pv_wrapped_g_assert(__expr) g_assert(__expr) + +/** pv_sec_gbytes_new_take: + * + * #g_bytes_new_take() with secure cleanup + */ +GBytes *pv_sec_gbytes_new_take(void *data, size_t size); + +/** pv_sec_gbytes_new: + * + * #g_bytes_new() with secure cleanup + */ +GBytes *pv_sec_gbytes_new(const void *data, size_t size); + +/** pv_file_get_content_as_secure_bytes: + * + * @filename: path to file for reading in + * + * read file and save as secure gbytes + * + * Return: + * Content of file as #GBytes with secure cleanup + */ +GBytes *pv_file_get_content_as_secure_bytes(const char *filename); + +/** pv_file_get_content_as_g_bytes: + * + * @filename: path to file for reading in + * + * read file and save as gbytes + * + * Return: + * Content of file as #GBytes + */ +GBytes *pv_file_get_content_as_g_bytes(const char *filename, GError **error); + +/** pv_file_seek: + * + * fseek with error reporting + */ +int pv_file_seek(FILE *file, long offset, int whence, GError **error); + +/** pv_file_write: + * + * fwrite with error reporting + */ +size_t pv_file_write(FILE *file, const void *ptr, size_t size, GError **error); + +/** pv_file_close: + * + * fclose with error reporting + */ +long pv_file_close(FILE *file, GError **error); + +/** pv_file_tell: + * + * ftell with error reporting + */ +long pv_file_tell(FILE *file, GError **error); + +/** pv_file_open: + * + * fopen with error reporting + */ +FILE *pv_file_open(const char *filename, const char *mode, GError **error); + +/** pv_gbytes_memcpy: + * + * memcpy with size check. + */ +void *pv_gbytes_memcpy(void *dst, size_t dst_size, GBytes *src); + +#define PV_GLIB_HELPER_ERROR g_quark_from_static_string("pv-glib-helper_error-quark") +typedef enum { + PV_GLIB_HELPER_FILE_ERROR, +} pv_glib_helper_error_e; + +void pv_auto_close_file(FILE *file); +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(FILE, pv_auto_close_file) + +#endif /* LIBPV_GLIB_HELPER_H */ diff --git a/include/libpv/hash.h b/include/libpv/hash.h new file mode 100644 index 00000000..1656e2c0 --- /dev/null +++ b/include/libpv/hash.h @@ -0,0 +1,119 @@ +/* + * Hashing 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. + */ +#ifndef LIBPV_HASH_H +#define LIBPV_HASH_H + +#include + +#include "libpv/common.h" + +/** pv_digest_ctx_new: + * @md: mode of digest, e.g. #EVP_sha256() + * @error: return location for a #GError + * + * Returns: (nullable) (transfer full): a new #EVP_MD_CTX, or %NULL in case of an error + */ +EVP_MD_CTX *pv_digest_ctx_new(const EVP_MD *md, GError **error); + +/** pv_digest_ctx_update: + * @ctx: EVP_MD_CTX to add data + * @data: #GBytes to add to the context + * @error: return location for a #GError + * + * Adds @data to the digest context. Can be called multiple times. + * + * Returns: 0 in case of success, -1 otherwise. + */ +int pv_digest_ctx_update(EVP_MD_CTX *ctx, GBytes *data, GError **error); + +/** pv_digest_ctx_update_raw: + * @ctx: #EVP_MD_CTX to add data + * @buf: data to add to the context + * @size: size of @buf + * @error: return location for a #GError + * + * Adds @buf to the digest context. Can be called multiple times. + * + * Returns: 0 in case of success, -1 otherwise. + */ +int pv_digest_ctx_update_raw(EVP_MD_CTX *ctx, const uint8_t *buf, size_t size, GError **error); + +/** pv_digest_ctx_finalize: + * @ctx: #EVP_MD_CTX with data to digest + * @error: return location for a #GError + * + * Calculates the digest of all previously added data. Do not use @ctx afterwards. + * + * Returns: (nullable) (transfer full): Digest of all data added before as #GBytes, or NULL in case of error. + */ +GBytes *pv_digest_ctx_finalize(EVP_MD_CTX *ctx, GError **error); + +/** pv_sha256_hash: + * @buf: data for which a sha256 hash sould be calculated + * @size: size of @buf + * @error: return location for a #GError + * + * Shorthand for initializing a sha256-digest ctx, updating, and finalizing. + * + * Returns: (nullable) (transfer full): SHA256 of @buf as #GBytes, or NULL in case of error. + */ +GBytes *pv_sha256_hash(uint8_t *buf, size_t size, GError **error); + +/** pv_hmac_ctx_new: + * @key: key used for the HMAC + * @md: mode of digest, e.g. #EVP_sha512() + * @error: return location for a #GError + * + * Returns: (nullable) (transfer full): New #HMAC_CTX or NULL in case of error + */ +HMAC_CTX *pv_hmac_ctx_new(GBytes *key, const EVP_MD *md, GError **error); + +/** pv_hmac_ctx_update_raw: + * @ctx: #HMAC_CTX to add data + * @buf: data to add to the context + * @size: size of @buf + * @error: return location for a #GError + * + * Adds @buf to the HMAC context. Can be called multiple times. + * + * Returns: 0 in case of success, -1 otherwise. + */ +int pv_hmac_ctx_update_raw(HMAC_CTX *ctx, const void *data, size_t size, GError **error); + +/** pv_hmac_ctx_update: + * @ctx: #HMAC_CTX to add data + * @data: #GBytes to add to the context + * @error: return location for a #GError + * + * Adds @data to the HMAC context. Can be called multiple times. + * + * Returns: 0 in case of success, -1 otherwise. + */ + +int pv_hmac_ctx_update(HMAC_CTX *ctx, GBytes *data, GError **error); + +/** pv_hmac_ctx_finalize: + * @ctx: #HMAC_CTX with data to digest + * @error: return location for a #GError + * + * Calculates the HMAC of all previously added data. Do not use @ctx afterwards. + * + * Returns: (nullable) (transfer full): HMAC of all data added before as #GBytes, or NULL in case of error. + */ +GBytes *pv_hamc_ctx_finalize(HMAC_CTX *ctx, GError **error); + +#define PV_HASH_ERROR g_quark_from_static_string("pv-crypro-error-quark") +typedef enum { + PV_HASH_ERROR_INTERNAL, +} PvHashErrors; + +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(EVP_MD_CTX, EVP_MD_CTX_free) +WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(HMAC_CTX, HMAC_CTX_free) + +#endif /* LIBPV_HASH_H */ diff --git a/include/libpv/macros.h b/include/libpv/macros.h new file mode 100644 index 00000000..4b9fe0fa --- /dev/null +++ b/include/libpv/macros.h @@ -0,0 +1,16 @@ +/* + * Libpv common macro 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. + * + */ +#ifndef LIBPV_MACROS_H +#define LIBPV_MACROS_H + +#define PV_NONNULL(...) +#define DO_PRAGMA(x) _Pragma(#x) + +#endif /* LIBPV_MACROS_H */ diff --git a/include/libpv/openssl-compat.h b/include/libpv/openssl-compat.h new file mode 100644 index 00000000..255f08f9 --- /dev/null +++ b/include/libpv/openssl-compat.h @@ -0,0 +1,29 @@ +/* + * OpenSSL compatibility utils + * + * Copyright IBM Corp. 2021 + * + * s390-tools is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ +#ifndef LIBPV_OPENSSL_COMPAT_H +#define LIBPV_OPENSSL_COMPAT_H + +#include +#include +#include + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L +#define pv_X509_STORE_CTX_get_current_cert(ctx) X509_STORE_CTX_get_current_cert(ctx) +#define pv_X509_STORE_CTX_get1_crls(ctx, nm) X509_STORE_CTX_get1_crls((ctx), (nm)) +#define pv_X509_STORE_set_lookup_crls(st, cb) X509_STORE_set_lookup_crls(st, cb) +#elif OPENSSL_VERSION_NUMBER >= 0x10100000L +#define pv_X509_STORE_CTX_get_current_cert(ctx) \ + X509_STORE_CTX_get_current_cert((X509_STORE_CTX *)(ctx)) +#define pv_X509_STORE_CTX_get1_crls(ctx, nm) \ + X509_STORE_CTX_get1_crls((X509_STORE_CTX *)(ctx), (X509_NAME *)(nm)) +#define pv_X509_STORE_set_lookup_crls(st, cb) \ + X509_STORE_set_lookup_crls(st, (X509_STORE_CTX_lookup_crls_fn)(cb)) +#endif + +#endif /* LIBPV_OPENSSL_COMPAT_H */ diff --git a/include/libpv/se-hdr.h b/include/libpv/se-hdr.h new file mode 100644 index 00000000..3d4219c1 --- /dev/null +++ b/include/libpv/se-hdr.h @@ -0,0 +1,98 @@ +/* + * PV/SE header definitions + * + * Copyright IBM Corp. 2020 + * + * s390-tools is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ +#ifndef LIBPV_SE_HDR_H +#define LIBPV_SE_HDR_H + +#include "libpv/common.h" + +#include + +#include "boot/s390.h" +#include "libpv/crypto.h" + +/* Magic number which is used to identify the file containing the PV + * header + */ +#define PV_MAGIC_NUMBER 0x49424d5365634578ULL +#define PV_VERSION_1 0x00000100U + +/* Internal helper macro */ +#define __PV_BIT(nr) (1ULL << (63 - (nr))) + +/* Plaintext control flags */ +/* dumping of the configuration is allowed */ +#define PV_PCF_ALLOW_DUMPING __PV_BIT(34) +/* prevent Ultravisor decryption during unpack operation */ +#define PV_PCF_NO_DECRYPTION __PV_BIT(35) +/* PCKMO encrypt-DEA/TDEA-key functions allowed */ +#define PV_PCF_PCKMO_DEA_TDEA __PV_BIT(56) +/* PCKMO encrypt-AES-key functions allowed */ +#define PV_PCF_PCKMO_AES __PV_BIT(57) +/* PCKMO encrypt-ECC-key functions allowed */ +#define PV_PCF_PCKM_ECC __PV_BIT(58) + +/* maxima for the PV version 1 */ +#define PV_V1_IPIB_MAX_SIZE PAGE_SIZE +#define PV_V1_PV_HDR_MIN_SIZE \ + (sizeof(struct pv_hdr_head) + sizeof(struct pv_hdr_encrypted) + \ + sizeof(((struct pv_hdr *)0)->tag) + 1 * sizeof(struct pv_hdr_key_slot)) +#define PV_V1_PV_HDR_MAX_SIZE (2 * PAGE_SIZE) + +#define PV_IMAGE_ENCR_KEY_SIZE 64 + +typedef struct pv_hdr_key_slot { + uint8_t digest_key[SHA256_DIGEST_LENGTH]; + uint8_t wrapped_key[32]; + uint8_t tag[16]; +} __packed PvHdrKeySlot; + +typedef struct pv_hdr_opt_item { + uint32_t otype; + uint8_t ibk[32]; + uint8_t data[]; +} __packed PvHdrOptItem; + +/* integrity protected data (by GCM tag), but non-encrypted */ +struct pv_hdr_head { + uint64_t magic; + uint32_t version; + uint32_t phs; + uint8_t iv[12]; + uint32_t res1; + uint64_t nks; + uint64_t sea; + uint64_t nep; + uint64_t pcf; + PvEcdhPubKey cust_pub_key; + uint8_t pld[SHA512_DIGEST_LENGTH]; + uint8_t ald[SHA512_DIGEST_LENGTH]; + uint8_t tld[SHA512_DIGEST_LENGTH]; +} __packed; + +/* Must not have any padding */ +struct pv_hdr_encrypted { + uint8_t cust_comm_key[32]; + uint8_t img_enc_key_1[PV_IMAGE_ENCR_KEY_SIZE / 2]; + uint8_t img_enc_key_2[PV_IMAGE_ENCR_KEY_SIZE / 2]; + struct psw_t psw; + uint64_t scf; + uint32_t noi; + uint32_t res2; +}; +G_STATIC_ASSERT(sizeof(struct pv_hdr_encrypted) == 32 + 32 + 32 + sizeof(struct psw_t) + 8 + 4 + 4); + +typedef struct pv_hdr { + struct pv_hdr_head head; + struct pv_hdr_key_slot *slots; + struct pv_hdr_encrypted *encrypted; + struct pv_hdr_opt_item **optional_items; + uint8_t tag[16]; +} PvHdr; + +#endif /* LIBPV_SE_HDR_H */ diff --git a/libpv/Makefile b/libpv/Makefile new file mode 100644 index 00000000..c76e6ceb --- /dev/null +++ b/libpv/Makefile @@ -0,0 +1,101 @@ +# Common definitions +include ../common.mak + +.DEFAULT_GOAL := all + +LIB := libpv.a + +ifneq ($(shell sh -c 'command -v pkg-config'),) +GLIB2_CFLAGS := $(shell pkg-config --silence-errors --cflags glib-2.0) +GLIB2_LIBS := $(shell pkg-config --silence-errors --libs glib-2.0) +LIBCRYPTO_CFLAGS := $(shell pkg-config --silence-errors --cflags libcrypto openssl) +LIBCRYPTO_LIBS := $(shell pkg-config --silence-errors --libs libcrypto openssl) +LIBCURL_CFLAGS := $(shell pkg-config --silence-errors --cflags libcurl) +LIBCURL_LIBS := $(shell pkg-config --silence-errors --libs libcurl) +else +GLIB2_CFLAGS := -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include +GLIB2_LIBS := -lglib-2.0 +LIBCRYPTO_CFLAGS := +LIBCRYPTO_LIBS := -lcrypto -lssl +LIBCURL_CFLAGS := -I/usr/include/s390x-linux-gnu +LIBCURL_LIBS := -lcurl +endif +LDLIBS += $(GLIB2_LIBS) $(LIBCRYPTO_LIBS) $(LIBCURL_LIBS) + +WARNINGS := -Wall -Wextra -Wshadow \ + -Wcast-align -Wwrite-strings -Wmissing-prototypes \ + -Wmissing-declarations -Wredundant-decls -Wnested-externs \ + -Wno-long-long -Wuninitialized -Wconversion -Wstrict-prototypes \ + -Wpointer-arith -Wno-error=inline \ + -Wno-unused-function -Wno-unused-parameter -Wno-unused-variable \ + -Werror \ + $(NULL) + +ALL_CFLAGS += -std=gnu11 \ + -DOPENSSL_API_COMPAT=0x10101000L \ + $(GLIB2_CFLAGS) \ + $(LIBCRYPTO_CFLAGS) \ + $(LIBCURL_CFLAGS) \ + $(WARNINGS) \ + $(NULL) + +BUILD_TARGETS := skip-$(LIB) +ifneq (${HAVE_OPENSSL},0) +ifneq (${HAVE_GLIB2},0) +ifneq (${HAVE_LIBCURL},0) +BUILD_TARGETS := $(LIB) +endif +endif +endif + +sources := $(wildcard *.c) +objects := $(patsubst %.c,%.o,$(sources)) + +all: $(BUILD_TARGETS) .check_dep-$(LIB) + +$(LIB): $(objects) +$(LIB): ALL_CFLAGS += -fPIC + +$(objects): .check-dep-$(LIB) + +install: all + +clean: + rm -f -- $(objects) + rm -f -- $(LIB) + rm -f -- .check-dep-$(LIB) .detect-openssl.dep.c + +skip-$(LIB): + echo " SKIP $(LIB) due to unresolved dependencies" + +.PHONY: all install clean skip-$(LIB) install-$(LIB) + + +.detect-openssl.dep.c: + echo "#include " > $@ + echo "#if OPENSSL_VERSION_NUMBER < 0x10101000L" >> $@ + echo " #error openssl version 1.1.0 is required" >> $@ + echo "#endif" >> $@ + echo "static void __attribute__((unused)) test(void) {" >> $@ + echo " EVP_MD_CTX *ctx = EVP_MD_CTX_new();" >> $@ + echo " EVP_MD_CTX_free(ctx);" >> $@ + echo "}" >> $@ + +.check-dep-$(LIB): .detect-openssl.dep.c + $(call check_dep, \ + "$(LIB)", \ + "glib.h", \ + "glib2-devel / libglib2.0-dev", \ + "HAVE_GLIB2=0") + $(call check_dep, \ + "$(LIB)", \ + $^, \ + "openssl-devel / libssl-dev version >= 1.1.0", \ + "HAVE_OPENSSL=0", \ + "-I.") + $(call check_dep, \ + "$(LIB)", \ + "curl/curl.h", \ + "libcurl-devel", \ + "HAVE_LIBCURL=0") + touch $@ diff --git a/libpv/cert.c b/libpv/cert.c new file mode 100644 index 00000000..4cd94211 --- /dev/null +++ b/libpv/cert.c @@ -0,0 +1,1645 @@ +/* + * 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 + +#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(X509_CRL) ret = NULL; + + 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; + } + + for (int i = 0; i < sk_DIST_POINT_num(crldps); i++) { + DIST_POINT *crldp = sk_DIST_POINT_value(crldps, i); + + g_assert(crldp); + + /* ignore error */ + ret = load_crl_by_dist_point(crldp, NULL); + if (ret) + return g_steal_pointer(&ret); + } + + 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); +} diff --git a/libpv/common.c b/libpv/common.c new file mode 100644 index 00000000..547d88a0 --- /dev/null +++ b/libpv/common.c @@ -0,0 +1,45 @@ +/* + * Libpv common functions. + * + * 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 "libpv/common.h" +#include "libpv/cert.h" +#include "libpv/curl.h" + +/* setup and tear down */ +int pv_init(void) +{ + static size_t openssl_initalized; + + if (g_once_init_enter(&openssl_initalized)) { + if (OPENSSL_VERSION_NUMBER < 0x1000100fL) + g_assert_not_reached(); +#if OPENSSL_VERSION_NUMBER < 0x10100000L + SSL_library_init(); + SSL_load_error_strings(); +#else + OPENSSL_init_crypto(0, NULL); +#endif + + if (pv_curl_init() != 0) + return -1; + + pv_cert_init(); + g_once_init_leave(&openssl_initalized, 1); + } + return 0; +} + +void pv_cleanup(void) +{ + pv_cert_cleanup(); + pv_curl_cleanup(); +} diff --git a/libpv/config.h b/libpv/config.h new file mode 100644 index 00000000..4e3536ea --- /dev/null +++ b/libpv/config.h @@ -0,0 +1,15 @@ +/* + * Config file. + * Must be include before any other header. + * + * 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. + * + */ +#ifndef LIBPV_CONFIG_H +#define LIBPV_CONFIG_H +#define GETTEXT_PACKAGE "libpv" + +#endif /* LIBPV_CONFIG_H */ diff --git a/libpv/crypto.c b/libpv/crypto.c new file mode 100644 index 00000000..207795d3 --- /dev/null +++ b/libpv/crypto.c @@ -0,0 +1,529 @@ +/* + * Cryptography functions + * + * 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 +#include +#include +#include + +#include "lib/zt_common.h" +#include "libpv/crypto.h" +#include "libpv/glib-helper.h" +#include "libpv/hash.h" + +const char *pv_get_openssl_error(void) +{ + const char *ret; + BIO *bio; + char *buf; + long len; + + bio = BIO_new(BIO_s_mem()); + ERR_print_errors(bio); + len = BIO_get_mem_data(bio, &buf); + if (len < 0) + ret = "Cannot receive OpenSSL error message."; + else + ret = g_strndup(buf, (size_t)len); + BIO_free(bio); + return ret; +} + +int pv_BIO_reset(BIO *b) +{ + int rc = BIO_reset(b); + + if (rc != 1 && (BIO_method_type(b) == BIO_TYPE_FILE && rc != 0)) + return -1; + return 1; +} + +GBytes *pv_generate_rand_data(size_t size, GError **error) +{ + g_autofree uint8_t *data = NULL; + + if (size > INT_MAX) { + g_set_error_literal(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_RANDOMIZATION, + "Too many random data requested. Split it up"); + OPENSSL_clear_free(data, size); + return NULL; + } + + data = g_malloc(size); + if (RAND_bytes(data, (int)size) != 1) { + g_set_error_literal(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_RANDOMIZATION, + "The required amount of random data is not available"); + return NULL; + } + + return pv_sec_gbytes_new_take(g_steal_pointer(&data), size); +} + +GBytes *pv_generate_key(const EVP_CIPHER *cipher, GError **error) +{ + int size; + + pv_wrapped_g_assert(cipher); + + size = EVP_CIPHER_key_length(cipher); + if (size <= 0) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_KEYGENERATION, + "Unknown cipher"); + return NULL; + } + + return pv_generate_rand_data((guint)size, error); +} + +GBytes *pv_generate_iv(const EVP_CIPHER *cipher, GError **error) +{ + int size; + + pv_wrapped_g_assert(cipher); + + size = EVP_CIPHER_iv_length(cipher); + if (size <= 0) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_KEYGENERATION, + "Unknown cipher"); + return NULL; + } + + return pv_generate_rand_data((guint)size, error); +} + +static int64_t pv_gcm_encrypt_decrypt(GBytes *input, GBytes *aad, const PvCipherParms *parms, + GBytes **output, GBytes **tagp, enum PvCryptoMode mode, + GError **error) +{ + const uint8_t *in_data, *aad_data = NULL, *iv_data, *key_data; + size_t in_size, aad_size = 0, iv_size, key_size, out_size; + const EVP_CIPHER *cipher = parms->cipher; + const size_t tag_size = parms->tag_size; + gboolean encrypt = mode == PV_ENCRYPT; + g_autoptr(EVP_CIPHER_CTX) ctx = NULL; + g_autofree uint8_t *out_data = NULL; + g_autofree uint8_t *tag_data = NULL; + const GBytes *key = parms->key; + const GBytes *iv = parms->iv; + int cipher_block_size; + int64_t ret = -1; + int len = -1; + GBytes *tag; + + g_assert(tagp); + g_assert(cipher); + g_assert(key); + g_assert(iv); + + tag = *tagp; + in_data = g_bytes_get_data((GBytes *)input, &in_size); + if (aad) + aad_data = g_bytes_get_data((GBytes *)aad, &aad_size); + iv_data = g_bytes_get_data((GBytes *)iv, &iv_size); + key_data = g_bytes_get_data((GBytes *)key, &key_size); + out_size = in_size; + cipher_block_size = EVP_CIPHER_block_size(cipher); + + /* Checks for later casts */ + g_assert(aad_size <= INT_MAX); + g_assert(in_size <= INT_MAX); + g_assert(iv_size <= INT_MAX); + g_assert(cipher_block_size > 0); + + ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + g_abort(); + + if (tag_size == 0 || (tag_size % (size_t)cipher_block_size != 0)) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "Passed tag size is incorrect"); + return -1; + } + + /* Has the passed key the correct size? */ + if (EVP_CIPHER_key_length(cipher) != (int)key_size) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "Passed key has incorrect size: %ld != %d", key_size, + EVP_CIPHER_key_length(cipher)); + return -1; + } + + /* First, set the cipher algorithm so we can verify our key/IV lengths + */ + if (EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, encrypt) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "EVP_CIPHER_CTX_new failed"); + return -1; + } + + /* Set IV length */ + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, (int)iv_size, NULL) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "EVP_CIPHER_CTX_ex failed"); + return -1; + } + + /* Initialise key and IV */ + if (EVP_CipherInit_ex(ctx, NULL, NULL, key_data, iv_data, encrypt) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "EVP_CipherInit_ex failed"); + return -1; + } + + /* Allocate output data */ + out_data = g_malloc0(out_size); + if (encrypt) + tag_data = g_malloc0(tag_size); + + if (aad_size > 0) { + /* Provide any AAD data */ + if (EVP_CipherUpdate(ctx, NULL, &len, aad_data, (int)aad_size) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "EVP_CipherUpdate failed"); + return -1; + } + g_assert(len == (int)aad_size); + } + + /* Provide data to be en/decrypted */ + if (EVP_CipherUpdate(ctx, out_data, &len, in_data, (int)in_size) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "EVP_CipherUpdate failed"); + return -1; + } + ret = len; + + if (!encrypt) { + const uint8_t *tmp_tag_data = NULL; + size_t tmp_tag_size = 0; + + if (tag) + tmp_tag_data = g_bytes_get_data(tag, &tmp_tag_size); + if (tag_size != tmp_tag_size) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "Getting the GCM tag failed"); + return -1; + } + + /* Set expected tag value */ + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, (int)tmp_tag_size, + (uint8_t *)tmp_tag_data) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "Setting the GCM tag failed"); + return -1; + } + } + + /* Finalize the en/decryption */ + if (EVP_CipherFinal_ex(ctx, (uint8_t *)out_data + len, &len) != 1) { + if (encrypt) + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "Encrypting failed (EVP_CipherFinal_ex)"); + else + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_NO_MATCH_TAG, + "Verifying the GCM tag failed"); + return -1; + } + ret += len; + + if (encrypt) { + /* Get the tag */ + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, (int)tag_size, tag_data) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + "Getting the GCM tag failed"); + return -1; + } + + g_assert(!*tagp); + *tagp = g_bytes_new_take(g_steal_pointer(&tag_data), tag_size); + } + g_assert(ret == (int)out_size); + g_assert(out_size == in_size); + + g_assert(!*output); + *output = pv_sec_gbytes_new_take(g_steal_pointer(&out_data), out_size); + return ret; +} + +int64_t pv_gcm_encrypt(GBytes *plain, GBytes *aad, const PvCipherParms *parms, GBytes **cipher, + GBytes **tag, GError **error) +{ + pv_wrapped_g_assert(plain); + pv_wrapped_g_assert(parms); + pv_wrapped_g_assert(cipher); + pv_wrapped_g_assert(tag); + + return pv_gcm_encrypt_decrypt(plain, aad, parms, cipher, tag, PV_ENCRYPT, error); +} + +int64_t pv_gcm_decrypt(GBytes *cipher, GBytes *aad, GBytes *tag, const PvCipherParms *parms, + GBytes **plain, GError **error) +{ + pv_wrapped_g_assert(cipher); + pv_wrapped_g_assert(tag); + pv_wrapped_g_assert(parms); + pv_wrapped_g_assert(plain); + + return pv_gcm_encrypt_decrypt(cipher, aad, parms, plain, &tag, PV_DECRYPT, error); +} + +GBytes *pv_hkdf_extract_and_expand(size_t derived_key_len, GBytes *key, GBytes *salt, GBytes *info, + const EVP_MD *md, G_GNUC_UNUSED GError **error) +{ + const unsigned char *salt_data, *key_data, *info_data; + g_autoptr(EVP_PKEY_CTX) ctx = NULL; + size_t salt_len, key_len, info_len; + g_autofree unsigned char *derived_key = NULL; + + g_assert(derived_key_len > 0); + pv_wrapped_g_assert(key); + pv_wrapped_g_assert(salt); + pv_wrapped_g_assert(info); + pv_wrapped_g_assert(md); + + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL); + if (!ctx) + g_abort(); + + if (EVP_PKEY_derive_init(ctx) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_HKDF_FAIL, + "FAILED to derive key via HKDF"); + return NULL; + } + + if (EVP_PKEY_CTX_hkdf_mode(ctx, EVP_PKEY_HKDEF_MODE_EXTRACT_AND_EXPAND) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_HKDF_FAIL, + "FAILED to derive key via HKDF"); + return NULL; + } + + if (EVP_PKEY_CTX_set_hkdf_md(ctx, md) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_HKDF_FAIL, + "FAILED to derive key via HKDF"); + return NULL; + } + + salt_data = g_bytes_get_data(salt, &salt_len); + if (salt_len > INT_MAX) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_HKDF_FAIL, + "FAILED to derive key via HKDF"); + return NULL; + } + + if (EVP_PKEY_CTX_set1_hkdf_salt(ctx, salt_data, (int)salt_len) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_HKDF_FAIL, + "FAILED to derive key via HKDF"); + return NULL; + } + + key_data = g_bytes_get_data(key, &key_len); + if (key_len > INT_MAX) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_HKDF_FAIL, + "FAILED to derive key via HKDF"); + return NULL; + } + + if (EVP_PKEY_CTX_set1_hkdf_key(ctx, key_data, (int)key_len) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_HKDF_FAIL, + "FAILED to derive key via HKDF"); + return NULL; + } + + info_data = g_bytes_get_data(info, &info_len); + if (info_len > INT_MAX) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_HKDF_FAIL, + "FAILED to derive key via HKDF"); + return NULL; + } + + if (EVP_PKEY_CTX_add1_hkdf_info(ctx, (unsigned char *)info_data, (int)info_len) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_HKDF_FAIL, + "FAILED to derive key via HKDF"); + return NULL; + } + + derived_key = g_malloc0(derived_key_len); + if (EVP_PKEY_derive(ctx, derived_key, &derived_key_len) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_HKDF_FAIL, + "FAILED to derive key via HKDF"); + printf("%s\n", pv_get_openssl_error()); + return NULL; + } + + return pv_sec_gbytes_new_take(g_steal_pointer(&derived_key), derived_key_len); +} + +EVP_PKEY *pv_generate_ec_key(int nid, GError **error) +{ + g_autoptr(EVP_PKEY_CTX) ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL); + g_autoptr(EVP_PKEY) ret = NULL; + + g_assert(ctx); + + if (EVP_PKEY_keygen_init(ctx) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_KEYGENERATION, + _("EC key could not be auto-generated")); + return NULL; + } + + if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, nid) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_KEYGENERATION, + _("EC key could not be auto-generated")); + return NULL; + } + + if (EVP_PKEY_keygen(ctx, &ret) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_KEYGENERATION, + _("EC key could not be auto-generated")); + return NULL; + } + + return g_steal_pointer(&ret); +} + +/* Convert a EVP_PKEY to the key format used in the PV header */ +PvEcdhPubKey *pv_evp_pkey_to_ecdh_pub_key(EVP_PKEY *key, GError **error) +{ + g_autofree PvEcdhPubKey *ret = g_new0(PvEcdhPubKey, 1); + g_autoptr(BIGNUM) pub_x_big = NULL, pub_y_big = NULL; + g_autoptr(EC_KEY) ec_key = NULL; + const EC_POINT *pub_key; + const EC_GROUP *grp; + + pv_wrapped_g_assert(key); + + ec_key = EVP_PKEY_get1_EC_KEY(key); + if (!ec_key) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + _("Key has the wrong type")); + return NULL; + } + + pub_key = EC_KEY_get0_public_key(ec_key); + if (!pub_key) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + _("Failed to get public key")); + return NULL; + } + + grp = EC_KEY_get0_group(ec_key); + if (!grp) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + _("Failed to get EC group")); + return NULL; + } + + pub_x_big = BN_new(); + if (!pub_x_big) + g_abort(); + + pub_y_big = BN_new(); + if (!pub_y_big) + g_abort(); + + if (EC_POINT_get_affine_coordinates_GFp(grp, pub_key, pub_x_big, pub_y_big, NULL) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + _("Cannot convert key to internal format")); + return NULL; + } + + if (BN_bn2binpad(pub_x_big, ret->x, sizeof(ret->x)) < 0) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + _("Cannot convert key to internal format")); + return NULL; + } + + if (BN_bn2binpad(pub_y_big, ret->y, sizeof(ret->y)) < 0) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + _("Cannot convert key to internal format")); + return NULL; + } + + return g_steal_pointer(&ret); +} + +static GBytes *derive_key(EVP_PKEY *key1, EVP_PKEY *key2, GError **error) +{ + g_autoptr(EVP_PKEY_CTX) ctx = NULL; + uint8_t *data = NULL; + size_t data_size, key_size; + + ctx = EVP_PKEY_CTX_new(key1, NULL); + if (!ctx) + g_abort(); + + if (EVP_PKEY_derive_init(ctx) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + _("Key derivation failed")); + return NULL; + } + + if (EVP_PKEY_derive_set_peer(ctx, key2) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_INTERNAL, + _("Key derivation failed")); + return NULL; + } + + /* Determine buffer length */ + if (EVP_PKEY_derive(ctx, NULL, &key_size) != 1) { + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_DERIVE, + _("Key derivation failed")); + return NULL; + } + + data_size = key_size; + data = OPENSSL_malloc(data_size); + if (!data) + g_abort(); + if (EVP_PKEY_derive(ctx, data, &data_size) != 1) { + OPENSSL_clear_free(data, data_size); + g_set_error(error, PV_CRYPTO_ERROR, PV_CRYPTO_ERROR_DERIVE, + _("Key derivation failed")); + return NULL; + } + + g_assert(data_size == key_size); + return pv_sec_gbytes_new_take(g_steal_pointer(&data), data_size); +} + +GBytes *pv_derive_exchange_key(EVP_PKEY *cust, EVP_PKEY *host, GError **error) +{ + const guint8 append[] = { 0x00, 0x00, 0x00, 0x01 }; + g_autoptr(GBytes) derived_key = NULL, ret = NULL; + g_autoptr(GByteArray) der_key_ga = NULL; + g_autofree uint8_t *raw = NULL; + size_t raw_len; + + pv_wrapped_g_assert(cust); + pv_wrapped_g_assert(host); + + derived_key = derive_key(cust, host, error); + if (!derived_key) + return NULL; + + der_key_ga = g_bytes_unref_to_array(g_steal_pointer(&derived_key)); + /* ANSI X.9.63-2011: 66 bytes x with leading 7 bits and + * concatenate 32 bit int '1' + */ + der_key_ga = g_byte_array_append(der_key_ga, append, sizeof(append)); + /* free GBytesArray and get underlying data */ + raw_len = der_key_ga->len; + raw = g_byte_array_free(g_steal_pointer(&der_key_ga), FALSE); + + ret = pv_sha256_hash(raw, raw_len, error); + OPENSSL_cleanse(raw, raw_len); + return g_steal_pointer(&ret); +} + +GQuark pv_crypto_error_quark(void) +{ + return g_quark_from_static_string("pv-crypto-error-quark"); +} diff --git a/libpv/curl.c b/libpv/curl.c new file mode 100644 index 00000000..898f9a8a --- /dev/null +++ b/libpv/curl.c @@ -0,0 +1,116 @@ +/* + * Libcurl utils + * + * Copyright IBM Corp. 2020 + * + * 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 +#include + +#include "lib/zt_common.h" +#include "libpv/curl.h" + +struct UserData { + GByteArray *buffer; + uint max_size; +}; + +static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + g_assert(userdata); + struct UserData *data = (struct UserData *)userdata; + GByteArray *buffer = data->buffer; + uint64_t actual_size; + size_t err; + + g_assert(buffer); + + if (!g_uint64_checked_mul(&actual_size, size, nmemb)) + g_abort(); + + /* Signal an error condition by returning a amount that differs + * from the amount passed to the callback. This results in a + * CURLE_WRITE_ERROR. + */ + err = actual_size + 1; + + if (actual_size > G_MAXUINT) + return err; + + data->buffer = g_byte_array_append(buffer, (uint8_t *)ptr, (uint)actual_size); + if (data->buffer->len > data->max_size) + return err; + + return actual_size; +} + +int pv_curl_init(void) +{ + if (curl_global_init(CURL_GLOBAL_ALL) != 0) + return -1; + return 0; +} + +void pv_curl_cleanup(void) +{ + curl_global_cleanup(); +} + +GByteArray *curl_download(const char *url, long timeout_ms, uint max_size, GError **err) +{ + g_autoptr(GByteArray) ret = NULL; + g_autoptr(CURL) handle = NULL; + g_autofree char *agent = NULL; + struct UserData userdata; + CURLcode rc; + + /* set up curl session */ + handle = curl_easy_init(); + if (!handle) + g_abort(); + + /* follow redirection */ + rc = curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L); + if (rc != CURLE_OK) + goto curl_err; + rc = curl_easy_setopt(handle, CURLOPT_TIMEOUT_MS, timeout_ms); + if (rc != CURLE_OK) + goto curl_err; + rc = curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L); + if (rc != CURLE_OK) + goto curl_err; + agent = g_strdup_printf("%s/%s", GETTEXT_PACKAGE, RELEASE_STRING); + rc = curl_easy_setopt(handle, CURLOPT_USERAGENT, agent); + if (rc != CURLE_OK) + goto curl_err; + rc = curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_callback); + if (rc != CURLE_OK) + goto curl_err; + ret = g_byte_array_new(); + userdata.buffer = ret; + userdata.max_size = max_size; + rc = curl_easy_setopt(handle, CURLOPT_WRITEDATA, (void *)&userdata); + if (rc != CURLE_OK) + goto curl_err; + rc = curl_easy_setopt(handle, CURLOPT_URL, url); + if (rc != CURLE_OK) + goto curl_err; + + rc = curl_easy_perform(handle); + if (rc != CURLE_OK) { + g_set_error(err, PV_CURL_ERROR, PV_CURL_ERROR_DOWNLOAD_FAILED, + _("download failed: %s"), curl_easy_strerror(rc)); + return NULL; + } + + return g_steal_pointer(&ret); +curl_err: + g_set_error(err, PV_CURL_ERROR, PV_CURL_ERROR_CURL_INIT_FAILED, + _("cURL initialization failed: %s"), curl_easy_strerror(rc)); + return NULL; +} diff --git a/libpv/glib-helper.c b/libpv/glib-helper.c new file mode 100644 index 00000000..fe89dd75 --- /dev/null +++ b/libpv/glib-helper.c @@ -0,0 +1,179 @@ +/* + * Glib convenience functions + * + * 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 +#include + +#include "libpv/glib-helper.h" + +struct __data { + void *data; + size_t size; + GFreeFunc free_func; +}; + +static void __data_clear_and_free(void *p) +{ + struct __data *ptr = p; + + if (!ptr) + return; + + if (ptr->data) { + OPENSSL_cleanse(ptr->data, ptr->size); + ptr->free_func(ptr->data); + } + g_free(ptr); +} + +static GBytes *pv_sec_gbytes_new_take_func(void *data, size_t size, GFreeFunc free_func) +{ + struct __data *tmp = g_new(struct __data, 1); + + tmp->data = data; + tmp->size = size; + tmp->free_func = free_func; + + return g_bytes_new_with_free_func(data, size, __data_clear_and_free, tmp); +} + +GBytes *pv_sec_gbytes_new_take(void *data, size_t size) +{ + return pv_sec_gbytes_new_take_func(data, size, g_free); +} + +GBytes *pv_sec_gbytes_new(const void *data, size_t size) +{ + g_autofree void *tmp_data = NULL; + + g_return_val_if_fail(data || size != 0, NULL); + + tmp_data = g_malloc(size); + memcpy(tmp_data, data, size); + return pv_sec_gbytes_new_take(g_steal_pointer(&tmp_data), size); +} + +int pv_file_seek(FILE *file, long offset, int whence, GError **error) +{ + int cached_errno; + int ret = fseek(file, offset, whence); + + if (ret) { + cached_errno = errno; + g_set_error(error, PV_GLIB_HELPER_ERROR, PV_GLIB_HELPER_FILE_ERROR, + "Cannot seek: %s", g_strerror(cached_errno)); + } + return ret; +} + +size_t pv_file_write(FILE *file, const void *ptr, size_t size, GError **error) +{ + int cached_errno; + size_t n = fwrite(ptr, 1, size, file); + + if (n != size) { + cached_errno = errno; + g_set_error(error, PV_GLIB_HELPER_ERROR, PV_GLIB_HELPER_FILE_ERROR, + "Cannot write: %s", g_strerror(cached_errno)); + } + return n; +} + +long pv_file_close(FILE *file, GError **error) +{ + int cached_errno; + int ret = fclose(file); + + if (ret) { + cached_errno = errno; + g_set_error(error, PV_GLIB_HELPER_ERROR, PV_GLIB_HELPER_FILE_ERROR, + "Cannot close: %s", g_strerror(cached_errno)); + } + return ret; +} + +void pv_auto_close_file(FILE *file) +{ + if (!file) + return; + + (void)pv_file_close(file, NULL); +} + +long pv_file_tell(FILE *file, GError **error) +{ + int cached_errno; + long n = ftell(file); + + if (n < 0) { + cached_errno = errno; + g_set_error(error, PV_GLIB_HELPER_ERROR, PV_GLIB_HELPER_FILE_ERROR, + "Cannot tell: %s", g_strerror(cached_errno)); + } + return n; +} + +FILE *pv_file_open(const char *filename, const char *mode, GError **error) +{ + FILE *file = fopen(filename, mode); + int cached_errno; + + if (!file) { + cached_errno = errno; + g_set_error(error, PV_GLIB_HELPER_ERROR, PV_GLIB_HELPER_FILE_ERROR, + "Cannot open '%s'. %s", filename, g_strerror(cached_errno)); + return NULL; + } + return file; +} + +GBytes *pv_file_get_content_as_g_bytes(const char *filename, GError **error) +{ + g_autofree char *data = NULL; + size_t data_size; + + if (!g_file_get_contents(filename, &data, &data_size, error)) + return NULL; + + return g_bytes_new_take(g_steal_pointer(&data), data_size); +} + +GBytes *pv_file_get_content_as_secure_bytes(const char *filename) +{ + g_autoptr(FILE) f = fopen(filename, "rb"); + g_autofree char *data = NULL; + ssize_t file_size; + size_t data_size; + + if (!f) + return NULL; + + fseek(f, 0, SEEK_END); + file_size = ftell(f); + if (file_size < 0) + return NULL; + data_size = (size_t)file_size; + fseek(f, 0, SEEK_SET); + data = g_malloc0(data_size); + if (data_size != fread(data, 1, data_size, f)) + return NULL; + return pv_sec_gbytes_new_take(g_steal_pointer(&data), data_size); +} + +void *pv_gbytes_memcpy(void *dst, size_t dst_size, GBytes *src) +{ + size_t src_size; + const void *src_data = g_bytes_get_data(src, &src_size); + + if (dst_size < src_size) + return NULL; + return memcpy(dst, src_data, src_size); +} diff --git a/libpv/hash.c b/libpv/hash.c new file mode 100644 index 00000000..5d58147d --- /dev/null +++ b/libpv/hash.c @@ -0,0 +1,147 @@ +/* + * Hashing functions. + + * 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 "libpv/crypto.h" +#include "libpv/hash.h" + +GBytes *pv_sha256_hash(uint8_t *buf, size_t size, GError **error) +{ + g_autoptr(EVP_MD_CTX) ctx = NULL; + + ctx = pv_digest_ctx_new(EVP_sha256(), error); + if (!ctx) + return NULL; + + if (pv_digest_ctx_update_raw(ctx, buf, size, error) != 0) + return NULL; + + return pv_digest_ctx_finalize(ctx, error); +} + +EVP_MD_CTX *pv_digest_ctx_new(const EVP_MD *md, GError **error) +{ + g_autoptr(EVP_MD_CTX) ctx = EVP_MD_CTX_new(); + + if (!ctx) { + g_set_error(error, PV_HASH_ERROR, PV_HASH_ERROR_INTERNAL, + _("Hash context generation failed")); + return NULL; + } + + if (EVP_DigestInit_ex(ctx, md, NULL) != 1) { + g_set_error(error, PV_HASH_ERROR, PV_HASH_ERROR_INTERNAL, + _("EVP_DigestInit_ex failed")); + return NULL; + } + + return g_steal_pointer(&ctx); +} + +int pv_digest_ctx_update_raw(EVP_MD_CTX *ctx, const uint8_t *buf, size_t size, GError **error) +{ + if (!buf || size == 0) + return 0; + + if (EVP_DigestUpdate(ctx, buf, size) != 1) { + g_set_error(error, PV_HASH_ERROR, PV_HASH_ERROR_INTERNAL, + _("EVP_DigestUpdate failed")); + return -1; + } + return 0; +} + +int pv_digest_ctx_update(EVP_MD_CTX *ctx, GBytes *data, GError **error) +{ + const uint8_t *buf; + size_t buf_size; + + if (!data) + return 0; + buf = g_bytes_get_data((GBytes *)data, &buf_size); + return pv_digest_ctx_update_raw(ctx, buf, buf_size, error); +} + +GBytes *pv_digest_ctx_finalize(EVP_MD_CTX *ctx, GError **error) +{ + int md_size = EVP_MD_size(EVP_MD_CTX_md(ctx)); + g_autofree uint8_t *digest = NULL; + unsigned int digest_size; + + g_assert(md_size > 0); + + digest = g_malloc0((uint)md_size); + if (EVP_DigestFinal_ex(ctx, digest, &digest_size) != 1) { + g_set_error(error, PV_HASH_ERROR, PV_HASH_ERROR_INTERNAL, + _("EVP_DigestFinal_ex failed")); + return NULL; + } + + g_assert(digest_size == (uint)md_size); + return g_bytes_new_take(g_steal_pointer(&digest), digest_size); +} + +HMAC_CTX *pv_hmac_ctx_new(GBytes *key, const EVP_MD *md, GError **error) +{ + g_autoptr(HMAC_CTX) ctx = HMAC_CTX_new(); + const uint8_t *key_data; + size_t key_size; + + key_data = g_bytes_get_data(key, &key_size); + + if (HMAC_Init_ex(ctx, key_data, (int)key_size, md, NULL) != 1) { + g_set_error(error, PV_HASH_ERROR, PV_HASH_ERROR_INTERNAL, + "unable to create HMAC context: %s", pv_get_openssl_error()); + return NULL; + } + return g_steal_pointer(&ctx); +} + +int pv_hmac_ctx_update_raw(HMAC_CTX *ctx, const void *buf, size_t size, GError **error) +{ + if (!buf || size == 0) + return 0; + + if (HMAC_Update(ctx, buf, size) != 1) { + g_set_error(error, PV_HASH_ERROR, PV_HASH_ERROR_INTERNAL, + "unable to add data to HMAC context: %s", pv_get_openssl_error()); + return -1; + } + return 0; +} + +int pv_hmac_ctx_update(HMAC_CTX *ctx, GBytes *data, GError **error) +{ + const uint8_t *buf; + size_t buf_size; + + if (!data) + return 0; + buf = g_bytes_get_data((GBytes *)data, &buf_size); + return pv_hmac_ctx_update_raw(ctx, buf, buf_size, error); +} + +GBytes *pv_hamc_ctx_finalize(HMAC_CTX *ctx, GError **error) +{ + int md_size = EVP_MD_size(HMAC_CTX_get_md(ctx)); + g_autofree uint8_t *hmac = NULL; + unsigned int hmac_size = 0; + + g_assert(md_size > 0); + + hmac = g_malloc0((unsigned int)md_size); + + if (HMAC_Final(ctx, hmac, &hmac_size) != 1) { + g_set_error(error, PV_HASH_ERROR, PV_HASH_ERROR_INTERNAL, + "unable to calculate HMAC: %s", pv_get_openssl_error()); + return NULL; + } + return g_bytes_new_take(g_steal_pointer(&hmac), hmac_size); +}