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