]> git.ipfire.org Git - thirdparty/strongswan.git/commitdiff
pki: Added ocsp command
authorAndreas Steffen <andreas.steffen@strongswan.org>
Fri, 13 Oct 2023 19:25:19 +0000 (21:25 +0200)
committerTobias Brunner <tobias@strongswan.org>
Mon, 13 Nov 2023 11:40:58 +0000 (12:40 +0100)
The pki --ocsp command implements an OCSP responder.

src/pki/Makefile.am
src/pki/command.h
src/pki/commands/ocsp.c [new file with mode: 0644]

index b4c05318a7144b6fca91baaffde6f552824d6dec..1dbcd9a4446b3932dc1128595e8c587b4d170f55 100644 (file)
@@ -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 \
index d628a956fe4fa0e5c7e9a06e8645ee04c09abc45..f46b49de03a271c8943ef9188d9b7226cac34393 100644 (file)
@@ -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 (file)
index 0000000..44fdb89
--- /dev/null
@@ -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 <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * 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 <errno.h>
+#include <time.h>
+
+#include "pki.h"
+
+#include <credentials/sets/mem_cred.h>
+#include <credentials/certificates/ocsp_request.h>
+#include <credentials/certificates/ocsp_response.h>
+#include <credentials/certificates/ocsp_single_response.h>
+#include <credentials/certificates/ocsp_responder.h>
+
+/*
+ * 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)"},
+               }
+       });
+}