Browse Source

APPS: generated certs bear X.509 V3, unless -x509v1 option of req app is given

Reviewed-by: Tomas Mraz <tomas@openssl.org>
Reviewed-by: Hugo Landau <hlandau@openssl.org>
Reviewed-by: David von Oheimb <david.von.oheimb@siemens.com>
(Merged from https://github.com/openssl/openssl/pull/19271)
Dr. David von Oheimb 1 year ago
parent
commit
342e3652c7

+ 7 - 0
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.
 

+ 1 - 1
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') */

+ 1 - 1
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,

+ 2 - 4
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;
 

+ 9 - 4
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 {

+ 2 - 2
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) {

+ 6 - 0
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,

+ 9 - 2
doc/man1/openssl-ca.pod.in

@@ -71,6 +71,11 @@ B<openssl> B<ca>
 
 This command emulates a CA application.
 See the B<WARNINGS> 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<x509v3_config(5)>.
+
 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<x509_extensions>
 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<x509v3_config(5)> 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<openssl(1)>,

+ 16 - 1
doc/man1/openssl-req.pod.in

@@ -33,6 +33,7 @@ B<openssl> B<req>
 [B<-config> I<filename>]
 [B<-section> I<name>]
 [B<-x509>]
+[B<-x509v1>]
 [B<-CA> I<filename>|I<uri>]
 [B<-CAkey> I<filename>|I<uri>]
 [B<-days> I<n>]
@@ -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<x509v3_config(5)>.
+
+=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<filename>|I<uri>
 
 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<key=value> 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.

+ 12 - 1
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<x509v3_config(5)>.
+
 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<x509v3_config(5)> 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<x509v3_config(5)> manual page for details of the
 extension section format.
 
+Unless specified otherwise,
+key identifier extensions are included as described in L<x509v3_config(5)>.
+
 =item B<-sigopt> I<nm>:I<v>
 
 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.

+ 2 - 2
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<x>. These correspond to the constants B<X509_VERSION_1>,
+certificate I<x>. These correspond to the constants B<X509_VERSION_1>,
 B<X509_VERSION_2>, and B<X509_VERSION_3>. Note: the values of these constants
 are defined by standards (X.509 et al) to be one less than the certificate
 version. So B<X509_VERSION_3> has value 2 and B<X509_VERSION_1> has value 0.
 
 X509_set_version() sets the numerical value of the version field of certificate
-B<x> to B<version>.
+I<x> to I<version>.
 
 Similarly X509_REQ_get_version(), X509_REQ_set_version(),
 X509_CRL_get_version() and X509_CRL_set_version() get and set the version

+ 2 - 0
doc/man3/X509_sign.pod

@@ -25,6 +25,8 @@ sign certificate, certificate request, or CRL signature
 X509_sign() signs certificate I<x> using private key I<pkey> and message
 digest I<md> and sets the signature in I<x>. X509_sign_ctx() also signs
 certificate I<x> but uses the parameters contained in digest context I<ctx>.
+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()

+ 19 - 5
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<none> then no SKID extension will be included.
-If the value is the word B<hash>, or by default for the B<x509>, B<req>, and
-B<ca> apps, the process specified in RFC 5280 section 4.2.1.2. (1) is followed:
+
+=over 4
+
+=item B<none>
+
+No SKID extension will be included.
+
+=item B<hash>
+
+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<x509>, B<req>, and B<ca> apps behave as if B<hash> was given.
 
 Example:
 
@@ -195,6 +208,7 @@ or both of them, separated by C<,>.
 Either or both can have the option B<always>,
 indicated by putting a colon C<:> between the value and this option.
 For self-signed certificates the AKID is suppressed unless B<always> is present.
+
 By default the B<x509>, B<req>, and B<ca> apps behave as if B<none> was given
 for self-signed certificates and B<keyid>C<,> B<issuer> otherwise.
 

+ 1 - 1
gost-engine

@@ -1 +1 @@
-Subproject commit b2b4d629f100eaee9f5942a106b1ccefe85b8808
+Subproject commit a6b90523e4ea6010b1109b0bae7e2a73b5b025c5

+ 2 - 0
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

+ 17 - 12
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"));

+ 6 - 1
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");

+ 22 - 7
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,
 });

+ 1 - 1
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;

+ 18 - 0
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{$_}++ } @_;