]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
Polish AKID/SKID handling and related docs
authorViktor Dukhovni <openssl-users@dukhovni.org>
Sat, 28 Feb 2026 15:40:52 +0000 (02:40 +1100)
committerNeil Horman <nhorman@openssl.org>
Mon, 2 Mar 2026 17:04:10 +0000 (12:04 -0500)
- Drop empty requestExtensions CSR attributes

  While `attributes` is a required CSR field, its `requestExtensions`
  attribute is optional, and should be avoided if empty.

- Detail documentation of req extension section selection

- Fixed req CI test case naming nits

- Refer to config(5) for meaning of "variable"

- In code comments, note possibility of fewer extensions after adding
  an ignored empty extension while deleting a previous value.

- Mention new "nonss" AKID qualifier in CHANGES

- I x509_config(5) Clarify AKID issuer as fallback (unless ":always")

- In stock config file, comment proxy cert issuer SKID expectation.

- Clarify comment on empty SKID/AKID vs. prior value

- Use B<default> not C<default> for unnamed section

- Polish (mostly CSR) extension handling

  * In update_req_extensions() drop extraneous duplicate
    X509at_delete_attr() call.
  * Consolidate empty SKID/AKID detection in new
    ossl_ignored_x509_extension().
  * Handle empty SKID/AKID also in X509V3_add1_i2d().
  * In test_drop_empty_csr_keyids() exercise the full NCONF extension
    management stack, using X509_REQ_get_attr_count() to check that
    after "subjectKeyIdentifier = none" not an even an empty extension
    set remains as a CSR attribute (X509_REQ_get_extensions() always
    returns at least an empty stack because NULL signals an error).

Reviewed-by: David von Oheimb <david.von.oheimb@siemens.com>
Reviewed-by: Tim Hudson <tjh@openssl.org>
Reviewed-by: Neil Horman <nhorman@openssl.org>
MergeDate: Mon Mar  2 17:04:22 2026
(Merged from https://github.com/openssl/openssl/pull/30217)

16 files changed:
CHANGES.md
apps/openssl-vms.cnf
apps/openssl.cnf
crypto/x509/v3_conf.c
crypto/x509/v3_lib.c
crypto/x509/x509_ext.c
crypto/x509/x509_local.h
crypto/x509/x509_v3.c
crypto/x509/x_all.c
doc/man1/openssl-req.pod.in
doc/man1/openssl-x509.pod.in
doc/man5/config.pod
doc/man5/x509v3_config.pod
test/recipes/25-test_req.t
test/recipes/80-test_ca.t
test/x509_test.c

index 09c2d4207cfac597469d3ff62c70b069cc68a1ec..bef6abebb6aa1364a496fbbe36adaeafb06159c4 100644 (file)
@@ -37,6 +37,10 @@ OpenSSL Releases
    options just like any other extension.  See their documentation and also
    `x509v3_config(5)` for additional details.
 
+   Updated the syntax of the `subjectKeyIdentifier` (SKID) and
+   `authorityKeyIdentifier` (AKID) extensions, introducing the `nonss` qualifier
+   for the `keyid` and `issuer` keywords.
+
    The x509 "mini-CA" now attempts to find extension settings in the default
    configuration file even if neither the `-extfile` nor the `-extensions`
    option is explicitly specified.  Failure to open the default configuration
index da0a530bb550afa5cfe17c4e8ca3761d6fd5afbc..1378dfe2480d01b9f342ce9aa5735875f80f9770 100644 (file)
@@ -291,6 +291,7 @@ basicConstraints=CA:FALSE
 
 # PKIX recommendations harmless if included in all certificates.
 subjectKeyIdentifier=hash
+# The issuer of a proxy certificate should have SKID.
 authorityKeyIdentifier=keyid:always
 
 # This stuff is for subjectAltName and issuerAltname.
index 1de7883d23df01305299508e41eb06234d69dfaa..c5b72620781306755c1a518c5720720793db2d8c 100644 (file)
@@ -291,6 +291,7 @@ basicConstraints=CA:FALSE
 
 # PKIX recommendations harmless if included in all certificates.
 subjectKeyIdentifier=hash
+# The issuer of a proxy certificate should have SKID.
 authorityKeyIdentifier=keyid:always
 
 # This stuff is for subjectAltName and issuerAltname.
index 81cfce8b4833c9ac93c69991b162d1b7738b24fc..6db7f37d6852b9acfb9c432e434d8607de2c006e 100644 (file)
@@ -396,11 +396,12 @@ update_req_extensions(X509_REQ *req, int *pnid, STACK_OF(X509_EXTENSION) *exts)
 
         if (att == NULL)
             goto end;
-        X509at_delete_attr(req->req_info.attributes, loc);
         X509_ATTRIBUTE_free(att);
     }
