From 1d6c86709c72442aff3bdde8ab48b048e6df153a Mon Sep 17 00:00:00 2001 From: "Dr. David von Oheimb" Date: Mon, 10 Aug 2020 14:23:46 +0200 Subject: [PATCH] apps/pkcs12.c: Add -untrusted option Also improve EE cert selection, user guidance, and documentation. Reviewed-by: Tomas Mraz (Merged from https://github.com/openssl/openssl/pull/12643) --- apps/pkcs12.c | 163 +++++++++++++++++++++++---------- doc/man1/openssl-pkcs12.pod.in | 42 ++++++--- test/recipes/80-test_pkcs12.t | 10 +- 3 files changed, 150 insertions(+), 65 deletions(-) diff --git a/apps/pkcs12.c b/apps/pkcs12.c index 60ede2e1a1..f5bb18a8db 100644 --- a/apps/pkcs12.c +++ b/apps/pkcs12.c @@ -35,6 +35,7 @@ DEFINE_STACK_OF_STRING() #define PASSWD_BUF_SIZE 2048 static int get_cert_chain(X509 *cert, X509_STORE *store, + STACK_OF(X509) *untrusted_certs, STACK_OF(X509) **chain); int dump_certs_keys_p12(BIO *out, const PKCS12 *p12, const char *pass, int passlen, int options, @@ -59,7 +60,8 @@ typedef enum OPTION_choice { OPT_CACERTS, OPT_NOOUT, OPT_INFO, OPT_CHAIN, OPT_TWOPASS, OPT_NOMACVER, OPT_DESCERT, OPT_EXPORT, OPT_ITER, OPT_NOITER, OPT_MACITER, OPT_NOMACITER, OPT_NOMAC, OPT_LMK, OPT_NODES, OPT_NOENC, OPT_MACALG, OPT_CERTPBE, OPT_KEYPBE, - OPT_INKEY, OPT_CERTFILE, OPT_PASSCERTS, OPT_NAME, OPT_CSP, OPT_CANAME, + OPT_INKEY, OPT_CERTFILE, OPT_UNTRUSTED, OPT_PASSCERTS, + OPT_NAME, OPT_CSP, OPT_CANAME, OPT_IN, OPT_OUT, OPT_PASSIN, OPT_PASSOUT, OPT_PASSWORD, OPT_CAPATH, OPT_CAFILE, OPT_CASTORE, OPT_NOCAPATH, OPT_NOCAFILE, OPT_NOCASTORE, OPT_ENGINE, OPT_R_ENUM, OPT_PROV_ENUM, OPT_LEGACY_ALG @@ -73,7 +75,7 @@ const OPTIONS pkcs12_options[] = { {"engine", OPT_ENGINE, 's', "Use engine, possibly a hardware device"}, #endif - OPT_SECTION("CA"), + OPT_SECTION("CA input for export with the -chain option"), {"CApath", OPT_CAPATH, '/', "PEM-format directory of CA's"}, {"CAfile", OPT_CAFILE, '<', "PEM-format file of CA's"}, {"CAstore", OPT_CASTORE, ':', "URI to store of CA's"}, @@ -85,39 +87,42 @@ const OPTIONS pkcs12_options[] = { "Do not load certificates from the default certificates store"}, OPT_SECTION("Input"), - {"inkey", OPT_INKEY, 's', "Private key if not infile"}, - {"certfile", OPT_CERTFILE, '<', "Load certs from file"}, + {"in", OPT_IN, '<', "Input file for PKCS12 parsing or certs and possibly key"}, + {"passin", OPT_PASSIN, 's', "Input file pass phrase source"}, + {"inkey", OPT_INKEY, 's', "Private key, else read from -in input file"}, + {"certfile", OPT_CERTFILE, '<', "Extra certificates for PKCS12 output"}, + {"untrusted", OPT_UNTRUSTED, '<', "Untrusted certificates for chain building"}, {"passcerts", OPT_PASSCERTS, 's', "Certificate file pass phrase source"}, {"name", OPT_NAME, 's', "Use name as friendly name"}, {"CSP", OPT_CSP, 's', "Microsoft CSP name"}, {"caname", OPT_CANAME, 's', "Use name as CA friendly name (can be repeated)"}, - {"in", OPT_IN, '<', "Input filename"}, - {"passin", OPT_PASSIN, 's', "Input file pass phrase source"}, OPT_SECTION("Output"), {"export", OPT_EXPORT, '-', "Output PKCS12 file"}, {"LMK", OPT_LMK, '-', "Add local machine keyset attribute to private key"}, {"macalg", OPT_MACALG, 's', - "Digest algorithm used in MAC (default SHA1)"}, + "Digest algorithm to use in MAC (default SHA1)"}, {"keypbe", OPT_KEYPBE, 's', "Private key PBE algorithm (default 3DES)"}, {"out", OPT_OUT, '>', "Output filename"}, - {"passout", OPT_PASSOUT, 's', "Output file pass phrase source"}, + {"passout", OPT_PASSOUT, 's', "Output pass phrase source"}, {"password", OPT_PASSWORD, 's', "Set import/export password source"}, {"nocerts", OPT_NOCERTS, '-', "Don't output certificates"}, {"clcerts", OPT_CLCERTS, '-', "Only output client certificates"}, {"cacerts", OPT_CACERTS, '-', "Only output CA certificates"}, - {"noout", OPT_NOOUT, '-', "Don't output anything, just verify"}, - {"chain", OPT_CHAIN, '-', "Add certificate chain"}, + {"noout", OPT_NOOUT, '-', "Don't output anything, just verify PKCS#12 input"}, + {"chain", OPT_CHAIN, '-', "Build and add certificate chain for EE cert,"}, + {OPT_MORE_STR, 0, 0, + "which is the 1st cert from -in matching the private key (if given)"}, {"twopass", OPT_TWOPASS, '-', "Separate MAC, encryption passwords"}, {"nomacver", OPT_NOMACVER, '-', "Don't verify MAC"}, {"info", OPT_INFO, '-', "Print info about PKCS#12 structure"}, {"nokeys", OPT_NOKEYS, '-', "Don't output private keys"}, - {"keyex", OPT_KEYEX, '-', "Set MS key exchange type"}, - {"keysig", OPT_KEYSIG, '-', "Set MS key signature type"}, + {"keyex", OPT_KEYEX, '-', "Set key type to MS key exchange"}, + {"keysig", OPT_KEYSIG, '-', "Set key type to MS key signature"}, - OPT_SECTION("Encryption"), + OPT_SECTION("PKCS12 output encryption and MAC"), #ifndef OPENSSL_NO_RC2 {"descert", OPT_DESCERT, '-', "Encrypt output with 3DES (default PBES2 with PBKDF2 and AES-256 CBC)"}, @@ -144,6 +149,7 @@ const OPTIONS pkcs12_options[] = { int pkcs12_main(int argc, char **argv) { char *infile = NULL, *outfile = NULL, *keyname = NULL, *certfile = NULL; + char *untrusted = NULL; char *passcertsarg = NULL, *passcerts = NULL; char *name = NULL, *csp_name = NULL; char pass[PASSWD_BUF_SIZE] = "", macpass[PASSWD_BUF_SIZE] = ""; @@ -262,6 +268,9 @@ int pkcs12_main(int argc, char **argv) case OPT_CERTFILE: certfile = opt_arg(); break; + case OPT_UNTRUSTED: + untrusted = opt_arg(); + break; case OPT_PASSCERTS: passcertsarg = opt_arg(); break; @@ -327,9 +336,37 @@ int pkcs12_main(int argc, char **argv) } argc = opt_num_rest(); - if (!export_cert && passcertsarg != NULL) - BIO_printf(bio_err, - "Warning: -passcerts option ignored without -export\n"); + if (!export_cert) { + if (chain) + BIO_printf(bio_err, "Warning: -chain option ignored without -export\n"); + if (certfile != NULL) + BIO_printf(bio_err, "Warning: -certfile option ignored without -export\n"); + if (untrusted != NULL) + BIO_printf(bio_err, "Warning: -untrusted option ignored without -export\n"); + if (passcertsarg != NULL) + BIO_printf(bio_err, + "Warning: -passcerts option ignored without -export\n"); + if (CApath != NULL || noCApath) + BIO_printf(bio_err, "Warning: -[no-]CApath option ignored without -export\n"); + if (CAfile != NULL || noCAfile) + BIO_printf(bio_err, "Warning: -[no-]CAfile option ignored without -export\n"); + if (CAstore != NULL || noCAstore) + BIO_printf(bio_err, "Warning: -[no-]CAstore option ignored without -export\n"); + if (add_lmk) + BIO_printf(bio_err, "Warning: -LMK option ignored without -export\n"); + if (name != NULL) + BIO_printf(bio_err, "Warning: -name option ignored without -export\n"); + if (csp_name != NULL) + BIO_printf(bio_err, "Warning: -CSP option ignored without -export\n"); + if (canames != NULL) + BIO_printf(bio_err, "Warning: -caname option ignored without -export\n"); + if (keyname != NULL) + BIO_printf(bio_err, "Warning: -inkey option ignored without -export\n"); + if (keytype != 0) + BIO_printf(bio_err, "Warning: -keyex and -keysig options ignored without -export\n"); + if (macalg != NULL) + BIO_printf(bio_err, "Warning: -macalg option ignored without -export\n"); + } if (use_legacy) { /* load the legacy provider if not loaded already*/ if (!OSSL_PROVIDER_available(app_get0_libctx(), "legacy")) { @@ -414,94 +451,119 @@ int pkcs12_main(int argc, char **argv) if (export_cert) { EVP_PKEY *key = NULL; - X509 *ucert = NULL, *x = NULL; + X509 *ee_cert = NULL, *x = NULL; STACK_OF(X509) *certs = NULL; + STACK_OF(X509) *untrusted_certs = NULL; const EVP_MD *macmd = NULL; unsigned char *catmp = NULL; int i; if ((options & (NOCERTS | NOKEYS)) == (NOCERTS | NOKEYS)) { - BIO_printf(bio_err, "Nothing to do!\n"); + BIO_printf(bio_err, "Nothing to export due to -nocerts and -nokeys or -noout!\n"); goto export_end; } - if (options & NOCERTS) + if ((options & NOCERTS) != 0) { chain = 0; + BIO_printf(bio_err, "Warning: -chain option ignored with -nocerts\n"); + } if (!(options & NOKEYS)) { key = load_key(keyname ? keyname : infile, - FORMAT_PEM, 1, passin, e, "private key"); + FORMAT_PEM, 1, passin, e, + keyname ? + "private key from -inkey file" : + "private key from -in file"); if (key == NULL) goto export_end; } - /* Load in all certs in input file */ + /* Load all certs in input file */ if (!(options & NOCERTS)) { - if (!load_certs(infile, &certs, passin, "input certificates")) + if (!load_certs(infile, &certs, passin, + "certificates from -in file")) goto export_end; + if (sk_X509_num(certs) < 1) { + BIO_printf(bio_err, "No certificate in -in file %s\n", infile); + goto export_end; + } if (key != NULL) { /* Look for matching private key */ for (i = 0; i < sk_X509_num(certs); i++) { x = sk_X509_value(certs, i); if (X509_check_private_key(x, key)) { - ucert = x; + ee_cert = x; /* Zero keyid and alias */ - X509_keyid_set1(ucert, NULL, 0); - X509_alias_set1(ucert, NULL, 0); + X509_keyid_set1(ee_cert, NULL, 0); + X509_alias_set1(ee_cert, NULL, 0); /* Remove from list */ (void)sk_X509_delete(certs, i); break; } } - if (ucert == NULL) { + if (ee_cert == NULL) { BIO_printf(bio_err, - "No certificate matches private key\n"); + "No cert in -in file '%s' matches private key\n", + infile); goto export_end; } + } else { + ee_cert = X509_dup(sk_X509_value(certs, 0)); /* take 1st cert */ } - } - /* Add any more certificates asked for */ - if (certfile != NULL) { - if (!load_certs(certfile, &certs, passcerts, - "certificates from certfile")) + /* Load any untrusted certificates for chain building */ + if (untrusted != NULL) { + if (!load_certs(untrusted, &untrusted_certs, passcerts, + "untrusted certificates")) goto export_end; } - /* If chaining get chain from user cert */ + /* If chaining get chain from end entity cert */ if (chain) { int vret; STACK_OF(X509) *chain2; X509_STORE *store; + + if (ee_cert == NULL) { + BIO_printf(bio_err, + "No end entity certificate to check with -chain\n"); + goto export_end; + } + if ((store = setup_verify(CAfile, noCAfile, CApath, noCApath, CAstore, noCAstore)) == NULL) goto export_end; - vret = get_cert_chain(ucert, store, &chain2); + vret = get_cert_chain(ee_cert, store, untrusted_certs, &chain2); X509_STORE_free(store); if (vret == X509_V_OK) { - /* Exclude verified certificate */ - for (i = 1; i < sk_X509_num(chain2); i++) - sk_X509_push(certs, sk_X509_value(chain2, i)); - /* Free first certificate */ - X509_free(sk_X509_value(chain2, 0)); - sk_X509_free(chain2); + /* Remove from chain2 the first (end entity) certificate */ + X509_free(sk_X509_shift(chain2)); + /* Add the remaining certs (except for duplicates) */ + if (!X509_add_certs(certs, chain2, X509_ADD_FLAG_UP_REF + | X509_ADD_FLAG_NO_DUP)) + goto export_end; + sk_X509_pop_free(chain2, X509_free); } else { if (vret != X509_V_ERR_UNSPECIFIED) - BIO_printf(bio_err, "Error %s getting chain.\n", + BIO_printf(bio_err, "Error getting chain: %s\n", X509_verify_cert_error_string(vret)); - else - ERR_print_errors(bio_err); goto export_end; } } - /* Add any CA names */ + /* Add any extra certificates asked for */ + if (certfile != NULL) { + if (!load_certs(certfile, &certs, passcerts, + "extra certificates from -certfile")) + goto export_end; + } + /* Add any CA names */ for (i = 0; i < sk_OPENSSL_STRING_num(canames); i++) { catmp = (unsigned char *)sk_OPENSSL_STRING_value(canames, i); X509_alias_set1(sk_X509_value(certs, i), catmp, -1); @@ -534,7 +596,7 @@ int pkcs12_main(int argc, char **argv) if (!twopass) OPENSSL_strlcpy(macpass, pass, sizeof(macpass)); - p12 = PKCS12_create(cpass, name, key, ucert, certs, + p12 = PKCS12_create(cpass, name, key, ee_cert, certs, key_pbe, cert_pbe, iter, -1, keytype); if (p12 == NULL) { @@ -542,7 +604,7 @@ int pkcs12_main(int argc, char **argv) goto export_end; } - if (macalg) { + if (macalg != NULL) { if (!opt_md(macalg, &macmd)) goto opthelp; } @@ -564,7 +626,8 @@ int pkcs12_main(int argc, char **argv) EVP_PKEY_free(key); sk_X509_pop_free(certs, X509_free); - X509_free(ucert); + sk_X509_pop_free(untrusted_certs, X509_free); + X509_free(ee_cert); goto end; @@ -831,18 +894,20 @@ int dump_certs_pkeys_bag(BIO *out, const PKCS12_SAFEBAG *bag, /* Given a single certificate return a verified chain or NULL if error */ static int get_cert_chain(X509 *cert, X509_STORE *store, + STACK_OF(X509) *untrusted_certs, STACK_OF(X509) **chain) { X509_STORE_CTX *store_ctx = NULL; STACK_OF(X509) *chn = NULL; int i = 0; - store_ctx = X509_STORE_CTX_new(); + store_ctx = X509_STORE_CTX_new_with_libctx(app_get0_libctx(), + app_get0_propq()); if (store_ctx == NULL) { i = X509_V_ERR_UNSPECIFIED; goto end; } - if (!X509_STORE_CTX_init(store_ctx, store, cert, NULL)) { + if (!X509_STORE_CTX_init(store_ctx, store, cert, untrusted_certs)) { i = X509_V_ERR_UNSPECIFIED; goto end; } diff --git a/doc/man1/openssl-pkcs12.pod.in b/doc/man1/openssl-pkcs12.pod.in index e148d229b0..c702d39e43 100644 --- a/doc/man1/openssl-pkcs12.pod.in +++ b/doc/man1/openssl-pkcs12.pod.in @@ -13,6 +13,7 @@ B B [B<-chain>] [B<-inkey> I] [B<-certfile> I] +[B<-untrusted> I] [B<-passcerts> I] [B<-name> I] [B<-caname> I] @@ -73,8 +74,9 @@ programs including Netscape, MSIE and MS Outlook. =head1 OPTIONS There are a lot of options the meaning of some depends of whether a PKCS#12 file -is being created or parsed. By default a PKCS#12 file is parsed. A PKCS#12 -file can be created by using the B<-export> option (see below). +is being created or parsed. By default a PKCS#12 file is parsed. +A PKCS#12 file can be created by using the B<-export> option (see below). +Many further options such as B<-chain> make sense only with B<-export>. =head1 PARSING OPTIONS @@ -86,10 +88,10 @@ Print out a usage message. =item B<-in> I -This specifies filename or URI of the PKCS#12 file to be parsed. -With B<-export>, this refers to the the certificate and/or key input, -which can be in PEM, DER, or PKCS#12 format. +This specifies the input filename or URI. Standard input is used by default. +Without the B<-export> option this is a PKCS#12 file to be parsed. +With the B<-export> option this is a file with certificates and possibly a key. =item B<-out> I @@ -103,8 +105,8 @@ otherwise it is equivalent to B<-passin>. =item B<-noout> -This option inhibits output of the keys and certificates to the output file -version of the PKCS#12 file. +This option inhibits credentials output, +and so the PKCS#12 input is just verified. =item B<-clcerts> @@ -206,8 +208,8 @@ certificates are present they will also be included in the PKCS#12 file. =item B<-inkey> I -File to read private key from. If not present then a private key must be present -in the input file. +File to read private key from for PKCS12 output. +If not present then the input file (B<-in> argument) must contain a private key. If no engine is used, the argument is taken as a file; if an engine is specified, the argument is given to the engine as a key identifier. @@ -218,8 +220,15 @@ name is typically displayed in list boxes by software importing the file. =item B<-certfile> I -A filename or URI to read additional certificates from. -The file can be in PEM, DER, or PKCS#12 format. +An input file with extra certificates to be added to the PKCS12 output +if the B<-export> option is given. + +=item B<-untrusted> I + +An input file of untrusted certificates that may be used +for chain building, which is relevant only when a PKCS#12 file is created +with the B<-export> option and the B<-chain> option is given as well. +Any certificates that are actually part of the chain are added to the output. =item B<-passcerts> I @@ -243,9 +252,12 @@ see L. =item B<-chain> -If this option is present then an attempt is made to include the entire -certificate chain of the user certificate. The standard CA store is used -for this search. If the search fails it is considered a fatal error. +If this option is present then the certificate chain of the end entity +certificate is built and included in the PKCS#12 output file. +The end entity certificate is the first one read from the B<-in> file +if no key is given, else the first certificate matching the given key. +The standard CA trust store is used for chain building, +as well as any untrusted CA certificates given with the B<-untrusted> option. =item B<-descert> @@ -404,7 +416,7 @@ L =head1 HISTORY The B<-engine> option was deprecated in OpenSSL 3.0. -The <-nodes> option was deprecated in OpenSSL 3.0, too; use B<-noenc> instead. +The B<-nodes> option was deprecated in OpenSSL 3.0, too; use B<-noenc> instead. =head1 COPYRIGHT diff --git a/test/recipes/80-test_pkcs12.t b/test/recipes/80-test_pkcs12.t index 24247b8c25..e474d4448c 100644 --- a/test/recipes/80-test_pkcs12.t +++ b/test/recipes/80-test_pkcs12.t @@ -57,7 +57,7 @@ if (eval { require Win32::API; 1; }) { } $ENV{OPENSSL_WIN32_UTF8}=1; -plan tests => 4; +plan tests => 5; # Test different PKCS#12 formats ok(run(test(["pkcs12_format_test"])), "test pkcs12 formats"); @@ -71,6 +71,14 @@ ok(run(app(["openssl", "pkcs12", "-noout", my @path = qw(test certs); my $tmpfile = "tmp.p12"; +# Test the -chain option with -untrusted +ok(run(app(["openssl", "pkcs12", "-export", "-chain", + "-CAfile", srctop_file(@path, "sroot-cert.pem"), + "-untrusted", srctop_file(@path, "ca-cert.pem"), + "-in", srctop_file(@path, "ee-cert.pem"), + "-nokeys", "-passout", "pass:", "-out", $tmpfile])), + "test_pkcs12_chain_untrusted"); + # Test the -passcerts option ok(run(app(["openssl", "pkcs12", "-export", "-in", srctop_file(@path, "ee-cert.pem"),