From: Andreas Steffen Date: Fri, 13 Oct 2023 19:25:19 +0000 (+0200) Subject: pki: Added ocsp command X-Git-Tag: 5.9.12rc1~3^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a0c9f9b8429d51b629aa8c00ede8b50e6fc4ba26;p=thirdparty%2Fstrongswan.git pki: Added ocsp command The pki --ocsp command implements an OCSP responder. --- diff --git a/src/pki/Makefile.am b/src/pki/Makefile.am index b4c05318a7..1dbcd9a444 100644 --- a/src/pki/Makefile.am +++ b/src/pki/Makefile.am @@ -10,6 +10,7 @@ pki_SOURCES = pki.c pki.h pki_cert.c pki_cert.h command.c command.h \ commands/gen.c \ commands/issue.c \ commands/keyid.c \ + commands/ocsp.c \ commands/pkcs12.c \ commands/pkcs7.c \ commands/print.c \ diff --git a/src/pki/command.h b/src/pki/command.h index d628a956fe..f46b49de03 100644 --- a/src/pki/command.h +++ b/src/pki/command.h @@ -25,7 +25,7 @@ /** * Maximum number of commands (+1). */ -#define MAX_COMMANDS 18 +#define MAX_COMMANDS 19 /** * Maximum number of options in a command (+3) diff --git a/src/pki/commands/ocsp.c b/src/pki/commands/ocsp.c new file mode 100644 index 0000000000..44fdb89d86 --- /dev/null +++ b/src/pki/commands/ocsp.c @@ -0,0 +1,601 @@ +/* + * Copyright (C) 2023 Andreas Steffen, strongSec GmbH + * + * Copyright (C) secunet Security Networks AG + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See . + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include +#include + +#include "pki.h" + +#include +#include +#include +#include +#include + +/* + * Verifies the optional OCSP request signature generated by an OCSP requestor + */ +static bool verify_signature(certificate_t *ocsp_req, mem_cred_t *creds) +{ + identification_t *signer; + certificate_t *signer_cert, *cert_found; + ocsp_request_t *ocsp_request; + enumerator_t *certs; + bool trusted = TRUE; + + ocsp_request = (ocsp_request_t*)ocsp_req; + + signer_cert = ocsp_request->get_signer_cert(ocsp_request); + if (signer_cert) + { + signer = signer_cert->get_subject(signer_cert); + + /* establish trust relative to root CA */ + creds->add_cert(creds, FALSE, signer_cert->get_ref(signer_cert)); + certs = lib->credmgr->create_trusted_enumerator(lib->credmgr, + KEY_ANY, signer, FALSE); + trusted = certs->enumerate(certs, &cert_found, NULL) && + (cert_found == signer_cert); + certs->destroy(certs); + trusted = trusted && ocsp_req->issued_by(ocsp_req, signer_cert, NULL); + } + DBG1(DBG_APP, "requestor is %strusted", trusted ? "" : "not "); + + return trusted; +} + +/* + * Find the CA certificate of the certificate issuer based on the issuerKey and + * issuerName hashes contained in the OCSP request + */ +static bool find_issuer_cacert(hash_algorithm_t hashAlgorithm, + chunk_t issuerKeyHash, chunk_t issuerNameHash, + certificate_t **issuer_cacert) +{ + bool issuerKeyHash_ok = FALSE, issuerNameHash_ok = FALSE; + certificate_t *candidate; + identification_t *issuer; + public_key_t *public; + x509_t *x509_ca; + chunk_t caKeyHash = chunk_empty, caNameHash = chunk_empty; + hasher_t *hasher; + enumerator_t *certs; + + *issuer_cacert = NULL; + + if (hashAlgorithm != HASH_SHA1) + { + return FALSE; + } + certs = lib->credmgr->create_cert_enumerator(lib->credmgr, CERT_X509, + KEY_ANY, NULL, TRUE); + while (certs->enumerate(certs, &candidate)) + { + x509_ca = (x509_t*)candidate; + + if (!(x509_ca->get_flags(x509_ca) & X509_CA)) + { + continue; + } + + /* retrieve the caKeyHash of the candidate issuer */ + public = candidate->get_public_key(candidate); + if (!public) + { + DBG1(DBG_APP, "could not retrieve public key of cacert candidate"); + break; + } + if (!public->get_fingerprint(public, KEYID_PUBKEY_SHA1, &caKeyHash)) + { + DBG1(DBG_APP, "could not retrieve SHA1 hash of public key"); + public->destroy(public); + break; + } + + issuerKeyHash_ok = chunk_equals_const(issuerKeyHash, caKeyHash); + public->destroy(public); + if (!issuerKeyHash_ok) + { + continue; + } + + /* compute the caNameHash of the candidate issuer */ + issuer = candidate->get_subject(candidate); + + hasher = lib->crypto->create_hasher(lib->crypto, HASH_SHA1); + if (!hasher) + { + DBG1(DBG_APP, "failed to create SHA1 hasher"); + break; + } + + if (!hasher->allocate_hash(hasher, issuer->get_encoding(issuer), + &caNameHash)) + { + hasher->destroy(hasher); + DBG1(DBG_APP, "failed to compute SHA1 caNameHash"); + break; + } + hasher->destroy(hasher); + + issuerNameHash_ok = chunk_equals_const(issuerNameHash, caNameHash); + chunk_free(&caNameHash); + if (!issuerNameHash_ok) + { + continue; + } + + /* we found the issuer */ + DBG1(DBG_APP, "issuer: \"%Y\"", issuer); + *issuer_cacert = candidate; + + break; + } + certs->destroy(certs); + + DBG1(DBG_APP, " issuerKeyHash: %#B (%s)", &issuerKeyHash, + issuerKeyHash_ok ? "ok" : "no match"); + DBG1(DBG_APP, " issuerNameHash: %#B (%s)", &issuerNameHash, + issuerNameHash_ok ? "ok" : "no match"); + + return (*issuer_cacert != NULL); +} + +/* + * Find an OCSP signer certificate. Either the certificate of the CA itself that + * issued the end entitity certificate, the certificate of an OCSP signer + * delegated by the CA via the standard OCSPSigning Extended Key Usage (EKU) + * flag or a self-signed OCSP signer certificate when multiple issuer OCSP + * requests have to be supported. + */ +static void find_ocsp_signer(certificate_t *first_issuer, bool *self_signed, + certificate_t **cert, private_key_t **key) +{ + certificate_t *candidate; + identification_t *keyid; + private_key_t *key_candidate; + chunk_t subKeyId, authKeyId, subKeyId_ca; + x509_t *x509, *x509_ca; + x509_flag_t flags; + enumerator_t *certs; + + /* retrieve the subjectKeyIdentifier of the first issuer certificate */ + x509_ca = (x509_t*)first_issuer; + subKeyId_ca = x509_ca->get_subjectKeyIdentifier(x509_ca); + + /* iterate over all certificates */ + certs = lib->credmgr->create_cert_enumerator(lib->credmgr, CERT_X509, + KEY_ANY, NULL, TRUE); + while (certs->enumerate(certs, &candidate)) + { + /* get the flags and key identfiers of the candidate certificate */ + x509 = (x509_t*)candidate; + flags = x509->get_flags(x509); + subKeyId = x509->get_subjectKeyIdentifier(x509); + authKeyId = x509->get_authKeyIdentifier(x509); + + /* get a private key matching the candidate certificate */ + keyid = identification_create_from_encoding(ID_KEY_ID, subKeyId); + key_candidate = lib->credmgr->get_private(lib->credmgr, + KEY_ANY, keyid, NULL); + keyid->destroy(keyid); + + if (key_candidate) + { + if (((flags & X509_OCSP_SIGNER && chunk_equals(authKeyId, subKeyId_ca)) || + (flags & X509_CA && chunk_equals(subKeyId, subKeyId_ca))) && !*key) + { + *cert = candidate; + *key = key_candidate; + continue; + } + else if (flags & X509_SELF_SIGNED && !(flags & X509_CA)) + { + DESTROY_IF(*key); + *cert = candidate; + *key = key_candidate; + *self_signed = TRUE; + break; + } + key_candidate->destroy(key_candidate); + } + } + certs->destroy(certs); +} + +/** + * Show|Respond to OCSP requests + */ +static int ocsp() +{ + char *arg, *file = NULL, *error = NULL; + identification_t *requestor; + cred_encoding_type_t form = CERT_ASN1_DER; + private_key_t *key = NULL; + certificate_t *cert = NULL, *ocsp_req = NULL, *ocsp_resp = NULL; + certificate_t *cacert = NULL, *first_issuer = NULL; + ocsp_request_t *ocsp_request; + ocsp_status_t ocsp_status = OCSP_SUCCESSFUL; + ocsp_responder_t *ocsp_responder = NULL; + linked_list_t *responses = NULL; + chunk_t encoding = chunk_empty, nonce = chunk_empty; + chunk_t issuerNameHash, issuerKeyHash, serialNumber; + hash_algorithm_t hashAlgorithm = HASH_SHA1, digest = HASH_UNKNOWN; + signature_params_t *scheme = NULL; + time_t lifetime = 0; + mem_cred_t *creds; + bool multiple_issuers = FALSE, self_signed = FALSE; + enumerator_t *enumerator; + int res = 1; + + enum { + OP_SHOW, + OP_RESPOND, + } op = OP_SHOW; + + bool pss = lib->settings->get_bool(lib->settings, "%s.rsa_pss", FALSE, + lib->ns); + + creds = mem_cred_create(); + + while (TRUE) + { + switch (command_getopt(&arg)) + { + case 'h': + goto usage; + case 'i': + file = arg; + continue; + case 'r': + op = OP_RESPOND; + continue; + case 'k': + key = lib->creds->create(lib->creds, + CRED_PRIVATE_KEY, KEY_ANY, + BUILD_FROM_FILE, arg, BUILD_END); + if (!key) + { + error = "parsing private key failed"; + goto usage; + } + creds->add_key(creds, key); + continue; + case 'c': + cert = lib->creds->create(lib->creds, + CRED_CERTIFICATE, CERT_X509, + BUILD_FROM_FILE, arg, BUILD_END); + if (!cert) + { + error = "parsing certificate failed"; + goto usage; + } + creds->add_cert(creds, TRUE, cert); + continue; + case 'C': + cacert = lib->creds->create(lib->creds, + CRED_CERTIFICATE, CERT_X509, + BUILD_FROM_FILE, arg, BUILD_END); + if (!cacert) + { + error = "parsing CA certificate failed"; + goto usage; + } + creds->add_cert(creds, TRUE, cacert); + continue; + case 'l': + lifetime = atoi(arg) * 60; + if (!lifetime) + { + error = "invalid --lifetime value"; + goto usage; + } + continue; + case 'g': + if (!enum_from_name(hash_algorithm_short_names, arg, &digest)) + { + error = "invalid --digest type"; + goto usage; + } + continue; + case 'R': + if (!parse_rsa_padding(arg, &pss)) + { + error = "invalid RSA padding"; + goto usage; + } + continue; + case EOF: + break; + default: + error = "invalid --ocsp option"; + goto usage; + } + break; + } + + lib->credmgr->add_local_set(lib->credmgr, &creds->set, FALSE); + + responses = linked_list_create(); + + if (op == OP_RESPOND && !cacert) + { + error = "respond mode requires a ca certificate"; + goto end; + } + + if (op == OP_RESPOND && !key) + { + error = "respond mode requires a private signer key"; + goto end; + } + + /* re-initialize signing certificate and key pointers */ + cert = NULL; + key = NULL; + + if (file) + { + ocsp_req = lib->creds->create(lib->creds, CRED_CERTIFICATE, + CERT_X509_OCSP_REQUEST, + BUILD_FROM_FILE, file, BUILD_END); + } + else + { + chunk_t chunk; + + set_file_mode(stdin, CERT_ASN1_DER); + if (!chunk_from_fd(0, &chunk)) + { + fprintf(stderr, "%s: ", strerror(errno)); + error = "reading certificate request failed"; + goto end; + } + ocsp_req = lib->creds->create(lib->creds, CRED_CERTIFICATE, + CERT_X509_OCSP_REQUEST, + BUILD_BLOB_ASN1_DER, chunk, BUILD_END); + free(chunk.ptr); + } + if (!ocsp_req) + { + if (op == OP_SHOW) + { + error = "malformed OCSP request"; + goto end; + } + else + { + DBG1(DBG_APP, "malformed OCSP request"); + ocsp_status = OCSP_MALFORMEDREQUEST; + goto gen; + } + } + ocsp_request = (ocsp_request_t*)ocsp_req; + + /* does the requestor identify itself? */ + requestor = ocsp_req->get_subject(ocsp_req); + if (requestor) + { + DBG1(DBG_APP, "requestor: \"%Y\"", requestor); + + /* verify an optional ocsp request signature */ + if (!verify_signature(ocsp_req, creds)) + { + ocsp_status = OCSP_UNAUTHORIZED; + goto gen; + } + } + + /* extract nonce from OCSP request */ + nonce = ocsp_request->get_nonce(ocsp_request); + if (nonce.len > 0) + { + DBG1(DBG_APP, "nonce: %#B", &nonce); + } + + /* check for an ocsp responder */ + if (op == OP_RESPOND) + { + ocsp_responder = lib->get(lib, "ocsp-responder"); + if (!ocsp_responder) + { + DBG1(DBG_APP, " no ocsp-responder found"); + ocsp_status = OCSP_INTERNALERROR; + goto gen; + } + } + + /* enumerate over the ocsp requests and try to identify the issuers */ + enumerator = ocsp_request->create_request_enumerator(ocsp_request); + while (enumerator->enumerate(enumerator, &hashAlgorithm, &issuerNameHash, + &issuerKeyHash, &serialNumber)) + { + certificate_t *issuer_cacert = NULL; + cert_validation_t status = VALIDATION_FAILED; + ocsp_single_response_t *response = NULL; + crl_reason_t revocationReason; + time_t revocationTime; + + /* search for the matching issuer cacert */ + if (find_issuer_cacert(hashAlgorithm, issuerKeyHash, issuerNameHash, + &issuer_cacert)) + { + if (!first_issuer) + { + first_issuer = issuer_cacert; + + /* search for a signing certificate plus matching private key */ + if (op == OP_RESPOND) + { + find_ocsp_signer(first_issuer, &self_signed, &cert, &key); + } + } + else if (first_issuer != issuer_cacert) + { + multiple_issuers = TRUE; + } + } + DBG1(DBG_APP, " serialNumber: %#B", &serialNumber); + + if (op == OP_SHOW) + { + continue; + } + + /** + * fill in the OCSP single response + */ + response = ocsp_single_response_create(); + response->hashAlgorithm = hashAlgorithm; + response->issuerNameHash = chunk_clone(issuerNameHash); + response->issuerKeyHash = chunk_clone(issuerKeyHash); + response->serialNumber = chunk_clone(serialNumber); + response->thisUpdate = time(NULL); + DBG1(DBG_APP, " thisUpdate: %#T", &response->thisUpdate, TRUE); + + if (lifetime) + { + response->nextUpdate = response->thisUpdate + lifetime; + DBG1(DBG_APP, " nextUpdate: %#T", &response->nextUpdate, TRUE); + } + + if (issuer_cacert && (issuer_cacert == first_issuer || self_signed)) + { + status = ocsp_responder->get_status(ocsp_responder, + issuer_cacert, serialNumber, + &revocationTime, &revocationReason); + } + DBG1(DBG_APP, " certValidation: %N", cert_validation_names, status); + response->status = status; + + if (status == VALIDATION_REVOKED || status == VALIDATION_ON_HOLD) + { + DBG1(DBG_APP, " revocationTime: %T", &revocationTime, TRUE); + DBG1(DBG_APP, " revocationReason: %N", crl_reason_names, + revocationReason); + response->revocationTime = revocationTime; + response->revocationReason = revocationReason; + } + responses->insert_last(responses, response); + } + enumerator->destroy(enumerator); + + if (multiple_issuers) + { + DBG1(DBG_APP, "there are multiple known issuers"); + } + +gen: + if (op == OP_RESPOND) + { + if (cert) + { + DBG1(DBG_APP, "%s \"%Y\"", + self_signed ? "self-signed signer:" : "trusted signer: ", + cert->get_subject(cert)); + + scheme = get_signature_scheme(key, digest, pss); + if (scheme) + { + if (digest == HASH_UNKNOWN) + { + digest = hasher_from_signature_scheme(scheme->scheme, + scheme->params); + } + } + else + { + DBG1(DBG_APP, "no signature scheme found"); + ocsp_status = OCSP_INTERNALERROR; + } + } + DBG1(DBG_APP, "ocspResponseStatus: %N", ocsp_status_names, ocsp_status); + + enumerator = responses->create_enumerator(responses); + ocsp_resp = lib->creds->create(lib->creds, CRED_CERTIFICATE, + CERT_X509_OCSP_RESPONSE, + BUILD_OCSP_STATUS, ocsp_status, + BUILD_OCSP_RESPONSES, enumerator, + BUILD_SIGNING_KEY, key, + BUILD_SIGNING_CERT, cert, + BUILD_SIGNATURE_SCHEME, scheme, + BUILD_NONCE, nonce, + BUILD_END); + enumerator->destroy(enumerator); + + if (!ocsp_resp) + { + error = "generating OCSP response failed"; + goto end; + } + if (!ocsp_resp->get_encoding(ocsp_resp, form, &encoding)) + { + error = "encoding OCSP response failed"; + goto end; + } + set_file_mode(stdout, form); + if (fwrite(encoding.ptr, encoding.len, 1, stdout) != 1) + { + error = "writing OCSP response failed"; + goto end; + } + } + res = 0; + +end: + DESTROY_IF(key); + lib->credmgr->remove_local_set(lib->credmgr, &creds->set); + creds->destroy(creds); + responses->destroy_offset(responses, + offsetof(ocsp_single_response_t, destroy)); + DESTROY_IF(ocsp_req); + DESTROY_IF(ocsp_resp); + signature_params_destroy(scheme); + free(encoding.ptr); + if (error) + { + fprintf(stderr, "%s\n", error); + } + return res; + +usage: + creds->destroy(creds); + return command_usage(error); +} + +/** + * Register the command. + */ +static void __attribute__ ((constructor))reg() +{ + command_register((command_t) { + ocsp, 'o', "ocsp", "OCSP responder", + {"[--in file] [--respond] [--cert file]+ [--key file]+ [--cacert file]+ ", + "[--digest md5|sha1|sha224|sha256|sha384|sha512|sha3_224|sha3_256|sha3_384|sha3_512]", + "[--rsa-padding pkcs1|pss] [--lifetime minutes]"}, + { + {"help", 'h', 0, "show usage information"}, + {"respond", 'r', 0, "respond to OCSP request with OCSP response"}, + {"in", 'i', 1, "input file, default: stdin"}, + {"key", 'k', 1, "path to OCSP signing private key (can be used multiple times)"}, + {"cert", 'c', 1, "path to OCSP signing certificate (can be used multiple times"}, + {"cacert", 'C', 1, "CA certificate (can be used multiple times"}, + {"digest", 'g', 1, "digest for signature creation, default: key-specific"}, + {"rsa-padding", 'R', 1, "padding for RSA signatures, default: pkcs1"}, + {"lifetime", 'l', 1, "validity in minutes of the OCSP response (if missing, nextUpdate is omitted)"}, + } + }); +}