-    ret = X509_REQ_add1_attr_by_NID(req, *pnid, V_ASN1_SEQUENCE, ext, extlen);
-
+    if (sk_X509_EXTENSION_num(exts) > 0)
+        ret = X509_REQ_add1_attr_by_NID(req, *pnid, V_ASN1_SEQUENCE, ext, extlen);
+    else
+        ret = 1;
 end:
     OPENSSL_free(ext);
     return ret;
index 8217fcdb2cc99d24b3dbbfc4d75bc4ad27ac60f7..68ac36baf797ff2e3343eb739a93504f790f2fa8 100644 (file)
@@ -15,6 +15,7 @@
 #include <openssl/x509v3.h>
 
 #include "ext_dat.h"
+#include "x509_local.h"
 
 static STACK_OF(X509V3_EXT_METHOD) *ext_list = NULL;
 
@@ -129,6 +130,38 @@ int X509V3_add_standard_extensions(void)
     return 1;
 }
 
+int ossl_ignored_x509_extension(const X509_EXTENSION *ex, int flags)
+{
+    /*
+     * Empty OCTET STRINGs and empty SEQUENCEs encode to just two bytes of tag
+     * (0x04 or 0x30) and length (0x00).  We use this fact to suppress empty
+     * AKID and SKID extensions that may be briefly generated when processing
+     * the "= none" value or only ":nonss"-qualified AKIDs when the subject is
+     * self-signed.
+     *
+     * The resulting extension is empty, and must not be retained, but does
+     * serve to drop any previous value of the same extension, when called
+     * via
+     * - X509v3_add_extensions(), or
+     * - either of X509V3_add1_i2d() or X509V3_EXT_add_nconf_sk(),
+     *   with a flags (or ctx->flags) value that allows replacement.
+     */
+    if (ex->value.length == 2
+        && (ex->value.data[0] == 0x30 || ex->value.data[0] == 0x04)) {
+        ASN1_OBJECT *obj = ex->object;
+        ASN1_OBJECT *skid = OBJ_nid2obj(NID_subject_key_identifier);
+        ASN1_OBJECT *akid = OBJ_nid2obj(NID_authority_key_identifier);
+
+        if (OBJ_cmp(obj, skid) == 0 || OBJ_cmp(obj, akid) == 0) {
+            if ((flags & X509V3_ADD_SILENT) == 0)
+                ERR_raise_data(ERR_LIB_X509, X509_R_INVALID_EXTENSION,
+                    "Invalid empty X.509 %s extension", obj->sn);
+            return 1;
+        }
+    }
+    return 0;
+}
+
 /* Return an extension internal structure */
 
 void *X509V3_EXT_d2i(const X509_EXTENSION *ext)
