/** * Copyright (C) 2021 Graham Leggett * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /* * redwax_nss - NSS routines for munching certificates * */ /* * To read notBefore and notAfter, use CERT_GetCertTimes(). * * PRTime is equivalent to time_t (apr_time_t). */ #include #include #include #include "config.h" #include "redwax-tool.h" #include "redwax_util.h" #if HAVE_NSS_INITIALIZE #include #include #include #include #include #include #include #define REDWAX_NSS_MAX HUGE_STRING_LEN #define REDWAX_NSS_INTERNAL_SOFTWARE_TOKEN "Internal (Software) Token" typedef struct redwax_nss_secret_t { redwax_tool_t *r; apr_pool_t *pool; const char *what; apr_hash_t *secrets; const char *file; int verify; } redwax_nss_secret_t; module nss_module; static apr_status_t cleanup_nss(void *dummy) { if (dummy) { SECStatus rv = NSS_ShutdownContext(dummy); if (rv != SECSuccess) { fprintf(stderr, "Could not shutdown NSS database: %s\n", PR_ErrorToName(PR_GetError())); } } return APR_SUCCESS; } static apr_status_t cleanup_slot(void *dummy) { if (dummy) { PK11_FreeSlot(dummy); } return APR_SUCCESS; } static apr_status_t cleanup_slotlist(void *dummy) { if (dummy) { PK11_FreeSlotList(dummy); } return APR_SUCCESS; } static apr_status_t cleanup_cert(void *dummy) { if (dummy) { CERT_DestroyCertificate(dummy); } return APR_SUCCESS; } static apr_status_t cleanup_key(void *dummy) { if (dummy) { SECKEY_DestroyPrivateKey(dummy); } return APR_SUCCESS; } static apr_status_t cleanup_free(void *dummy) { if (dummy) { PORT_Free(dummy); } return APR_SUCCESS; } static apr_status_t redwax_nss_initialise(redwax_tool_t *r) { return APR_SUCCESS; } static char *redwax_nss_password_cb(PK11SlotInfo *slot, PRBool retry, void *arg) { apr_pool_t *pool; redwax_nss_secret_t *s = arg; redwax_tool_t *r = s->r; const char *what = s->what; apr_hash_t *secrets = s->secrets; const char *file = s->file; const char *name = PK11_GetSlotName(slot); apr_pool_create(&pool, s->pool); apr_size_t min = PK11_GetMinimumPwdLength(slot); /* apr_size_t max = slot->maxPassword; */ apr_size_t max = REDWAX_NSS_MAX; apr_status_t status; /* * Obtain a secret to encrypt a key. * * Secret file specified and secret file exists, use that secret. * * No secret file specified, ask for the secret twice. */ if (secrets) { char *pin; if (PK11_IsInternal(slot)) { pin = apr_hash_get(secrets, REDWAX_NSS_INTERNAL_SOFTWARE_TOKEN, APR_HASH_KEY_STRING); name = REDWAX_NSS_INTERNAL_SOFTWARE_TOKEN; } else { pin = apr_hash_get(secrets, name, APR_HASH_KEY_STRING); } /* pass this way just once */ s->secrets = NULL; if (pin) { int len; len = strlen(pin); if (len < min) { redwax_print_error(r, "Passphrase for '%s' is too short, must be at least %" APR_SIZE_T_FMT " characters.\n", name, min); } else if (len > max) { redwax_print_error(r, "Passphrase for '%s' is too long, must be at most %" APR_SIZE_T_FMT " characters.\n", name, max); } else { char *passphrase = PORT_Strdup((char *)pin); apr_pool_destroy(pool); return passphrase; } } } /* last step, try read the passphrase twice */ { char *prompt1, *prompt2; char *buf1 = apr_pcalloc(pool, max + 2); char *buf2 = apr_pcalloc(pool, max + 2); #if HAVE_APR_CRYPTO_CLEAR apr_crypto_clear(pool, buf1, max + 2); apr_crypto_clear(pool, buf2, max + 2); #endif prompt1 = apr_psprintf(r->pool, "Enter %s for %s: ", what, file); prompt2 = apr_psprintf(r->pool, "Verifying - %s", prompt1); while (1) { int len; /* skip command completion */ if (r->complete) { break; } status = apr_password_get(prompt1, buf1, &max); if (APR_ENAMETOOLONG == status) { redwax_print_error(r, "Passphrase was longer than %" APR_SIZE_T_FMT ", try again.\n", max); continue; } if (APR_SUCCESS != status) { redwax_print_error(r, "Could not read passphrase: %pm\n", &status); break; } len = strlen(buf1); if (len < min) { redwax_print_error(r, "Passphrase is too short, must be at least %" APR_SIZE_T_FMT " characters.\n", min); } else if (len > max) { redwax_print_error(r, "Passphrase is too long, must be at most %" APR_SIZE_T_FMT " characters.\n", max); } if (!s->verify) { char *passphrase = PORT_Strdup((char *)buf1); apr_pool_destroy(pool); return passphrase; } status = apr_password_get(prompt2, buf2, &max); if (APR_ENAMETOOLONG == status) { redwax_print_error(r, "Passphrase was longer than %" APR_SIZE_T_FMT ", please try again.\n", max); continue; } if (APR_SUCCESS != status) { redwax_print_error(r, "Could not read passphrase: %pm\n", &status); break; } if (!strcmp(buf1, buf2)) { char *passphrase = PORT_Strdup((char *)buf1); apr_pool_destroy(pool); return passphrase; } else { redwax_print_error(r, "Passphrases did not match, please try again.\n"); continue; } } } apr_pool_destroy(pool); return NULL; } static apr_status_t redwax_nss_complete_nss_token_out(redwax_tool_t *r, apr_hash_t *out) { apr_pool_t *pool; NSSInitContext *crypto_context; PK11SlotList *slots; PK11SlotListElement *se; const char *tname; apr_pool_create(&pool, r->pool); NSSInitParameters *init_params = apr_pcalloc(pool, sizeof(NSSInitParameters)); init_params->length = sizeof(NSSInitParameters); if (r->nss_out.dir && r->nss_out.dir[0]) { crypto_context = NSS_InitContext(r->nss_out.dir, "", "", SECMOD_DB, init_params, NSS_INIT_OPTIMIZESPACE); } else { crypto_context = NSS_InitContext("", "", "", SECMOD_DB, init_params, NSS_INIT_NOCERTDB | NSS_INIT_NOMODDB | NSS_INIT_OPTIMIZESPACE); } if (crypto_context) { apr_pool_cleanup_register(pool, crypto_context, cleanup_nss, apr_pool_cleanup_null); } else { return APR_ENOENT; } slots = PK11_GetAllTokens(CKM_INVALID_MECHANISM, 0, 0, NULL); if (!slots) { return APR_ENOENT; } apr_pool_cleanup_register(pool, slots, cleanup_slotlist, apr_pool_cleanup_null); for (se = PK11_GetFirstSafe(slots); se; se = PK11_GetNextSafe(slots, se, 0)) { tname = apr_pstrdup(apr_hash_pool_get(out), PK11_GetTokenName(se->slot)); apr_hash_set(out, tname, APR_HASH_KEY_STRING, tname); } apr_pool_destroy(pool); return APR_SUCCESS; } static apr_size_t rtrim(char *buf, apr_size_t len) { if (len) { len--; while (len >= 0 && apr_isspace(buf[len])) { len--; } len++; } return len; } static apr_status_t redwax_nss_process_nss_out(redwax_tool_t *r, const char *file, const char *sname, apr_hash_t *secrets) { apr_pool_t *pool; NSSInitContext *crypto_context; CERTCertDBHandle *handle; PK11SlotInfo *slot; CERTCertificate *x = NULL; SECKEYPrivateKey *k = NULL; const char *label; redwax_nss_secret_t s; CK_TOKEN_INFO token = { { 0 } }; SECStatus rv; int i; apr_pool_create(&pool, r->pool); NSSInitParameters *init_params = apr_pcalloc(pool, sizeof(NSSInitParameters)); init_params->length = sizeof(NSSInitParameters); if (file && file[0]) { crypto_context = NSS_InitContext(file, "", "", SECMOD_DB, init_params, NSS_INIT_OPTIMIZESPACE); } else { crypto_context = NSS_InitContext("", "", "", SECMOD_DB, init_params, NSS_INIT_NOCERTDB | NSS_INIT_NOMODDB | NSS_INIT_OPTIMIZESPACE); } if (crypto_context) { apr_pool_cleanup_register(pool, crypto_context, cleanup_nss, apr_pool_cleanup_null); } else { redwax_print_error(r, "Could not open NSS database '%s', skipping: %s\n", file, PR_ErrorToName(PR_GetError())); return APR_EINIT; } /* no need to free this one apparently */ handle = CERT_GetDefaultCertDB(); if (sname) { slot = PK11_FindSlotByName(sname); if (!slot) { redwax_print_error(r, "Could not open NSS slot '%s', skipping: %s\n", sname, PR_ErrorToName(PR_GetError())); return APR_EINIT; } } else { slot = PK11_GetInternalKeySlot(); } apr_pool_cleanup_register(pool, slot, cleanup_slot, apr_pool_cleanup_null); PK11_GetTokenInfo(slot, &token); PK11_SetPasswordFunc(redwax_nss_password_cb); s.r = r; s.pool = pool; s.file = file; s.secrets = secrets; s.what = apr_pstrndup(pool, (char*) token.label, rtrim((char*) token.label, sizeof(token.label))); if (r->key_out) { for (i = 0; i < r->keys_out->nelts; i++) { const redwax_key_t *key = &APR_ARRAY_IDX(r->keys_out, i, const redwax_key_t); SECItem pkcs8PrivKeyItem = { siBuffer, (unsigned char *)key->der, key->len}; SECItem nickname = { siBuffer, (unsigned char*) key->label, key->label_len }; if (!key->der) { redwax_print_error(r, "nss-out: non-extractable private key, skipping\n"); continue; } redwax_print_error(r, "nss-out: private key\n"); rv = PK11_ImportDERPrivateKeyInfo(slot, &pkcs8PrivKeyItem, &nickname, NULL /*publicValue*/, 1 /*isPerm*/, 0 /*isPrivate*/, KU_ALL, &s); if (rv != SECSuccess) { if (PORT_GetError() == SEC_ERROR_TOKEN_NOT_LOGGED_IN) { rv = PK11_Authenticate(slot, PR_TRUE, &s); if (rv != SECSuccess) { redwax_print_error(r, "Error: could not log in to token '%s', giving up.\n", PK11_GetTokenName(slot)); apr_pool_destroy(pool); return APR_EACCES; } else { rv = PK11_ImportDERPrivateKeyInfo(slot, &pkcs8PrivKeyItem, &nickname, NULL /*publicValue*/, 1 /*isPerm*/, 0 /*isPrivate*/, KU_ALL, &s); } } if (rv != SECSuccess) { redwax_print_error(r, "Warning: could not import key to token '%s', skipping: %s\n", PK11_GetTokenName(slot), PR_ErrorToName(PR_GetError())); continue; } } } } label = r->label_out; if (r->cert_out) { for (i = 0; i < r->certs_out->nelts; i++) { const redwax_certificate_t *cert = &APR_ARRAY_IDX(r->certs_out, i, const redwax_certificate_t); x = CERT_DecodeCertFromPackage((char *)cert->der, cert->len); if (!x) { redwax_print_error(r, "Warning: could not decode certificate to be written to '%s', skipping: %s\n", file, PR_ErrorToName(PR_GetError())); continue; } apr_pool_cleanup_register(pool, x, cleanup_cert, apr_pool_cleanup_null); if (r->auto_out) { CERTCertificate *xx; xx = PK11_FindCertFromDERCert(slot, x, &s); if (xx) { redwax_print_error(r, "Warning: nss-out: certificate '%s' already exists, skipping.\n", x->subjectName); apr_pool_cleanup_register(pool, xx, cleanup_cert, apr_pool_cleanup_null); continue; } } if (!label) { if (cert->label) { label = apr_pstrndup(pool, cert->label, cert->label_len); } else { CERTName *subject = CERT_AsciiToName(x->subjectName); if (subject) { label = CERT_GetCommonName(subject); apr_pool_cleanup_register(pool, label, cleanup_free, apr_pool_cleanup_null); } } } redwax_print_error(r, "nss-out: certificate: %s\n", x->subjectName); k = PK11_FindPrivateKeyFromCert(slot, x, &s); apr_pool_cleanup_register(pool, k, cleanup_key, apr_pool_cleanup_null); if (k) { rv = PK11_ImportCertForKeyToSlot(slot, x, (char *)label, PR_TRUE, &s); } else { rv = PK11_ImportCert(slot, x, CK_INVALID_HANDLE, label, PR_FALSE); } if (rv != SECSuccess) { if (PORT_GetError() == SEC_ERROR_TOKEN_NOT_LOGGED_IN) { rv = PK11_Authenticate(slot, PR_TRUE, &s); if (rv != SECSuccess) { redwax_print_error(r, "Error: could not log in to token '%s', giving up.\n", PK11_GetTokenName(slot)); apr_pool_destroy(pool); return APR_EACCES; } else { if (k) { rv = PK11_ImportCertForKeyToSlot(slot, x, (char *)label, PR_TRUE, &s); } else { rv = PK11_ImportCert(slot, x, CK_INVALID_HANDLE, label, PR_FALSE); } } } if (rv != SECSuccess) { redwax_print_error(r, "Warning: could not add certificate to token '%s', skipping.\n", PK11_GetTokenName(slot)); continue; } } if (k) { PK11_SetPrivateKeyNickname(k, label); } /* we use the label once and once only */ label = NULL; } } if (r->chain_out) { for (i = 0; i < r->intermediates_out->nelts; i++) { const redwax_certificate_t *cert = &APR_ARRAY_IDX(r->intermediates_out, i, const redwax_certificate_t); CERTName *subject; x = CERT_DecodeCertFromPackage((char *)cert->der, cert->len); if (!x) { redwax_print_error(r, "Could not decode certificate to be written to '%s', skipping: %s\n", file, PR_ErrorToName(PR_GetError())); apr_pool_destroy(pool); return APR_EINVAL; } apr_pool_cleanup_register(pool, x, cleanup_cert, apr_pool_cleanup_null); if (r->auto_out) { CERTCertificate *xx; xx = PK11_FindCertFromDERCert(slot, x, &s); if (xx) { redwax_print_error(r, "Warning: nss-out: intermediate '%s' already exists, skipping.\n", x->subjectName); apr_pool_cleanup_register(pool, xx, cleanup_cert, apr_pool_cleanup_null); continue; } } redwax_print_error(r, "nss-out: intermediate: %s\n", x->subjectName); if (cert->label) { label = apr_pstrndup(pool, cert->label, cert->label_len); } else { subject = CERT_AsciiToName(x->subjectName); if (subject) { label = CERT_GetCommonName(subject); apr_pool_cleanup_register(pool, label, cleanup_free, apr_pool_cleanup_null); } else { label = apr_psprintf(pool, "(unspecified intermediate %d)", i); } } rv = PK11_ImportCert(slot, x, CK_INVALID_HANDLE, label, PR_FALSE); if (rv != SECSuccess) { if (PORT_GetError() == SEC_ERROR_TOKEN_NOT_LOGGED_IN) { rv = PK11_Authenticate(slot, PR_TRUE, &s); if (rv != SECSuccess) { redwax_print_error(r, "Error: could not log in to token '%s', giving up.\n", PK11_GetTokenName(slot)); apr_pool_destroy(pool); return APR_EACCES; } else { rv = PK11_ImportCert(slot, x, CK_INVALID_HANDLE, label, PR_FALSE); } } if (rv != SECSuccess) { redwax_print_error(r, "Warning: could not add certificate to token '%s', skipping.\n", PK11_GetTokenName(slot)); continue; } } } } if (r->trust_out) { CERTCertTrust *trust = apr_pcalloc(pool, sizeof(CERTCertTrust)); for (i = 0; i < r->trusted_out->nelts; i++) { const redwax_certificate_t *cert = &APR_ARRAY_IDX(r->trusted_out, i, const redwax_certificate_t); x = CERT_DecodeCertFromPackage((char *)cert->der, cert->len); if (!x) { redwax_print_error(r, "Could not decode certificate to be written to '%s', skipping: %s\n", file, PR_ErrorToName(PR_GetError())); apr_pool_destroy(pool); return APR_EINVAL; } apr_pool_cleanup_register(pool, x, cleanup_cert, apr_pool_cleanup_null); if (r->auto_out) { CERTCertificate *xx; xx = PK11_FindCertFromDERCert(slot, x, &s); if (xx) { redwax_print_error(r, "Warning: nss-out: trusted '%s' already exists, skipping.\n", x->subjectName); apr_pool_cleanup_register(pool, xx, cleanup_cert, apr_pool_cleanup_null); continue; } } redwax_print_error(r, "nss-out: trusted: %s\n", x->subjectName); if (cert->label) { label = apr_pstrndup(pool, cert->label, cert->label_len); } else { CERTName *subject = CERT_AsciiToName(x->subjectName); if (subject) { label = CERT_GetCommonName(subject); apr_pool_cleanup_register(pool, label, cleanup_free, apr_pool_cleanup_null); } else { label = apr_psprintf(pool, "(unspecified root %d)", i); } } /* FIXME: more granular trust import needed */ rv = CERT_DecodeTrustString(trust, "CT,CT,CT"); if (rv != SECSuccess) { redwax_print_error(r, "Could not decode trust for token '%s', skipping.\n", PK11_GetTokenName(slot)); apr_pool_destroy(pool); return APR_EINVAL; } rv = PK11_ImportCert(slot, x, CK_INVALID_HANDLE, label, PR_FALSE); if (rv != SECSuccess) { if (PORT_GetError() == SEC_ERROR_TOKEN_NOT_LOGGED_IN) { rv = PK11_Authenticate(slot, PR_TRUE, &s); if (rv != SECSuccess) { redwax_print_error(r, "Error: could not log in to token '%s', giving up.\n", PK11_GetTokenName(slot)); apr_pool_destroy(pool); return APR_EACCES; } else { rv = PK11_ImportCert(slot, x, CK_INVALID_HANDLE, label, PR_FALSE); } } if (rv != SECSuccess) { redwax_print_error(r, "Warning: could not add certificate to token '%s', skipping.\n", PK11_GetTokenName(slot)); continue; } } rv = CERT_ChangeCertTrust(handle, x, trust); if (rv != SECSuccess) { if (PORT_GetError() == SEC_ERROR_TOKEN_NOT_LOGGED_IN) { rv = PK11_Authenticate(slot, PR_TRUE, &s); if (rv != SECSuccess) { redwax_print_error(r, "Error: could not log in to token '%s', giving up.\n", PK11_GetTokenName(slot)); apr_pool_destroy(pool); return APR_EACCES; } rv = CERT_ChangeCertTrust(handle, x, trust); } if (rv != SECSuccess) { redwax_print_error(r, "Warning: could not set trust on certificate to token '%s', skipping: %s\n", PK11_GetTokenName(slot), PR_ErrorToName(PR_GetError())); continue; } } } } apr_pool_destroy(pool); return APR_SUCCESS; } void redwax_add_default_nss_hooks() { rt_hook_initialise(redwax_nss_initialise, NULL, NULL, APR_HOOK_MIDDLE); rt_hook_process_nss_out(redwax_nss_process_nss_out, NULL, NULL, APR_HOOK_MIDDLE); rt_hook_complete_nss_token_out(redwax_nss_complete_nss_token_out, NULL, NULL, APR_HOOK_MIDDLE); } #else void redwax_add_default_nss_hooks() { } #endif REDWAX_DECLARE_MODULE(nss) = { STANDARD_MODULE_STUFF, redwax_add_default_nss_hooks /* register hooks */ };