diff --git a/CHANGES.md b/CHANGES.md index b616638e48..d04a8c2bdd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -127,6 +127,13 @@ OpenSSL 3.2 * Lutz Jänicke* + * The `x509`, `ca`, and `req` apps now produce X.509 v3 certificates. + The `-x509v1` option of `req` prefers generation of X.509 v1 certificates. + `X509_sign()` and `X509_sign_ctx()` make sure that the certificate has + X.509 version 3 if the certificate information includes X.509 extensions. + + *David von Oheimb* + * Fix and extend certificate handling and the apps `x509`, `verify` etc. such as adding a trace facility for debugging certificate chain building. diff --git a/apps/ca.c b/apps/ca.c index e60ce6410c..50bb944969 100644 --- a/apps/ca.c +++ b/apps/ca.c @@ -1926,7 +1926,7 @@ static int do_body(X509 **xret, EVP_PKEY *pkey, X509 *x509, !EVP_PKEY_missing_parameters(pkey)) EVP_PKEY_copy_parameters(pktmp, pkey); - if (!do_X509_sign(ret, pkey, dgst, sigopts, &ext_ctx)) + if (!do_X509_sign(ret, 0, pkey, dgst, sigopts, &ext_ctx)) goto end; /* We now just add it to the database as DB_TYPE_VAL('V') */ diff --git a/apps/include/apps.h b/apps/include/apps.h index 335e80775c..a8b63fea8d 100644 --- a/apps/include/apps.h +++ b/apps/include/apps.h @@ -259,7 +259,7 @@ int init_gen_str(EVP_PKEY_CTX **pctx, const char *algname, ENGINE *e, int do_param, OSSL_LIB_CTX *libctx, const char *propq); int cert_matches_key(const X509 *cert, const EVP_PKEY *pkey); -int do_X509_sign(X509 *x, EVP_PKEY *pkey, const char *md, +int do_X509_sign(X509 *x, int force_v1, EVP_PKEY *pkey, const char *md, STACK_OF(OPENSSL_STRING) *sigopts, X509V3_CTX *ext_ctx); int do_X509_verify(X509 *x, EVP_PKEY *pkey, STACK_OF(OPENSSL_STRING) *vfyopts); int do_X509_REQ_sign(X509_REQ *x, EVP_PKEY *pkey, const char *md, diff --git a/apps/lib/apps.c b/apps/lib/apps.c index 4bd2ab964d..16d0cf9a85 100644 --- a/apps/lib/apps.c +++ b/apps/lib/apps.c @@ -2289,16 +2289,14 @@ int cert_matches_key(const X509 *cert, const EVP_PKEY *pkey) } /* Ensure RFC 5280 compliance, adapt keyIDs as needed, and sign the cert info */ -int do_X509_sign(X509 *cert, EVP_PKEY *pkey, const char *md, +int do_X509_sign(X509 *cert, int force_v1, EVP_PKEY *pkey, const char *md, STACK_OF(OPENSSL_STRING) *sigopts, X509V3_CTX *ext_ctx) { - const STACK_OF(X509_EXTENSION) *exts = X509_get0_extensions(cert); EVP_MD_CTX *mctx = EVP_MD_CTX_new(); int self_sign; int rv = 0; - if (sk_X509_EXTENSION_num(exts /* may be NULL */) > 0) { - /* Prevent X509_V_ERR_EXTENSIONS_REQUIRE_VERSION_3 */ + if (!force_v1) { if (!X509_set_version(cert, X509_VERSION_3)) goto end; diff --git a/apps/req.c b/apps/req.c index 1e97c1d914..fa0c9a050a 100644 --- a/apps/req.c +++ b/apps/req.c @@ -85,8 +85,8 @@ typedef enum OPTION_choice { OPT_KEYOUT, OPT_PASSIN, OPT_PASSOUT, OPT_NEWKEY, OPT_PKEYOPT, OPT_SIGOPT, OPT_VFYOPT, OPT_BATCH, OPT_NEWHDR, OPT_MODULUS, OPT_VERIFY, OPT_NOENC, OPT_NODES, OPT_NOOUT, OPT_VERBOSE, OPT_UTF8, - OPT_NAMEOPT, OPT_REQOPT, OPT_SUBJ, OPT_SUBJECT, OPT_TEXT, OPT_X509, - OPT_CA, OPT_CAKEY, + OPT_NAMEOPT, OPT_REQOPT, OPT_SUBJ, OPT_SUBJECT, OPT_TEXT, + OPT_X509, OPT_X509V1, OPT_CA, OPT_CAKEY, OPT_MULTIVALUE_RDN, OPT_DAYS, OPT_SET_SERIAL, OPT_COPY_EXTENSIONS, OPT_EXTENSIONS, OPT_REQEXTS, OPT_ADDEXT, OPT_PRECERT, OPT_MD, @@ -117,6 +117,7 @@ const OPTIONS req_options[] = { {"text", OPT_TEXT, '-', "Text form of request"}, {"x509", OPT_X509, '-', "Output an X.509 certificate structure instead of a cert request"}, + {"x509v1", OPT_X509V1, '-', "Request cert generation with X.509 version 1"}, {"CA", OPT_CA, '<', "Issuer cert to use for signing a cert, implies -x509"}, {"CAkey", OPT_CAKEY, 's', "Issuer private key to use with -CA; default is -CA arg"}, @@ -261,7 +262,7 @@ int req_main(int argc, char **argv) int ret = 1, gen_x509 = 0, i = 0, newreq = 0, verbose = 0; int informat = FORMAT_UNDEF, outformat = FORMAT_PEM, keyform = FORMAT_UNDEF; int modulus = 0, multirdn = 1, verify = 0, noout = 0, text = 0; - int noenc = 0, newhdr = 0, subject = 0, pubkey = 0, precert = 0; + int noenc = 0, newhdr = 0, subject = 0, pubkey = 0, precert = 0, x509v1 = 0; long newkey_len = -1; unsigned long chtype = MBSTRING_ASC, reqflag = 0; @@ -403,6 +404,9 @@ int req_main(int argc, char **argv) case OPT_TEXT: text = 1; break; + case OPT_X509V1: + x509v1 = 1; + /* fall thru */ case OPT_X509: gen_x509 = 1; break; @@ -867,7 +871,8 @@ int req_main(int argc, char **argv) } } - i = do_X509_sign(new_x509, issuer_key, digest, sigopts, &ext_ctx); + i = do_X509_sign(new_x509, x509v1, issuer_key, digest, sigopts, + &ext_ctx); if (!i) goto end; } else { diff --git a/apps/x509.c b/apps/x509.c index 71c622b8c6..b9087fc27a 100644 --- a/apps/x509.c +++ b/apps/x509.c @@ -894,7 +894,7 @@ int x509_main(int argc, char **argv) } noout = 1; } else if (privkey != NULL) { - if (!do_X509_sign(x, privkey, digest, sigopts, &ext_ctx)) + if (!do_X509_sign(x, 0, privkey, digest, sigopts, &ext_ctx)) goto end; } else if (CAfile != NULL) { if ((CAkey = load_key(CAkeyfile, CAkeyformat, @@ -906,7 +906,7 @@ int x509_main(int argc, char **argv) goto err; } - if (!do_X509_sign(x, CAkey, digest, sigopts, &ext_ctx)) + if (!do_X509_sign(x, 0, CAkey, digest, sigopts, &ext_ctx)) goto end; } if (badsig) { diff --git a/crypto/x509/x_all.c b/crypto/x509/x_all.c index c3f5ee11df..e4c5c16f76 100644 --- a/crypto/x509/x_all.c +++ b/crypto/x509/x_all.c @@ -63,6 +63,9 @@ int X509_sign(X509 *x, EVP_PKEY *pkey, const EVP_MD *md) ERR_raise(ERR_LIB_X509, ERR_R_PASSED_NULL_PARAMETER); return 0; } + if (sk_X509_EXTENSION_num(X509_get0_extensions(x)) > 0 + && !X509_set_version(x, X509_VERSION_3)) + return 0; /* * Setting the modified flag before signing it. This makes the cached @@ -83,6 +86,9 @@ int X509_sign_ctx(X509 *x, EVP_MD_CTX *ctx) ERR_raise(ERR_LIB_X509, ERR_R_PASSED_NULL_PARAMETER); return 0; } + if (sk_X509_EXTENSION_num(X509_get0_extensions(x)) > 0 + && !X509_set_version(x, X509_VERSION_3)) + return 0; x->cert_info.enc.modified = 1; return ASN1_item_sign_ctx(ASN1_ITEM_rptr(X509_CINF), &x->cert_info.signature, diff --git a/doc/man1/openssl-ca.pod.in b/doc/man1/openssl-ca.pod.in index 955bac8fd3..3474e12c79 100644 --- a/doc/man1/openssl-ca.pod.in +++ b/doc/man1/openssl-ca.pod.in @@ -71,6 +71,11 @@ B B This command emulates a CA application. See the B especially when considering to use it productively. + +It generates certificates bearing X.509 version 3. +Unless specified otherwise, +key identifier extensions are included as described in L. + It can be used to sign certificate requests (CSRs) in a variety of forms and generate certificate revocation lists (CRLs). It also maintains a text database of issued certificates and their status. @@ -287,8 +292,7 @@ and all certificates will be certified automatically. The section of the configuration file containing certificate extensions to be added when a certificate is issued (defaults to B unless the B<-extfile> option is used). -If no X.509 extensions are specified then a V1 certificate is created, -else a V3 certificate is created. + See the L manual page for details of the extension section format. @@ -833,6 +837,9 @@ has no effect. The B<-engine> option was deprecated in OpenSSL 3.0. +Since OpenSSL 3.2, generated certificates bear X.509 version 3, +and key identifier extensions are included by default. + =head1 SEE ALSO L, diff --git a/doc/man1/openssl-req.pod.in b/doc/man1/openssl-req.pod.in index b677160f6b..099582fa72 100644 --- a/doc/man1/openssl-req.pod.in +++ b/doc/man1/openssl-req.pod.in @@ -33,6 +33,7 @@ B B [B<-config> I] [B<-section> I] [B<-x509>] +[B<-x509v1>] [B<-CA> I|I] [B<-CAkey> I|I] [B<-days> I] @@ -299,6 +300,16 @@ X.509 extensions to be added can be specified in the configuration file, possibly using the B<-config> and B<-extensions> options, and/or using the B<-addext> option. +Unless B<-x509v1> is given, generated certificates bear X.509 version 3. +Unless specified otherwise, +key identifier extensions are included as described in L. + +=item B<-x509v1> + +Request generation of certificates with X.509 version 1. +This implies B<-x509>. +If X.509 extensions are given, anyway X.509 version 3 is set. + =item B<-CA> I|I Specifies the "CA" certificate to be used for signing a new certificate @@ -349,7 +360,7 @@ file to specify requests for a variety of purposes. Add a specific extension to the certificate (if B<-x509> is in use) or certificate request. The argument must have the form of -a key=value pair as it would appear in a config file. +a C pair as it would appear in a config file. This option can be given multiple times. @@ -770,6 +781,10 @@ The <-nodes> option was deprecated in OpenSSL 3.0, too; use B<-noenc> instead. The B<-reqexts> option has been made an alias of B<-extensions> in OpenSSL 3.2. +Since OpenSSL 3.2, +generated certificates bear X.509 version 3 unless B<-x509v1> is given, +and key identifier extensions are included by default. + =head1 COPYRIGHT Copyright 2000-2021 The OpenSSL Project Authors. All Rights Reserved. diff --git a/doc/man1/openssl-x509.pod.in b/doc/man1/openssl-x509.pod.in index ad9659c565..84110d24f5 100644 --- a/doc/man1/openssl-x509.pod.in +++ b/doc/man1/openssl-x509.pod.in @@ -87,6 +87,10 @@ convert certificates to various forms, edit certificate trust settings, generate certificates from scratch or from certificating requests and then self-signing them or signing them like a "micro CA". +Generated certificates bear X.509 version 3. +Unless specified otherwise, +key identifier extensions are included as described in L. + Since there are a large number of options they will split up into various sections. @@ -303,7 +307,7 @@ as used by OpenSSL before version 1.0.0. Prints out the certificate extensions in text form. Can also be used to restrict which extensions to copy. Extensions are specified -with a comma separated string, e.g., "subjectAltName,subjectKeyIdentifier". +with a comma separated string, e.g., "subjectAltName, subjectKeyIdentifier". See the L manual page for the extension names. =item B<-ocspid> @@ -435,9 +439,13 @@ If this option is not specified then the extensions should either be contained in the unnamed (default) section or the default section should contain a variable called "extensions" which contains the section to use. + See the L manual page for details of the extension section format. +Unless specified otherwise, +key identifier extensions are included as described in L. + =item B<-sigopt> I:I Pass options to the signature algorithm during sign operations. @@ -782,6 +790,9 @@ The B<-engine> option was deprecated in OpenSSL 3.0. The B<-C> option was removed in OpenSSL 3.0. +Since OpenSSL 3.2, generated certificates bear X.509 version 3, +and key identifier extensions are included by default. + =head1 COPYRIGHT Copyright 2000-2021 The OpenSSL Project Authors. All Rights Reserved. diff --git a/doc/man3/X509_get_version.pod b/doc/man3/X509_get_version.pod index 082859e4f4..5d377c91d3 100644 --- a/doc/man3/X509_get_version.pod +++ b/doc/man3/X509_get_version.pod @@ -22,13 +22,13 @@ certificate request or CRL version =head1 DESCRIPTION X509_get_version() returns the numerical value of the version field of -certificate B. These correspond to the constants B, +certificate I. These correspond to the constants B, B, and B. Note: the values of these constants are defined by standards (X.509 et al) to be one less than the certificate version. So B has value 2 and B has value 0. X509_set_version() sets the numerical value of the version field of certificate -B to B. +I to I. Similarly X509_REQ_get_version(), X509_REQ_set_version(), X509_CRL_get_version() and X509_CRL_set_version() get and set the version diff --git a/doc/man3/X509_sign.pod b/doc/man3/X509_sign.pod index df66e3e5b7..af21148f67 100644 --- a/doc/man3/X509_sign.pod +++ b/doc/man3/X509_sign.pod @@ -25,6 +25,8 @@ sign certificate, certificate request, or CRL signature X509_sign() signs certificate I using private key I and message digest I and sets the signature in I. X509_sign_ctx() also signs certificate I but uses the parameters contained in digest context I. +If the certificate information includes X.509 extensions, +these two functions make sure that the certificate bears X.509 version 3. X509_REQ_sign(), X509_REQ_sign_ctx(), X509_CRL_sign(), and X509_CRL_sign_ctx() diff --git a/doc/man5/x509v3_config.pod b/doc/man5/x509v3_config.pod index e369997b60..cf42e9053a 100644 --- a/doc/man5/x509v3_config.pod +++ b/doc/man5/x509v3_config.pod @@ -173,14 +173,27 @@ Examples: =head2 Subject Key Identifier The SKID extension specification has a value with three choices. -If the value is the word B then no SKID extension will be included. -If the value is the word B, or by default for the B, B, and -B apps, the process specified in RFC 5280 section 4.2.1.2. (1) is followed: + +=over 4 + +=item B + +No SKID extension will be included. + +=item B + +The process specified in RFC 5280 section 4.2.1.2. (1) is followed: The keyIdentifier is composed of the 160-bit SHA-1 hash of the value of the BIT STRING subjectPublicKey (excluding the tag, length, and number of unused bits). -Otherwise, the value must be a hex string (possibly with C<:> separating bytes) -to output directly, however, this is strongly discouraged. +=item A hex string (possibly with C<:> separating bytes) + +The provided value is output directly. +This choice is strongly discouraged. + +=back + +By default the B, B, and B apps behave as if B was given. Example: @@ -195,6 +208,7 @@ or both of them, separated by C<,>. Either or both can have the option B, indicated by putting a colon C<:> between the value and this option. For self-signed certificates the AKID is suppressed unless B is present. + By default the B, B, and B apps behave as if B was given for self-signed certificates and BC<,> B otherwise. diff --git a/gost-engine b/gost-engine index b2b4d629f1..a6b90523e4 160000 --- a/gost-engine +++ b/gost-engine @@ -1 +1 @@ -Subproject commit b2b4d629f100eaee9f5942a106b1ccefe85b8808 +Subproject commit a6b90523e4ea6010b1109b0bae7e2a73b5b025c5 diff --git a/test/ca-and-certs.cnf b/test/ca-and-certs.cnf index 463b49954c..58ca0eda64 100644 --- a/test/ca-and-certs.cnf +++ b/test/ca-and-certs.cnf @@ -31,6 +31,8 @@ organizationName = Dodgy Brothers 0.commonName = Brother 1 1.commonName = $ENV::CN2 +[ empty ] + [ v3_ee ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always diff --git a/test/recipes/25-test_req.t b/test/recipes/25-test_req.t index a11fe36a58..1487fa70be 100644 --- a/test/recipes/25-test_req.t +++ b/test/recipes/25-test_req.t @@ -15,7 +15,7 @@ use OpenSSL::Test qw/:DEFAULT srctop_file/; setup("test_req"); -plan tests => 92; +plan tests => 102; require_ok(srctop_file('test', 'recipes', 'tconversion.pl')); @@ -393,16 +393,7 @@ sub generate_cert { push(@cmd, ("-CA", $ca_cert, "-CAkey", $ca_key)) unless $ss; ok(run(app([@cmd])), "generate $cert"); } -sub has_SKID { - my $cert = shift @_; - my $expect = shift @_; - cert_contains($cert, "Subject Key Identifier", $expect); -} -sub has_AKID { - my $cert = shift @_; - my $expect = shift @_; - cert_contains($cert, "Authority Key Identifier", $expect); -} + sub has_keyUsage { my $cert = shift @_; my $expect = shift @_; @@ -424,6 +415,12 @@ my $SKID_AKID = "subjectKeyIdentifier,authorityKeyIdentifier"; # # SKID +my $cert = "self-signed_default_SKID_no_explicit_exts.pem"; +generate_cert($cert); +has_version($cert, 3); +has_SKID($cert, 1); # SKID added, though no explicit extensions given +has_AKID($cert, 0); + my $cert = "self-signed_v3_CA_hash_SKID.pem"; generate_cert($cert, @v3_ca, "-addext", "subjectKeyIdentifier = hash"); has_SKID($cert, 1); # explicit hash SKID @@ -441,7 +438,8 @@ strict_verify($cert, 1); # AKID of self-signed certs $cert = "self-signed_v1_CA_no_KIDs.pem"; -generate_cert($cert); +generate_cert($cert, "-x509v1"); +has_version($cert, 1); cert_ext_has_n_different_lines($cert, 0, $SKID_AKID); # no SKID and no AKID #TODO strict_verify($cert, 1); # self-signed v1 root cert should be accepted as CA @@ -515,6 +513,8 @@ strict_verify($cert, 1); $cert = "self-issued_v3_CA_no_AKID.pem"; generate_cert($cert, "-addext", "authorityKeyIdentifier = none", "-in", srctop_file(@certs, "x509-check.csr")); +has_version($cert, 3); +has_SKID($cert, 1); # SKID added, though no explicit extensions given has_AKID($cert, 0); strict_verify($cert, 1); @@ -556,6 +556,11 @@ cert_ext_has_n_different_lines($cert, 6, $SKID_AKID); # SKID != AKID, both force # AKID of not self-issued certs +$cert = "regular_v3_EE_default_KIDs_no_other_exts.pem"; +generate_cert($cert, "-key", srctop_file(@certs, "ee-key.pem")); +has_version($cert, 3); +cert_ext_has_n_different_lines($cert, 4, $SKID_AKID); # SKID != AKID + $cert = "regular_v3_EE_default_KIDs.pem"; generate_cert($cert, "-addext", "keyUsage = dataEncipherment", "-key", srctop_file(@certs, "ee-key.pem")); diff --git a/test/recipes/25-test_x509.t b/test/recipes/25-test_x509.t index 0d4fc548cc..c843d3870a 100644 --- a/test/recipes/25-test_x509.t +++ b/test/recipes/25-test_x509.t @@ -16,7 +16,7 @@ use OpenSSL::Test qw/:DEFAULT srctop_file/; setup("test_x509"); -plan tests => 29; +plan tests => 32; # Prevent MSys2 filename munging for arguments that look like file paths but # aren't @@ -202,6 +202,11 @@ ok(run(app(["openssl", "x509", "-req", "-text", "-CAcreateserial", # Verify issuer is CA ok(get_issuer($b_cert) =~ /CN=ca.example.com/); +# although no explicit extensions given: +has_version($b_cert, 3); +has_SKID($b_cert, 1); +has_AKID($b_cert, 1); + SKIP: { skip "EC is not supported by this OpenSSL build", 1 if disabled("ec"); diff --git a/test/recipes/80-test_ca.t b/test/recipes/80-test_ca.t index eb025f4d59..6a7a74b7e7 100644 --- a/test/recipes/80-test_ca.t +++ b/test/recipes/80-test_ca.t @@ -25,18 +25,25 @@ my $std_openssl_cnf = '"' . srctop_file("apps", $^O eq "VMS" ? "openssl-vms.cnf" : "openssl.cnf") . '"'; +sub src_file { + return srctop_file("test", "certs", shift); +} + rmtree("demoCA", { safe => 0 }); -plan tests => 15; +plan tests => 20; + +require_ok(srctop_file("test", "recipes", "tconversion.pl")); + SKIP: { - my $cakey = srctop_file("test", "certs", "ca-key.pem"); + my $cakey = src_file("ca-key.pem"); $ENV{OPENSSL_CONFIG} = qq(-config "$cnf"); skip "failed creating CA structure", 4 if !ok(run(perlapp(["CA.pl","-newca", "-extra-req", "-key $cakey"], stdin => undef)), 'creating CA structure'); - my $eekey = srctop_file("test", "certs", "ee-key.pem"); + my $eekey = src_file("ee-key.pem"); $ENV{OPENSSL_CONFIG} = qq(-config "$cnf"); skip "failed creating new certificate request", 3 if !ok(run(perlapp(["CA.pl","-newreq", @@ -53,7 +60,7 @@ plan tests => 15; skip "CT not configured, can't use -precert", 1 if disabled("ct"); - my $eekey2 = srctop_file("test", "certs", "ee-key-3072.pem"); + my $eekey2 = src_file("ee-key-3072.pem"); $ENV{OPENSSL_CONFIG} = qq(-config "$cnf"); ok(run(perlapp(["CA.pl", "-precert", '-extra-req', "-section userreq -key $eekey2"], stderr => undef)), 'creating new pre-certificate'); @@ -65,17 +72,25 @@ SKIP: { is(yes(cmdstr(app(["openssl", "ca", "-config", $cnf, - "-in", srctop_file("test", "certs", "sm2-csr.pem"), + "-in", src_file("sm2-csr.pem"), "-out", "sm2-test.crt", "-sigopt", "distid:1234567812345678", "-vfyopt", "distid:1234567812345678", "-md", "sm3", - "-cert", srctop_file("test", "certs", "sm2-root.crt"), - "-keyfile", srctop_file("test", "certs", "sm2-root.key")]))), + "-cert", src_file("sm2-root.crt"), + "-keyfile", src_file("sm2-root.key")]))), 0, "Signing SM2 certificate request"); } +my $v3_cert = "v3-test.crt"; +ok(run(app(["openssl", "ca", "-batch", "-config", $cnf, "-extensions", "empty", + "-in", src_file("x509-check.csr"), "-out", $v3_cert]))); +# although no explicit extensions given: +has_version($v3_cert, 3); +has_SKID($v3_cert, 1); +has_AKID($v3_cert, 1); + test_revoke('notimes', { should_succeed => 1, }); diff --git a/test/recipes/90-test_store.t b/test/recipes/90-test_store.t index 12a8a32d98..3af8178e89 100644 --- a/test/recipes/90-test_store.t +++ b/test/recipes/90-test_store.t @@ -402,7 +402,7 @@ sub init { }, grep(/-key-pkcs8-pbes2-sha256\.pem$/, @generated_files)) # *-cert.pem (intermediary for the .p12 inits) && run(app(["openssl", "req", "-x509", @std_args, - "-config", $cnf, "-noenc", + "-config", $cnf, "-reqexts", "v3_ca", "-noenc", "-key", $cakey, "-out", "cacert.pem"])) && runall(sub { my $srckey = shift; diff --git a/test/recipes/tconversion.pl b/test/recipes/tconversion.pl index 063be620a3..222ef1ac13 100644 --- a/test/recipes/tconversion.pl +++ b/test/recipes/tconversion.pl @@ -132,6 +132,24 @@ sub cert_contains { # not unlinking $out } +sub has_version { + my $cert = shift @_; + my $expect = shift @_; + cert_contains($cert, "Version: $expect", 1); +} + +sub has_SKID { + my $cert = shift @_; + my $expect = shift @_; + cert_contains($cert, "Subject Key Identifier", $expect); +} + +sub has_AKID { + my $cert = shift @_; + my $expect = shift @_; + cert_contains($cert, "Authority Key Identifier", $expect); +} + sub uniq (@) { my %seen = (); grep { not $seen{$_}++ } @_;