Commit da8f09846b for openssl.org
commit da8f09846b98c5a518d5634c50f6835c5bc4be95
Author: Theo Buehler <tb@openbsd.org>
Date: Thu Dec 11 08:38:53 2025 -0700
Add ASN1_BIT_STRING_get_length()
From tb@openbsd.org with tests adapted by beck for OpenSSL.
Fixes: https://github.com/openssl/openssl/issues/29184
Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
Reviewed-by: Neil Horman <nhorman@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/29387)
diff --git a/crypto/asn1/a_bitstr.c b/crypto/asn1/a_bitstr.c
index 223efc2bb1..1d58c9cbbf 100644
--- a/crypto/asn1/a_bitstr.c
+++ b/crypto/asn1/a_bitstr.c
@@ -220,3 +220,36 @@ int ASN1_BIT_STRING_check(const ASN1_BIT_STRING *a,
}
return ok;
}
+
+int ASN1_BIT_STRING_get_length(const ASN1_BIT_STRING *abs, size_t *out_length,
+ int *out_unused_bits)
+{
+ size_t length;
+ int unused_bits;
+
+ if (abs == NULL || abs->type != V_ASN1_BIT_STRING)
+ return 0;
+
+ if (out_length == NULL || out_unused_bits == NULL)
+ return 0;
+
+ length = abs->length;
+ unused_bits = 0;
+
+ if ((abs->flags & ASN1_STRING_FLAG_BITS_LEFT) != 0)
+ unused_bits = abs->flags & 0x07;
+
+ if (length == 0 && unused_bits != 0)
+ return 0;
+
+ if (unused_bits != 0) {
+ unsigned char mask = (1 << unused_bits) - 1;
+ if ((abs->data[length - 1] & mask) != 0)
+ return 0;
+ }
+
+ *out_length = length;
+ *out_unused_bits = unused_bits;
+
+ return 1;
+}
diff --git a/doc/build.info b/doc/build.info
index fdaa35df76..d53aed8df8 100644
--- a/doc/build.info
+++ b/doc/build.info
@@ -479,6 +479,10 @@ DEPEND[html/man3/ADMISSIONS.html]=man3/ADMISSIONS.pod
GENERATE[html/man3/ADMISSIONS.html]=man3/ADMISSIONS.pod
DEPEND[man/man3/ADMISSIONS.3]=man3/ADMISSIONS.pod
GENERATE[man/man3/ADMISSIONS.3]=man3/ADMISSIONS.pod
+DEPEND[html/man3/ASN1_BIT_STRING_get_length.html]=man3/ASN1_BIT_STRING_get_length.pod
+GENERATE[html/man3/ASN1_BIT_STRING_get_length.html]=man3/ASN1_BIT_STRING_get_length.pod
+DEPEND[man/man3/ASN1_BIT_STRING_get_length.3]=man3/ASN1_BIT_STRING_get_length.pod
+GENERATE[man/man3/ASN1_BIT_STRING_get_length.3]=man3/ASN1_BIT_STRING_get_length.pod
DEPEND[html/man3/ASN1_EXTERN_FUNCS.html]=man3/ASN1_EXTERN_FUNCS.pod
GENERATE[html/man3/ASN1_EXTERN_FUNCS.html]=man3/ASN1_EXTERN_FUNCS.pod
DEPEND[man/man3/ASN1_EXTERN_FUNCS.3]=man3/ASN1_EXTERN_FUNCS.pod
@@ -3165,6 +3169,7 @@ DEPEND[man/man3/s2i_ASN1_IA5STRING.3]=man3/s2i_ASN1_IA5STRING.pod
GENERATE[man/man3/s2i_ASN1_IA5STRING.3]=man3/s2i_ASN1_IA5STRING.pod
IMAGEDOCS[man3]=
HTMLDOCS[man3]=html/man3/ADMISSIONS.html \
+html/man3/ASN1_BIT_STRING_get_length.html \
html/man3/ASN1_EXTERN_FUNCS.html \
html/man3/ASN1_INTEGER_get_int64.html \
html/man3/ASN1_INTEGER_new.html \
@@ -3837,6 +3842,7 @@ html/man3/i2d_re_X509_tbs.html \
html/man3/o2i_SCT_LIST.html \
html/man3/s2i_ASN1_IA5STRING.html
MANDOCS[man3]=man/man3/ADMISSIONS.3 \
+man/man3/ASN1_BIT_STRING_get_length.3 \
man/man3/ASN1_EXTERN_FUNCS.3 \
man/man3/ASN1_INTEGER_get_int64.3 \
man/man3/ASN1_INTEGER_new.3 \
diff --git a/doc/man3/ASN1_BIT_STRING_get_length.pod b/doc/man3/ASN1_BIT_STRING_get_length.pod
new file mode 100644
index 0000000000..79497f41ec
--- /dev/null
+++ b/doc/man3/ASN1_BIT_STRING_get_length.pod
@@ -0,0 +1,35 @@
+=pod
+
+=head1 NAME
+
+ASN1_BIT_STRING_get_length - ASN1_BIT_STRING accessors
+
+=head1 SYNOPSIS
+
+ #include <openssl/asn1.h>
+
+ int ASN1_BIT_STRING_get_length(const ASN1_BIT_STRING *bitstr, size_t *length, int *unused_bits);
+
+=head1 DESCRIPTION
+
+ASN1_BIT_STRING_get_length() returns the number of octets in I<bitstr>
+containing bit values in I<length> and the number of unused bits in
+the last octet in I<unused_bits>. The value returned in
+I<unused_bits> is guaranteed to be between 0 and 7, inclusive.
+
+=head1 RETURN VALUES
+
+ASN1_BIT_STRING_get_length() returns 1 on success or 0 if the encoding
+of I<bitstr> is internally inconsistent, or if one of I<bitstr>,
+I<length>, or I<unused_bits> is NULL.
+
+=head1 COPYRIGHT
+
+Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+
+Licensed under the Apache License 2.0 (the "License"). You may not use
+this file except in compliance with the License. You can obtain a copy
+in the file LICENSE in the source distribution or at
+L<https://www.openssl.org/source/license.html>.
+
+=cut
diff --git a/include/openssl/asn1.h.in b/include/openssl/asn1.h.in
index 72c20f76d7..1b7dd66d68 100644
--- a/include/openssl/asn1.h.in
+++ b/include/openssl/asn1.h.in
@@ -586,6 +586,8 @@ int ASN1_BIT_STRING_name_print(BIO *out, ASN1_BIT_STRING *bs,
int ASN1_BIT_STRING_num_asc(const char *name, BIT_STRING_BITNAME *tbl);
int ASN1_BIT_STRING_set_asc(ASN1_BIT_STRING *bs, const char *name, int value,
BIT_STRING_BITNAME *tbl);
+int ASN1_BIT_STRING_get_length(const ASN1_BIT_STRING *abs, size_t *length,
+ int *unused_bits);
/* clang-format off */
{-
diff --git a/test/asn1_string_test.c b/test/asn1_string_test.c
new file mode 100644
index 0000000000..7428597621
--- /dev/null
+++ b/test/asn1_string_test.c
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+/* ASN1_STRING tests */
+
+#include <stdio.h>
+
+#include <openssl/asn1.h>
+#include "testutil.h"
+
+struct abs_get_length_test {
+ const char *descr;
+ int valid;
+ const unsigned char der[20];
+ int der_len;
+ size_t length;
+ int unused_bits;
+};
+
+static const struct abs_get_length_test abs_get_length_tests[] = {
+ {
+ .descr = "zero bits",
+ .valid = 1,
+ .der = {
+ 0x03,
+ 0x01,
+ 0x00,
+ },
+ .der_len = 3,
+ .length = 0,
+ .unused_bits = 0,
+ },
+ {
+ .descr = "zero bits one unused",
+ .valid = 0,
+ .der = {
+ 0x03,
+ 0x01,
+ 0x01,
+ },
+ .der_len = 3,
+ },
+ {
+ .descr = "single zero bit",
+ .valid = 1,
+ .der = {
+ 0x03,
+ 0x02,
+ 0x07,
+ 0x00,
+ },
+ .der_len = 4,
+ .length = 1,
+ .unused_bits = 7,
+ },
+ {
+ .descr = "single one bit",
+ .valid = 1,
+ .der = {
+ 0x03,
+ 0x02,
+ 0x07,
+ 0x80,
+ },
+ .der_len = 4,
+ .length = 1,
+ .unused_bits = 7,
+ },
+ {
+ /* XXX - the library pretends this is 03 02 07 80 */
+ .descr = "invalid: single one bit, seventh bit set",
+ .valid = 1,
+ .der = {
+ 0x03,
+ 0x02,
+ 0x07,
+ 0xc0,
+ },
+ .der_len = 4,
+ .length = 1,
+ .unused_bits = 7,
+ },
+ {
+ .descr = "x.690, primitive encoding in example 8.6.4.2",
+ .valid = 1,
+ .der = {
+ 0x03,
+ 0x07,
+ 0x04,
+ 0x0A,
+ 0x3b,
+ 0x5F,
+ 0x29,
+ 0x1c,
+ 0xd0,
+ },
+ .der_len = 9,
+ .length = 6,
+ .unused_bits = 4,
+ },
+ {
+ /*
+ * XXX - the library thinks it "decodes" this but gets it
+ * quite wrong. Looks like it uses the unused bits of the
+ * first component, and the unused bits octet 04 of the
+ * second component somehow becomes part of the value.
+ */
+ .descr = "x.690, constructed encoding in example 8.6.4.2",
+ .valid = 1,
+ .der = {
+ 0x23,
+ 0x80,
+ 0x03,
+ 0x03,
+ 0x00,
+ 0x0A,
+ 0x3b,
+ 0x03,
+ 0x05,
+ 0x04,
+ 0x5F,
+ 0x29,
+ 0x1c,
+ 0xd0,
+ 0x00,
+ 0x00,
+ },
+ .der_len = 16,
+ .length = 7, /* XXX - should be 6. */
+ .unused_bits = 0, /* XXX - should be 4. */
+ },
+ {
+ .descr = "RFC 3779, 2.1.1, IPv4 address 10.5.0.4",
+ .valid = 1,
+ .der = {
+ 0x03,
+ 0x05,
+ 0x00,
+ 0x0a,
+ 0x05,
+ 0x00,
+ 0x04,
+ },
+ .der_len = 7,
+ .length = 4,
+ .unused_bits = 0,
+ },
+ {
+ .descr = "RFC 3779, 2.1.1, IPv4 prefix 10.5.0/23",
+ .valid = 1,
+ .der = {
+ 0x03,
+ 0x04,
+ 0x01,
+ 0x0a,
+ 0x05,
+ 0x00,
+ },
+ .der_len = 6,
+ .length = 3,
+ .unused_bits = 1,
+ },
+ {
+ .descr = "RFC 3779, 2.1.1, IPv6 address 2001:0:200:3::1",
+ .valid = 1,
+ .der = {
+ 0x03,
+ 0x11,
+ 0x00,
+ 0x20,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x02,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x03,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ },
+ .der_len = 19,
+ .length = 16,
+ .unused_bits = 0,
+ },
+ {
+ .descr = "RFC 3779, 2.1.1, IPv6 prefix 2001:0:200/39",
+ .valid = 1,
+ .der = {
+ 0x03,
+ 0x06,
+ 0x01,
+ 0x20,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x02,
+ },
+ .der_len = 8,
+ .length = 5,
+ .unused_bits = 1,
+ },
+};
+
+static int
+abs_get_length_test(const struct abs_get_length_test *tbl, int idx)
+{
+ const struct abs_get_length_test *test = &tbl[idx];
+ ASN1_BIT_STRING *abs = NULL;
+ const unsigned char *p;
+ int unused_bits, ret;
+ size_t length;
+ int success = 0;
+
+ p = test->der;
+ if (!TEST_ptr(abs = d2i_ASN1_BIT_STRING(NULL, &p, test->der_len))) {
+ TEST_info("%s, (idx=%d) - d2i_ASN1_BIT_STRING faled", __func__, idx);
+ goto err;
+ }
+
+ ret = ASN1_BIT_STRING_get_length(abs, &length, &unused_bits);
+ if (!TEST_int_eq(test->valid, ret)) {
+ TEST_info("%s (idx=%d): %s ASN1_BIT_STRING_get_length want %d, got %d\n",
+ __func__, idx, test->descr, test->valid, ret);
+ goto err;
+ }
+ if (!test->valid)
+ goto done;
+
+ if (!TEST_size_t_eq(length, test->length)
+ || !TEST_int_eq(unused_bits, test->unused_bits)) {
+ TEST_info("%s: (idx=%d) %s: want (%zu, %d), got (%zu, %d)\n", __func__,
+ idx, test->descr, test->length, test->unused_bits, length,
+ unused_bits);
+ goto err;
+ }
+
+done:
+ success = 1;
+
+err:
+ ASN1_STRING_free(abs);
+
+ return success;
+}
+
+static int
+asn1_bit_string_get_length_test(int idx)
+{
+ return abs_get_length_test(abs_get_length_tests, idx);
+}
+
+int setup_tests(void)
+{
+ ADD_ALL_TESTS(asn1_bit_string_get_length_test, OSSL_NELEM(abs_get_length_tests));
+ return 1;
+}
diff --git a/test/build.info b/test/build.info
index e5990247d8..6890544f21 100644
--- a/test/build.info
+++ b/test/build.info
@@ -1096,6 +1096,11 @@ IF[{- !$disabled{tests} -}]
INCLUDE[asn1_time_test]=../include ../apps/include
DEPEND[asn1_time_test]=../libcrypto libtestutil.a
+ PROGRAMS{noinst}=asn1_string_test
+ SOURCE[asn1_string_test]=asn1_string_test.c
+ INCLUDE[asn1_string_test]=../include ../apps/include
+ DEPEND[asn1_string_test]=../libcrypto libtestutil.a
+
# We disable this test completely in a shared build because it deliberately
# redefines some internal libssl symbols. This doesn't work in a non-shared
# build
diff --git a/test/recipes/90-test_asn1_string.t b/test/recipes/90-test_asn1_string.t
new file mode 100644
index 0000000000..ce0db28a18
--- /dev/null
+++ b/test/recipes/90-test_asn1_string.t
@@ -0,0 +1,12 @@
+#! /usr/bin/env perl
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+
+use OpenSSL::Test::Simple;
+
+simple_test("test_asn1_string", "asn1_string_test");
diff --git a/util/libcrypto.num b/util/libcrypto.num
index 16a60ba98c..c1b1367028 100644
--- a/util/libcrypto.num
+++ b/util/libcrypto.num
@@ -2614,6 +2614,7 @@ ASN1_BIT_STRING_set ? 4_0_0 EXIST::FUNCTION:
ASN1_BIT_STRING_set_bit ? 4_0_0 EXIST::FUNCTION:
ASN1_BIT_STRING_get_bit ? 4_0_0 EXIST::FUNCTION:
ASN1_BIT_STRING_check ? 4_0_0 EXIST::FUNCTION:
+ASN1_BIT_STRING_get_length ? 4_0_0 EXIST::FUNCTION:
ASN1_BIT_STRING_name_print ? 4_0_0 EXIST::FUNCTION:
ASN1_BIT_STRING_num_asc ? 4_0_0 EXIST::FUNCTION:
ASN1_BIT_STRING_set_asc ? 4_0_0 EXIST::FUNCTION: