// SPDX-License-Identifier: GPL-2.0+ /* * Signature support for 'fsverity setup' * * Copyright (C) 2018 Google LLC * * Written by Eric Biggers. */ #include <fcntl.h> #include <limits.h> #include <openssl/bio.h> #include <openssl/err.h> #include <openssl/pem.h> #include <openssl/pkcs7.h> #include <stdlib.h> #include <string.h> #include "fsverity_uapi.h" #include "fsveritysetup.h" #include "hash_algs.h" static void __printf(1, 2) __cold error_msg_openssl(const char *format, ...) { va_list va; va_start(va, format); do_error_msg(format, va, 0); va_end(va); if (ERR_peek_error() == 0) return; fprintf(stderr, "OpenSSL library errors:\n"); ERR_print_errors_fp(stderr); } /* Read a PEM PKCS#8 formatted private key */ static EVP_PKEY *read_private_key(const char *keyfile) { BIO *bio; EVP_PKEY *pkey; bio = BIO_new_file(keyfile, "r"); if (!bio) { error_msg_openssl("can't open '%s' for reading", keyfile); return NULL; } pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); if (!pkey) { error_msg_openssl("Failed to parse private key file '%s'.\n" " Note: it must be in PEM PKCS#8 format.", keyfile); } BIO_free(bio); return pkey; } /* Read a PEM X.509 formatted certificate */ static X509 *read_certificate(const char *certfile) { BIO *bio; X509 *cert; bio = BIO_new_file(certfile, "r"); if (!bio) { error_msg_openssl("can't open '%s' for reading", certfile); return NULL; } cert = PEM_read_bio_X509(bio, NULL, NULL, NULL); if (!cert) { error_msg_openssl("Failed to parse X.509 certificate file '%s'.\n" " Note: it must be in PEM format.", certfile); } BIO_free(bio); return cert; } /* * Check that the given data is a valid 'struct fsverity_digest_disk' that * matches the given @expected_digest and @hash_alg. * * Return: NULL if the digests match, else a string describing the difference. */ static const char * compare_fsverity_digest(const void *data, size_t size, const u8 *expected_digest, const struct fsverity_hash_alg *hash_alg) { const struct fsverity_digest_disk *d = data; if (size != sizeof(*d) + hash_alg->digest_size) return "unexpected length"; if (le16_to_cpu(d->digest_algorithm) != hash_alg - fsverity_hash_algs) return "unexpected hash algorithm"; if (le16_to_cpu(d->digest_size) != hash_alg->digest_size) return "wrong digest size for hash algorithm"; if (memcmp(expected_digest, d->digest, hash_alg->digest_size)) return "wrong digest"; return NULL; } #ifdef OPENSSL_IS_BORINGSSL static bool sign_pkcs7(const void *data_to_sign, size_t data_size, EVP_PKEY *pkey, X509 *cert, const EVP_MD *md, void **sig_ret, int *sig_size_ret) { CBB out, outer_seq, wrapped_seq, seq, digest_algos_set, digest_algo, null, content_info, issuer_and_serial, signed_data, wrapped_signed_data, signer_infos, signer_info, sign_algo, signature; EVP_MD_CTX md_ctx; u8 *name_der = NULL, *sig = NULL, *pkcs7_data = NULL; size_t pkcs7_data_len, sig_len; int name_der_len, sig_nid; bool ok = false; EVP_MD_CTX_init(&md_ctx); BIGNUM *serial = ASN1_INTEGER_to_BN(X509_get_serialNumber(cert), NULL); if (!CBB_init(&out, 1024)) { error_msg("out of memory"); goto out; } name_der_len = i2d_X509_NAME(X509_get_subject_name(cert), &name_der); if (name_der_len < 0) { error_msg_openssl("i2d_X509_NAME failed"); goto out; } if (!EVP_DigestSignInit(&md_ctx, NULL, md, NULL, pkey)) { error_msg_openssl("EVP_DigestSignInit failed"); goto out; } sig_len = EVP_PKEY_size(pkey); sig = xmalloc(sig_len); if (!EVP_DigestSign(&md_ctx, sig, &sig_len, data_to_sign, data_size)) { error_msg_openssl("EVP_DigestSign failed"); goto out; } sig_nid = EVP_PKEY_id(pkey); /* To mirror OpenSSL behaviour, always use |NID_rsaEncryption| with RSA * rather than the combined hash+pkey NID. */ if (sig_nid != NID_rsaEncryption) { OBJ_find_sigid_by_algs(&sig_nid, EVP_MD_type(md), EVP_PKEY_id(pkey)); } // See https://tools.ietf.org/html/rfc2315#section-7 if (!CBB_add_asn1(&out, &outer_seq, CBS_ASN1_SEQUENCE) || !OBJ_nid2cbb(&outer_seq, NID_pkcs7_signed) || !CBB_add_asn1(&outer_seq, &wrapped_seq, CBS_ASN1_CONTEXT_SPECIFIC | CBS_ASN1_CONSTRUCTED | 0) || // See https://tools.ietf.org/html/rfc2315#section-9.1 !CBB_add_asn1(&wrapped_seq, &seq, CBS_ASN1_SEQUENCE) || !CBB_add_asn1_uint64(&seq, 1 /* version */) || !CBB_add_asn1(&seq, &digest_algos_set, CBS_ASN1_SET) || !CBB_add_asn1(&digest_algos_set, &digest_algo, CBS_ASN1_SEQUENCE) || !OBJ_nid2cbb(&digest_algo, EVP_MD_type(md)) || !CBB_add_asn1(&digest_algo, &null, CBS_ASN1_NULL) || !CBB_add_asn1(&seq, &content_info, CBS_ASN1_SEQUENCE) || !OBJ_nid2cbb(&content_info, NID_pkcs7_data) || !CBB_add_asn1( &content_info, &signed_data, CBS_ASN1_CONTEXT_SPECIFIC | CBS_ASN1_CONSTRUCTED | 0) || !CBB_add_asn1(&signed_data, &wrapped_signed_data, CBS_ASN1_OCTETSTRING) || !CBB_add_bytes(&wrapped_signed_data, (const u8 *)data_to_sign, data_size) || !CBB_add_asn1(&seq, &signer_infos, CBS_ASN1_SET) || !CBB_add_asn1(&signer_infos, &signer_info, CBS_ASN1_SEQUENCE) || !CBB_add_asn1_uint64(&signer_info, 1 /* version */) || !CBB_add_asn1(&signer_info, &issuer_and_serial, CBS_ASN1_SEQUENCE) || !CBB_add_bytes(&issuer_and_serial, name_der, name_der_len) || !BN_marshal_asn1(&issuer_and_serial, serial) || !CBB_add_asn1(&signer_info, &digest_algo, CBS_ASN1_SEQUENCE) || !OBJ_nid2cbb(&digest_algo, EVP_MD_type(md)) || !CBB_add_asn1(&digest_algo, &null, CBS_ASN1_NULL) || !CBB_add_asn1(&signer_info, &sign_algo, CBS_ASN1_SEQUENCE) || !OBJ_nid2cbb(&sign_algo, sig_nid) || !CBB_add_asn1(&sign_algo, &null, CBS_ASN1_NULL) || !CBB_add_asn1(&signer_info, &signature, CBS_ASN1_OCTETSTRING) || !CBB_add_bytes(&signature, sig, sig_len) || !CBB_finish(&out, &pkcs7_data, &pkcs7_data_len)) { error_msg_openssl("failed to construct PKCS#7 data"); goto out; } *sig_ret = xmemdup(pkcs7_data, pkcs7_data_len); *sig_size_ret = pkcs7_data_len; ok = true; out: BN_free(serial); EVP_MD_CTX_cleanup(&md_ctx); CBB_cleanup(&out); free(sig); OPENSSL_free(name_der); OPENSSL_free(pkcs7_data); return ok; } static const char * compare_fsverity_digest_pkcs7(const void *sig, size_t sig_len, const u8 *expected_measurement, const struct fsverity_hash_alg *hash_alg) { CBS in, content_info, content_type, wrapped_signed_data, signed_data, content, wrapped_data, data; u64 version; CBS_init(&in, sig, sig_len); if (!CBS_get_asn1(&in, &content_info, CBS_ASN1_SEQUENCE) || !CBS_get_asn1(&content_info, &content_type, CBS_ASN1_OBJECT) || (OBJ_cbs2nid(&content_type) != NID_pkcs7_signed) || !CBS_get_asn1( &content_info, &wrapped_signed_data, CBS_ASN1_CONTEXT_SPECIFIC | CBS_ASN1_CONSTRUCTED | 0) || !CBS_get_asn1(&wrapped_signed_data, &signed_data, CBS_ASN1_SEQUENCE) || !CBS_get_asn1_uint64(&signed_data, &version) || (version < 1) || !CBS_get_asn1(&signed_data, NULL /* digests */, CBS_ASN1_SET) || !CBS_get_asn1(&signed_data, &content, CBS_ASN1_SEQUENCE) || !CBS_get_asn1(&content, &content_type, CBS_ASN1_OBJECT) || (OBJ_cbs2nid(&content_type) != NID_pkcs7_data) || !CBS_get_asn1(&content, &wrapped_data, CBS_ASN1_CONTEXT_SPECIFIC | CBS_ASN1_CONSTRUCTED | 0) || !CBS_get_asn1(&wrapped_data, &data, CBS_ASN1_OCTETSTRING)) { return "invalid PKCS#7 data"; } return compare_fsverity_digest(CBS_data(&data), CBS_len(&data), expected_measurement, hash_alg); } #else /* OPENSSL_IS_BORINGSSL */ static BIO *new_mem_buf(const void *buf, size_t size) { BIO *bio; ASSERT(size <= INT_MAX); /* * Prior to OpenSSL 1.1.0, BIO_new_mem_buf() took a non-const pointer, * despite still marking the resulting bio as read-only. So cast away * the const to avoid a compiler warning with older OpenSSL versions. */ bio = BIO_new_mem_buf((void *)buf, size); if (!bio) error_msg_openssl("out of memory"); return bio; } static bool sign_pkcs7(const void *data_to_sign, size_t data_size, EVP_PKEY *pkey, X509 *cert, const EVP_MD *md, void **sig_ret, int *sig_size_ret) { /* * PKCS#7 signing flags: * * - PKCS7_BINARY signing binary data, so skip MIME translation * * - PKCS7_NOATTR omit extra authenticated attributes, such as * SMIMECapabilities * * - PKCS7_NOCERTS omit the signer's certificate * * - PKCS7_PARTIAL PKCS7_sign() creates a handle only, then * PKCS7_sign_add_signer() can add a signer later. * This is necessary to change the message digest * algorithm from the default of SHA-1. Requires * OpenSSL 1.0.0 or later. */ int pkcs7_flags = PKCS7_BINARY | PKCS7_NOATTR | PKCS7_NOCERTS | PKCS7_PARTIAL; void *sig; int sig_size; BIO *bio = NULL; PKCS7 *p7 = NULL; bool ok = false; bio = new_mem_buf(data_to_sign, data_size); if (!bio) goto out; p7 = PKCS7_sign(NULL, NULL, NULL, bio, pkcs7_flags); if (!p7) { error_msg_openssl("failed to initialize PKCS#7 signature object"); goto out; } if (!PKCS7_sign_add_signer(p7, cert, pkey, md, pkcs7_flags)) { error_msg_openssl("failed to add signer to PKCS#7 signature object"); goto out; } if (PKCS7_final(p7, bio, pkcs7_flags) != 1) { error_msg_openssl("failed to finalize PKCS#7 signature"); goto out; } BIO_free(bio); bio = BIO_new(BIO_s_mem()); if (!bio) { error_msg_openssl("out of memory"); goto out; } if (i2d_PKCS7_bio(bio, p7) != 1) { error_msg_openssl("failed to DER-encode PKCS#7 signature object"); goto out; } sig_size = BIO_get_mem_data(bio, &sig); *sig_ret = xmemdup(sig, sig_size); *sig_size_ret = sig_size; ok = true; out: PKCS7_free(p7); BIO_free(bio); return ok; } static const char * compare_fsverity_digest_pkcs7(const void *sig, size_t sig_len, const u8 *expected_measurement, const struct fsverity_hash_alg *hash_alg) { BIO *bio = NULL; PKCS7 *p7 = NULL; const char *reason = NULL; bio = new_mem_buf(sig, sig_len); if (!bio) return "out of memory"; p7 = d2i_PKCS7_bio(bio, NULL); if (!p7) { reason = "failed to decode PKCS#7 signature"; goto out; } if (OBJ_obj2nid(p7->type) != NID_pkcs7_signed || OBJ_obj2nid(p7->d.sign->contents->type) != NID_pkcs7_data) { reason = "unexpected PKCS#7 content type"; } else { const ASN1_OCTET_STRING *o = p7->d.sign->contents->d.data; reason = compare_fsverity_digest(o->data, o->length, expected_measurement, hash_alg); } out: BIO_free(bio); PKCS7_free(p7); return reason; } #endif /* !OPENSSL_IS_BORINGSSL */ /* * Sign the specified @data_to_sign of length @data_size bytes using the private * key in @keyfile, the certificate in @certfile, and the hash algorithm * @hash_alg. Returns the DER-formatted PKCS#7 signature, with the signed data * included (not detached), in @sig_ret and @sig_size_ret. */ static bool sign_data(const void *data_to_sign, size_t data_size, const char *keyfile, const char *certfile, const struct fsverity_hash_alg *hash_alg, void **sig_ret, int *sig_size_ret) { EVP_PKEY *pkey = NULL; X509 *cert = NULL; const EVP_MD *md; bool ok = false; pkey = read_private_key(keyfile); if (!pkey) goto out; cert = read_certificate(certfile); if (!cert) goto out; OpenSSL_add_all_digests(); ASSERT(hash_alg->cryptographic); md = EVP_get_digestbyname(hash_alg->name); if (!md) { fprintf(stderr, "Warning: '%s' algorithm not found in OpenSSL library.\n" " Falling back to SHA-256 signature.\n", hash_alg->name); md = EVP_sha256(); } ok = sign_pkcs7(data_to_sign, data_size, pkey, cert, md, sig_ret, sig_size_ret); out: EVP_PKEY_free(pkey); X509_free(cert); return ok; } /* * Read a file measurement signature in PKCS#7 DER format from @signature_file, * validate that the signed data matches the expected measurement, then return * the PKCS#7 DER message in @sig_ret and @sig_size_ret. */ static bool read_signature(const char *signature_file, const u8 *expected_measurement, const struct fsverity_hash_alg *hash_alg, void **sig_ret, int *sig_size_ret) { struct filedes file = { .fd = -1 }; u64 filesize; void *sig = NULL; bool ok = false; const char *reason; if (!open_file(&file, signature_file, O_RDONLY, 0)) goto out; if (!get_file_size(&file, &filesize)) goto out; if (filesize <= 0) { error_msg("signature file '%s' is empty", signature_file); goto out; } if (filesize > 1000000) { error_msg("signature file '%s' is too large", signature_file); goto out; } sig = xmalloc(filesize); if (!full_read(&file, sig, filesize)) goto out; reason = compare_fsverity_digest_pkcs7(sig, filesize, expected_measurement, hash_alg); if (reason) { error_msg("signed file measurement from '%s' is invalid (%s)", signature_file, reason); goto out; } printf("Using existing signed file measurement from '%s'\n", signature_file); *sig_ret = sig; *sig_size_ret = filesize; sig = NULL; ok = true; out: filedes_close(&file); free(sig); return ok; } static bool write_signature(const char *signature_file, const void *sig, int sig_size) { struct filedes file; bool ok; if (!open_file(&file, signature_file, O_WRONLY|O_CREAT|O_TRUNC, 0644)) return false; ok = full_write(&file, sig, sig_size); ok &= filedes_close(&file); if (ok) printf("Wrote signed file measurement to '%s'\n", signature_file); return ok; } /* * Append the signed file measurement to the output file as a PKCS7_SIGNATURE * extension item. * * Return: exit status code (0 on success, nonzero on failure) */ int append_signed_measurement(struct filedes *out, const struct fsveritysetup_params *params, const u8 *measurement) { struct fsverity_digest_disk *data_to_sign = NULL; void *sig = NULL; void *extbuf = NULL; void *tmp; int sig_size; int status; if (params->signing_key_file) { size_t data_size = sizeof(*data_to_sign) + params->hash_alg->digest_size; /* Sign the file measurement using the given key */ data_to_sign = xzalloc(data_size); data_to_sign->digest_algorithm = cpu_to_le16(params->hash_alg - fsverity_hash_algs); data_to_sign->digest_size = cpu_to_le16(params->hash_alg->digest_size); memcpy(data_to_sign->digest, measurement, params->hash_alg->digest_size); ASSERT(compare_fsverity_digest(data_to_sign, data_size, measurement, params->hash_alg) == NULL); if (!sign_data(data_to_sign, data_size, params->signing_key_file, params->signing_cert_file ?: params->signing_key_file, params->hash_alg, &sig, &sig_size)) goto out_err; if (params->signature_file && !write_signature(params->signature_file, sig, sig_size)) goto out_err; } else { /* Using a signature that was already created */ if (!read_signature(params->signature_file, measurement, params->hash_alg, &sig, &sig_size)) goto out_err; } tmp = extbuf = xzalloc(FSVERITY_EXTLEN(sig_size)); fsverity_append_extension(&tmp, FS_VERITY_EXT_PKCS7_SIGNATURE, sig, sig_size); ASSERT(tmp == extbuf + FSVERITY_EXTLEN(sig_size)); if (!full_write(out, extbuf, FSVERITY_EXTLEN(sig_size))) goto out_err; status = 0; out: free(data_to_sign); free(sig); free(extbuf); return status; out_err: status = 1; goto out; }