Add some tests for X509_NAME_ENTRY management.
The X509_NAME representation is a little odd because it flattens the
RDN sequence, but maintains RDN indices (X509_NAME_ENTRY_set) to recover
the information. add_entry and delete_entry both have logic to maintain
invariants. Test that they work.
Change-Id: I7d4a033db0a82a1f13888bbfca29076b589cc214
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/53325
Reviewed-by: Bob Beck <bbe@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/crypto/x509/x509_test.cc b/crypto/x509/x509_test.cc
index 50121c9..8aee2d5 100644
--- a/crypto/x509/x509_test.cc
+++ b/crypto/x509/x509_test.cc
@@ -4548,3 +4548,119 @@
X509V3_ADD_DELETE));
expect_extensions({{NID_subject_key_identifier, true, skid2_der}});
}
+
+TEST(X509Test, NameEntry) {
+ bssl::UniquePtr<X509_NAME> name(X509_NAME_new());
+ ASSERT_TRUE(name);
+
+ auto check_name = [&](const char *expected_rfc2253) {
+ // Check RDN indices are self-consistent.
+ int num = X509_NAME_entry_count(name.get());
+ if (num > 0) {
+ // RDN indices must start at zero.
+ EXPECT_EQ(0, X509_NAME_ENTRY_set(X509_NAME_get_entry(name.get(), 0)));
+ }
+ for (int i = 1; i < num; i++) {
+ int prev = X509_NAME_ENTRY_set(X509_NAME_get_entry(name.get(), i - 1));
+ int current = X509_NAME_ENTRY_set(X509_NAME_get_entry(name.get(), i));
+ // RDN indices must increase consecutively.
+ EXPECT_TRUE(prev == current || prev + 1 == current)
+ << "Entry " << i << " has RDN index " << current
+ << " which is inconsistent with previous index " << prev;
+ }
+
+ // Check the name based on the RFC 2253 serialization. Note the RFC 2253
+ // serialization is in reverse.
+ bssl::UniquePtr<BIO> bio(BIO_new(BIO_s_mem()));
+ ASSERT_TRUE(bio);
+ EXPECT_GE(X509_NAME_print_ex(bio.get(), name.get(), 0, XN_FLAG_RFC2253), 0);
+ const uint8_t *data;
+ size_t len;
+ ASSERT_TRUE(BIO_mem_contents(bio.get(), &data, &len));
+ EXPECT_EQ(expected_rfc2253, std::string(data, data + len));
+ };
+
+ check_name("");
+
+ // |loc| = -1, |set| = 0 appends as new RDNs.
+ ASSERT_TRUE(X509_NAME_add_entry_by_NID(
+ name.get(), NID_organizationName, MBSTRING_UTF8,
+ reinterpret_cast<const unsigned char *>("Org"), /*len=*/-1, /*loc=*/-1,
+ /*set=*/0));
+ check_name("O=Org");
+
+ // |loc| = -1, |set| = 0 appends as new RDNs.
+ ASSERT_TRUE(X509_NAME_add_entry_by_NID(
+ name.get(), NID_commonName, MBSTRING_UTF8,
+ reinterpret_cast<const unsigned char *>("Name"), /*len=*/-1, /*loc=*/-1,
+ /*set=*/0));
+ check_name("CN=Name,O=Org");
+
+ // Inserting in the middle of the set, but with |set| = 0 inserts a new RDN
+ // and fixes the "set" values as needed.
+ ASSERT_TRUE(X509_NAME_add_entry_by_NID(
+ name.get(), NID_organizationalUnitName, MBSTRING_UTF8,
+ reinterpret_cast<const unsigned char *>("Unit"), /*len=*/-1, /*loc=*/1,
+ /*set=*/0));
+ check_name("CN=Name,OU=Unit,O=Org");
+
+ // |set = -1| adds to the previous entry's RDN. (Although putting O and OU at
+ // the same level makes little sense, the test is written this way to check
+ // the function isn't using attribute types to order things.)
+ ASSERT_TRUE(X509_NAME_add_entry_by_NID(
+ name.get(), NID_organizationName, MBSTRING_UTF8,
+ reinterpret_cast<const unsigned char *>("Org2"), /*len=*/-1, /*loc=*/2,
+ /*set=*/-1));
+ check_name("CN=Name,O=Org2+OU=Unit,O=Org");
+
+ // |set| = 1 adds to the next entry's RDN.
+ ASSERT_TRUE(X509_NAME_add_entry_by_NID(
+ name.get(), NID_commonName, MBSTRING_UTF8,
+ reinterpret_cast<const unsigned char *>("Name2"), /*len=*/-1, /*loc=*/2,
+ /*set=*/-1));
+ check_name("CN=Name,O=Org2+CN=Name2+OU=Unit,O=Org");
+
+ // If there is no previous RDN, |set| = -1 makes a new RDN.
+ ASSERT_TRUE(X509_NAME_add_entry_by_NID(
+ name.get(), NID_countryName, MBSTRING_UTF8,
+ reinterpret_cast<const unsigned char *>("US"), /*len=*/-1, /*loc=*/0,
+ /*set=*/-1));
+ check_name("CN=Name,O=Org2+CN=Name2+OU=Unit,O=Org,C=US");
+
+ // Likewise if there is no next RDN.
+ ASSERT_TRUE(X509_NAME_add_entry_by_NID(
+ name.get(), NID_commonName, MBSTRING_UTF8,
+ reinterpret_cast<const unsigned char *>("Name3"), /*len=*/-1, /*loc=*/-1,
+ /*set=*/1));
+ check_name("CN=Name3,CN=Name,O=Org2+CN=Name2+OU=Unit,O=Org,C=US");
+
+ // If |set| = 0 and we insert in the middle of an existing RDN, it adds an
+ // RDN boundary after the entry but not before. This is a quirk of how the
+ // function is implemented and hopefully not something any caller depends on.
+ ASSERT_TRUE(X509_NAME_add_entry_by_NID(
+ name.get(), NID_commonName, MBSTRING_UTF8,
+ reinterpret_cast<const unsigned char *>("Name4"), /*len=*/-1, /*loc=*/3,
+ /*set=*/0));
+ check_name("CN=Name3,CN=Name,O=Org2+CN=Name2,CN=Name4+OU=Unit,O=Org,C=US");
+
+ // Entries may be deleted.
+ X509_NAME_ENTRY_free(X509_NAME_delete_entry(name.get(), 7));
+ check_name("CN=Name,O=Org2+CN=Name2,CN=Name4+OU=Unit,O=Org,C=US");
+
+ // When deleting the only attribute in an RDN, index invariants should still
+ // hold.
+ X509_NAME_ENTRY_free(X509_NAME_delete_entry(name.get(), 0));
+ check_name("CN=Name,O=Org2+CN=Name2,CN=Name4+OU=Unit,O=Org");
+
+ // Index invariants also hold when deleting attributes from non-singular RDNs.
+ X509_NAME_ENTRY_free(X509_NAME_delete_entry(name.get(), 1));
+ check_name("CN=Name,O=Org2+CN=Name2,CN=Name4,O=Org");
+ X509_NAME_ENTRY_free(X509_NAME_delete_entry(name.get(), 1));
+ check_name("CN=Name,O=Org2+CN=Name2,O=Org");
+
+ // Same as above, but delete the second attribute first.
+ X509_NAME_ENTRY_free(X509_NAME_delete_entry(name.get(), 2));
+ check_name("CN=Name,CN=Name2,O=Org");
+ X509_NAME_ENTRY_free(X509_NAME_delete_entry(name.get(), 1));
+ check_name("CN=Name,O=Org");
+}