@@ -278,9 +311,13 @@ int X509V3_add1_i2d(STACK_OF(X509_EXTENSION) **x, int nid, void *value,
     /* If extension exists replace it.. */
     if (extidx >= 0) {
         extmp = sk_X509_EXTENSION_value(*x, extidx);
-        X509_EXTENSION_free(extmp);
-        if (!sk_X509_EXTENSION_set(*x, extidx, ext))
+        if (ossl_ignored_x509_extension(ext, X509V3_ADD_SILENT)) {
+            if (!sk_X509_EXTENSION_delete(*x, extidx))
+                return -1;
+        } else if (!sk_X509_EXTENSION_set(*x, extidx, ext)) {
             return -1;
+        }
+        X509_EXTENSION_free(extmp);
         return 1;
     }
 
index 55aae2961829f06f75b4676f27eb75c859bed86f..2b73e14a60d82dd579fadf54008ce5035c3508f1 100644 (file)
@@ -117,10 +117,17 @@ int X509_add_ext(X509 *x, const X509_EXTENSION *ex, int loc)
 
     if (X509v3_add_ext(&exts, ex, loc) == NULL)
         return 0;
+    /*
+     * A duplicate empty SKID/AKID extension can displace a prior non-empty
+     * one, but is then not itself added, so, somewhat counter-intutively,  the
+     * the extension list can become empty after an "add", in which case we must
+     * drop the extension stack entirely, setting it to NULL.  The extensions
+     * list is either non-empty or absent.
+     */
     if (sk_X509_EXTENSION_num(exts) != 0) {
         x->cert_info.extensions = exts;
     } else {
-        sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
+        sk_X509_EXTENSION_free(exts);
         sk_X509_EXTENSION_pop_free(x->cert_info.extensions, X509_EXTENSION_free);
         x->cert_info.extensions = NULL;
     }
index 44260a23b797cb18b68f7962453963316e5413a8..6500226ec389f38a51e8786a66546c2e17ef174e 100644 (file)
@@ -183,6 +183,7 @@ DEFINE_STACK_OF(BY_DIR_ENTRY)
 typedef STACK_OF(X509_NAME_ENTRY) STACK_OF_X509_NAME_ENTRY;
 DEFINE_STACK_OF(STACK_OF_X509_NAME_ENTRY)
 
+int ossl_ignored_x509_extension(const X509_EXTENSION *ex, int flags);
 int ossl_x509_likely_issued(const X509 *issuer, const X509 *subject);
 int ossl_x509_signing_allowed(const X509 *issuer, const X509 *subject);
 int ossl_x509_store_ctx_get_by_subject(const X509_STORE_CTX *ctx, X509_LOOKUP_TYPE type,
index e34316258295f2ed1d7775f9df56bd909e900b2f..f9ea5f75fe92dcc8a871f8d815ee29cd2e9ba8de 100644 (file)
@@ -120,23 +120,8 @@ STACK_OF(X509_EXTENSION) *X509v3_add_ext(STACK_OF(X509_EXTENSION) **x,
     } else
         sk = *x;
 
-    /*
-     * Empty OCTET STRINGs and empty SEQUENCEs encode to just two bytes of tag
-     * (0x04 or 0x30) and length (0x00).  We use this fact to suppress empty
-     * AKID and SKID extensions that may be briefly generated when processing
-     * the "= none" value or only ":nonss"-qualified AKIDs when the subject is
-     * self-signed.  The resulting extension is empty, and must not be retained,
-     * but does serve to drop any previous value of the same extension.
-     */
-    if (ex->value.length == 2
-        && (ex->value.data[0] == 0x30 || ex->value.data[0] == 0x04)) {
-        ASN1_OBJECT *obj = ex->object;
-        ASN1_OBJECT *skid = OBJ_nid2obj(NID_subject_key_identifier);
-        ASN1_OBJECT *akid = OBJ_nid2obj(NID_authority_key_identifier);
-
-        if (OBJ_cmp(obj, skid) == 0 || OBJ_cmp(obj, akid) == 0)
-            goto done;
-    }
+    if (ossl_ignored_x509_extension(ex, X509V3_ADD_SILENT))
+        goto done;
 
     n = sk_X509_EXTENSION_num(sk);
     if (loc > n)
index fa7218ef86cd459dd849d959c832f3523d278910..f753282ca1bf5b0e9e0f5321a14f9c2124cf98ba 100644 (file)
@@ -30,6 +30,7 @@
 #include "crypto/x509.h"
 #include "crypto/x509_acert.h"
 #include "crypto/rsa.h"
+#include "x509_local.h"
 
 int X509_verify(const X509 *a, EVP_PKEY *r)
 {
@@ -83,23 +84,9 @@ static int bad_keyid_exts(const STACK_OF(X509_EXTENSION) *exts)
 
     for (i = 0; i < n; ++i) {
         X509_EXTENSION *ext = sk_X509_EXTENSION_value(exts, i);
-        const ASN1_STRING *der = X509_EXTENSION_get_data(ext);
-
-        /*
-         * Empty OCTET STRINGs and empty SEQUENCEs encode to just two bytes of
-         * tag (0x04 or 0x30) and length (0x00).
-         */
-        if (der->length == 2 && (der->data[0] == 0x04 || der->data[0] == 0x30)) {
-            const ASN1_OBJECT *obj = X509_EXTENSION_get_object(ext);
-            const ASN1_OBJECT *skid = OBJ_nid2obj(NID_subject_key_identifier);
-            const ASN1_OBJECT *akid = OBJ_nid2obj(NID_authority_key_identifier);
-
-            if (OBJ_cmp(obj, skid) != 0 && OBJ_cmp(obj, akid) != 0)
-                continue;
-            ERR_raise_data(ERR_LIB_X509, X509_R_INVALID_EXTENSION,
-                "Invalid empty X.509 %s extension", obj->sn);
+
+        if (ossl_ignored_x509_extension(ext, X509V3_ADD_DEFAULT))
             return 1;
-        }
     }
     return 0;
 }
index 031fd8ffcafec28e7baa81876b6ad723e1f423db..fb46b8218e57e42c6f684f64bfcbcf456301c294 100644 (file)
@@ -393,6 +393,16 @@ certificate requests.
 This allows several different sections to be used in the same configuration
 file to specify requests for a variety of purposes.
 
+When signing a new CSR or X.509 certificate, if neither the the B<-extensions>
+option nor its alias B<-reqexts> are specified, the name of the extension
+section is taken from from C<req> section of the configuration file.
+Specifically, from the value of that section's C<req_extensions> variable (for
+a CSR) or its C<x509_extensions> variable (for a certificate).
+If there is no setting for the variable (name/value assignment, see
+L<config(5)>) in question, the configuration file does not add any extensions.
+The B<-section> option can be used to override C<req> with a custom section in
+which to look for the above C<req_extensions> and C<x509_extensions> variables.
+
 OpenSSL 4.0 removed built-in generation of the B<subjectKeyIdentifier> and
 B<authorityKeyIdentifier> extensions when generating certificates.
 Any desired extensions need to be listed either in the configuration file or
index eff02af0476734a28ef31d45bb56534cbd0da3c2..b1c3f4f1902885b5fa3e7304d6667f9fb5e6144e 100644 (file)
@@ -490,9 +490,9 @@ This file must exist, be readable, and all the listed extensions must be valid.
 
 If only the B<-extfile> I<filename> option is given, the section name to use is
 taken from the value of the variable named C<extensions> in the file's unnamed
-(C<default>) section.
-If no B<extensions> setting is found, the file's C<default> section itself is
-instead used as the list of extensions to add.
+section (also called its B<default> section, see L<config(5)>).
+If no B<extensions> setting is found, the file's B<default> (unnamed) section
+itself is instead used as the list of extensions to add.
 As before, the file must be readable and all the extensions must be valid.
 
 Prior to OpenSSL 4.0, the B<-extensions> option required that the B<-extfile>
@@ -508,8 +508,8 @@ As before, the file must be readable and all the extensions must be valid.
 If neither of the options are given, an attempt is made to open the default
 configuration file (as detailed above).
 If the file does not exist or cannot be opened, no extensions are added.
-If the unnamed (C<default>) section of the file does not list an B<extensions>
-variable, no extensions are added.
+If the unnamed section (B<default>, see L<config(5)>) of the file does not list
+an B<extensions> variable, no extensions are added.
 Otherwise, the section named by the B<extensions> variable is taken to be the
 list of extensions to add.
 The extensions listed there must all be valid.
@@ -853,6 +853,7 @@ L<openssl-ca(1)>,
 L<openssl-genrsa(1)>,
 L<openssl-gendsa(1)>,
 L<openssl-verify(1)>,
+L<config(5)>,
 L<x509v3_config(5)>
 
 =head1 HISTORY
index 36159c7820c0782aca14324a12164ffadb4f9089..6198ec6f6a56c1fe1074b74fa06f0921960f020e 100644 (file)
@@ -89,13 +89,11 @@ A configuration file is divided into a number of I<sections>.  A section
 begins with the section name in square brackets, and ends when a new
 section starts, or at the end of the file.  The section name can consist
 of alphanumeric characters and underscores.
-Whitespace between the name and the brackets is removed.
+Whitespace between the name and the brackets is ignored.
 
-The first section of a configuration file is special and is referred to
-as the B<default> section. This section is usually unnamed and spans from
-the start of file until the first named section. When a name is being
-looked up, it is first looked up in the current or named section,
-and then the default section if necessary.
+The content at the start of the configuration file that precedes its first
+named section is special and is referred to as the B<default> or B<unnamed>
+section.
 
 The environment is mapped onto a section called B<ENV>.
 
index 4ca94f0539eb75638c9188704e80e1093e8d6740..358d67365e270373298788cc733c5deb3fbd4497 100644 (file)
@@ -218,7 +218,8 @@ identifier.
 The B<issuer> keyword asks that the AKID include the serial number and issuer
 distinguished name (grandparent name of subject certificate) of the issuer
 certificate.
-By default these are added only as a fallback when no SKID is available in the
+Unless qualified with C<always> (as described below), these are added only as a
+fallback if the B<keyid> is not requested, or no SKID was available in the
 issuer certificate.
 
 Either or both keywords can be suffixed with an optional qualifier, which can
@@ -237,9 +238,6 @@ When creating a self-signed certificate, be sure to specify a SKID extension if
 you want to have a mandatory (C<always> qualified) B<keyid> in the AKID
 extension.
 
-If an empty AKID would otherwise be generated, the extension is instead skipped
-entirely, just as with C<none>.
-
 See L<openssl-x509(1)>, L<openssl-req(1)>, and L<openssl-ca(1)> for further
 details relevant to that specific command-line utility.
 
index eb4088949acfd96f367413d6dd829f0f96e000df..43e0c8e4a07c89928bd793ce2a8ac3d6ec876026 100644 (file)
@@ -570,7 +570,7 @@ my $SKID_AKID = "subjectKeyIdentifier,authorityKeyIdentifier";
 
 # # SKID
 
-my $cert = "self-signed_default_SKID_no_explicit_exts.pem";
+my $cert = "self-signed_default_SKID_minimal_exts.pem";
 generate_cert($cert);
 has_version($cert, 3);
 has_SKID($cert, 1);
@@ -618,7 +618,7 @@ has_AKID($cert, 0); # forced no AKID
 
 $cert = "self-signed_v3_CA_explicit_AKID.pem";
 generate_cert($cert, @v3_ca, "-addext", "authorityKeyIdentifier = keyid:nonss");
-has_AKID($cert, 0); # for self-signed cert, AKID suppressed and not forced
+has_AKID($cert, 0); # for self-signed cert, AKID suppressed since self-signed
 
 $cert = "self-signed_v3_CA_forced_AKID.pem";
 generate_cert($cert, @v3_ca, "-addext", "authorityKeyIdentifier = keyid:always");
@@ -627,7 +627,7 @@ strict_verify($cert, 1);
 
 $cert = "self-signed_v3_CA_issuer_AKID.pem";
 generate_cert($cert, @v3_ca, "-addext", "authorityKeyIdentifier = issuer:nonss");
-has_AKID($cert, 0); # suppressed AKID since not forced
+has_AKID($cert, 0); # suppressed AKID since self-signed
 
 $cert = "self-signed_v3_CA_forced_issuer_AKID.pem";
 generate_cert($cert, @v3_ca, "-addext", "authorityKeyIdentifier = issuer:always");
index 363fc100b5308ab32d548ee183370da5c4aed241..f0f11902d75e79876e448435f8f232b75dfb8873 100644 (file)
@@ -82,7 +82,7 @@ SKIP: {
 my $v3_cert = "v3-test.crt";
 ok(run(app(["openssl", "ca", "-batch", "-config", $cnf, "-extensions", "minimal",
             "-in", src_file("x509-check.csr"), "-out", $v3_cert])));
-# although no explicit extensions given:
+# The "minimal" extensions include SKID and AKID.
 has_version($v3_cert, 3);
 has_SKID($v3_cert, 1);
 has_AKID($v3_cert, 1);
index a3306dc8af3326cfc2e3fe7445db178e0cd5f366..1381a1d33d5059d5d0c9f5d9bb9cdc0b763bab27 100644 (file)
@@ -339,6 +339,8 @@ err:
 static int test_drop_empty_csr_keyids(void)
 {
     static const unsigned char commonName[] = "test";
+    BIO *bio = NULL;
+    CONF *conf = NULL;
     X509_REQ *x = NULL;
     X509_NAME *subject = NULL;
     X509_NAME_ENTRY *name_entry = NULL;
@@ -360,39 +362,42 @@ static int test_drop_empty_csr_keyids(void)
         || !TEST_int_eq(X509_REQ_set_pubkey(x, pubkey), 1))
         goto err;
 
-    X509V3_set_ctx(&ctx, NULL, NULL, x, NULL, 0);
-    if (!TEST_ptr(ext = X509V3_EXT_conf(NULL, &ctx, "subjectKeyIdentifier",
-                      "none"))
-        || !TEST_ptr(X509v3_add_ext(&exts, ext, -1))
-        || !TEST_int_eq(sk_X509_EXTENSION_num(exts), 0))
-        goto err;
-    X509_EXTENSION_free(ext);
-
-    if (!TEST_ptr(ext = X509V3_EXT_conf(NULL, &ctx, "authorityKeyIdentifier",
-                      "none"))
-        || !TEST_ptr(X509v3_add_ext(&exts, ext, -1)))
-        goto err;
-
-    if (!TEST_int_eq(X509_REQ_add_extensions(x, exts), 1)
-        || !TEST_int_eq(sk_X509_EXTENSION_num(exts), 0))
+    /* Add non-empty SKID, CSRs have no issuer, so no AKID */
+    if (!TEST_ptr(bio = BIO_new(BIO_s_mem()))
+        || !TEST_int_ge(BIO_printf(bio, "subjectKeyIdentifier = hash\n"), 0)
+        || !TEST_ptr(conf = NCONF_new(NULL))
+        || !TEST_int_gt(NCONF_load_bio(conf, bio, NULL), 0))
         goto err;
-    sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
-
-    if (!TEST_ptr(exts = X509_REQ_get_extensions(x))
-        || !TEST_int_eq(sk_X509_EXTENSION_num(exts), 0))
+    (void)BIO_reset(bio);
+
+    X509V3_set_ctx(&ctx, NULL, NULL, x, NULL, X509V3_CTX_REPLACE);
+    X509V3_set_nconf(&ctx, conf);
+    if (!TEST_true(X509V3_EXT_REQ_add_nconf(conf, &ctx, "default", x))
+        || !TEST_int_eq(X509_REQ_get_attr_count(x), 1)
+        || !TEST_ptr(exts = X509_REQ_get_extensions(x))
+        || !TEST_int_eq(sk_X509_EXTENSION_num(exts), 1))
         goto err;
     sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
     exts = NULL;
 
-    if (!TEST_int_gt(X509_REQ_sign(x, privkey, signmd), 0))
+    /* Request an "empty" SKID in order to drop the previous SKID */
+    NCONF_free(conf);
+    if (!TEST_ptr(conf = NCONF_new(NULL))
+        || !TEST_int_ge(BIO_printf(bio, "subjectKeyIdentifier = none\n"), 0)
+        || !TEST_int_gt(NCONF_load_bio(conf, bio, NULL), 0))
         goto err;
 
-    if (!TEST_ptr(exts = X509_REQ_get_extensions(x))
-        || !TEST_int_eq(sk_X509_EXTENSION_num(exts), 0))
+    X509V3_set_nconf(&ctx, conf);
+    if (!TEST_true(X509V3_EXT_REQ_add_nconf(conf, &ctx, "default", x))
+        || !TEST_int_gt(X509_REQ_sign(x, privkey, signmd), 0)
+        || !TEST_int_eq(X509_REQ_get_attr_count(x), 0))
         goto err;
 
     ret = 1;
+
 err:
+    BIO_free(bio);
+    NCONF_free(conf);
     X509_NAME_ENTRY_free(name_entry);
     X509_NAME_free(subject);
     X509_EXTENSION_free(ext);