Implement Subject parsing in CertificateView.

This is meant to eventually replace QuicCertUtils::ExtractSubjectNameFromDERCert().

PiperOrigin-RevId: 339947896
Change-Id: I24e9955165976f5d8d3d1645c764b3ac32608def
diff --git a/quic/core/crypto/certificate_view.cc b/quic/core/crypto/certificate_view.cc
index 58cfe19..6042b9e 100644
--- a/quic/core/crypto/certificate_view.cc
+++ b/quic/core/crypto/certificate_view.cc
@@ -9,7 +9,9 @@
 #include <memory>
 #include <string>
 
+#include "absl/strings/escaping.h"
 #include "absl/strings/match.h"
+#include "absl/strings/str_join.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/optional.h"
 #include "third_party/boringssl/src/include/openssl/base.h"
@@ -32,6 +34,7 @@
 #include "net/third_party/quiche/src/common/platform/api/quiche_time_utils.h"
 #include "net/third_party/quiche/src/common/quiche_data_reader.h"
 
+namespace quic {
 namespace {
 
 using ::quiche::QuicheTextUtils;
@@ -100,9 +103,78 @@
   }
 }
 
+std::string AttributeNameToString(const CBS& oid_cbs) {
+  absl::string_view oid = CbsToStringPiece(oid_cbs);
+
+  // We only handle OIDs of form 2.5.4.N, which have binary encoding of
+  // "55 04 0N".
+  if (oid.length() == 3 && absl::StartsWith(oid, "\x55\x04")) {
+    // clang-format off
+    switch (oid[2]) {
+      case '\x3': return "CN";
+      case '\x7': return "L";
+      case '\x8': return "ST";
+      case '\xa': return "O";
+      case '\xb': return "OU";
+      case '\x6': return "C";
+    }
+    // clang-format on
+  }
+
+  bssl::UniquePtr<char> oid_representation(CBS_asn1_oid_to_text(&oid_cbs));
+  if (oid_representation == nullptr) {
+    return quiche::QuicheStrCat("(", absl::BytesToHexString(oid), ")");
+  }
+  return std::string(oid_representation.get());
+}
+
 }  // namespace
 
-namespace quic {
+absl::optional<std::string> X509NameAttributeToString(CBS input) {
+  CBS name, value;
+  unsigned value_tag;
+  if (!CBS_get_asn1(&input, &name, CBS_ASN1_OBJECT) ||
+      !CBS_get_any_asn1(&input, &value, &value_tag) || CBS_len(&input) != 0) {
+    return absl::nullopt;
+  }
+  // Note that this does not process encoding of |input| in any way.  This works
+  // fine for the most cases.
+  return quiche::QuicheStrCat(AttributeNameToString(name), "=",
+                              absl::CHexEscape(CbsToStringPiece(value)));
+}
+
+namespace {
+
+template <unsigned inner_tag,
+          char separator,
+          absl::optional<std::string> (*parser)(CBS)>
+absl::optional<std::string> ParseAndJoin(CBS input) {
+  std::vector<std::string> pieces;
+  while (CBS_len(&input) != 0) {
+    CBS attribute;
+    if (!CBS_get_asn1(&input, &attribute, inner_tag)) {
+      return absl::nullopt;
+    }
+    absl::optional<std::string> formatted = parser(attribute);
+    if (!formatted.has_value()) {
+      return absl::nullopt;
+    }
+    pieces.push_back(*formatted);
+  }
+
+  return absl::StrJoin(pieces, std::string({separator}));
+}
+
+absl::optional<std::string> RelativeDistinguishedNameToString(CBS input) {
+  return ParseAndJoin<CBS_ASN1_SEQUENCE, '+', X509NameAttributeToString>(input);
+}
+
+absl::optional<std::string> DistinguishedNameToString(CBS input) {
+  return ParseAndJoin<CBS_ASN1_SET, ',', RelativeDistinguishedNameToString>(
+      input);
+}
+
+}  // namespace
 
 absl::optional<quic::QuicWallTime> ParseDerTime(unsigned tag,
                                                 absl::string_view payload) {
@@ -257,6 +329,8 @@
     return nullptr;
   }
 
+  result->subject_der_ = CbsToStringPiece(subject);
+
   unsigned not_before_tag, not_after_tag;
   CBS not_before, not_after;
   if (!CBS_get_any_asn1(&validity, &not_before, &not_before_tag) ||
@@ -446,6 +520,11 @@
       data.size());
 }
 
+absl::optional<std::string> CertificateView::GetHumanReadableSubject() const {
+  CBS input = StringPieceToCbs(subject_der_);
+  return DistinguishedNameToString(input);
+}
+
 std::unique_ptr<CertificatePrivateKey> CertificatePrivateKey::LoadFromDer(
     absl::string_view private_key) {
   std::unique_ptr<CertificatePrivateKey> result(new CertificatePrivateKey());
diff --git a/quic/core/crypto/certificate_view.h b/quic/core/crypto/certificate_view.h
index 8ec292b..f2e99ba 100644
--- a/quic/core/crypto/certificate_view.h
+++ b/quic/core/crypto/certificate_view.h
@@ -60,6 +60,10 @@
     return subject_alt_name_ips_;
   }
 
+  // Returns a human-readable representation of the Subject field.  The format
+  // is similar to RFC 2253, but does not match it exactly.
+  absl::optional<std::string> GetHumanReadableSubject() const;
+
   // |signature_algorithm| is a TLS signature algorithm ID.
   bool VerifySignature(absl::string_view data,
                        absl::string_view signature,
@@ -70,6 +74,7 @@
 
   QuicWallTime validity_start_ = QuicWallTime::Zero();
   QuicWallTime validity_end_ = QuicWallTime::Zero();
+  absl::string_view subject_der_;
 
   // Public key parsed from SPKI.
   bssl::UniquePtr<EVP_PKEY> public_key_;
@@ -118,6 +123,9 @@
   bssl::UniquePtr<EVP_PKEY> private_key_;
 };
 
+// Parses a DER-encoded X.509 NameAttribute.  Exposed primarily for testing.
+absl::optional<std::string> X509NameAttributeToString(CBS input);
+
 // Parses a DER time based on the specified ASN.1 tag.  Exposed primarily for
 // testing.
 QUIC_EXPORT_PRIVATE absl::optional<quic::QuicWallTime> ParseDerTime(
diff --git a/quic/core/crypto/certificate_view_der_fuzzer.cc b/quic/core/crypto/certificate_view_der_fuzzer.cc
index e2de1ba..e66f4f9 100644
--- a/quic/core/crypto/certificate_view_der_fuzzer.cc
+++ b/quic/core/crypto/certificate_view_der_fuzzer.cc
@@ -9,7 +9,11 @@
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
   std::string input(reinterpret_cast<const char*>(data), size);
 
-  quic::CertificateView::ParseSingleCertificate(input);
+  std::unique_ptr<quic::CertificateView> view =
+      quic::CertificateView::ParseSingleCertificate(input);
+  if (view != nullptr) {
+    view->GetHumanReadableSubject();
+  }
   quic::CertificatePrivateKey::LoadFromDer(input);
   return 0;
 }
diff --git a/quic/core/crypto/certificate_view_test.cc b/quic/core/crypto/certificate_view_test.cc
index 7f8b60c..46e30f1 100644
--- a/quic/core/crypto/certificate_view_test.cc
+++ b/quic/core/crypto/certificate_view_test.cc
@@ -7,11 +7,13 @@
 #include <memory>
 #include <sstream>
 
+#include "absl/strings/escaping.h"
 #include "absl/strings/string_view.h"
 #include "third_party/boringssl/src/include/openssl/base.h"
 #include "third_party/boringssl/src/include/openssl/bytestring.h"
 #include "third_party/boringssl/src/include/openssl/evp.h"
 #include "third_party/boringssl/src/include/openssl/ssl.h"
+#include "net/third_party/quiche/src/quic/core/crypto/boring_utils.h"
 #include "net/third_party/quiche/src/quic/core/quic_time.h"
 #include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
 #include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
@@ -56,6 +58,9 @@
   const QuicWallTime validity_end = QuicWallTime::FromUNIXSeconds(
       *quiche::QuicheUtcDateTimeToUnixSeconds(2020, 2, 2, 18, 13, 59));
   EXPECT_EQ(view->validity_end(), validity_end);
+
+  EXPECT_EQ("C=US,ST=California,L=Mountain View,O=QUIC Server,CN=127.0.0.1",
+            view->GetHumanReadableSubject());
 }
 
 TEST(CertificateViewTest, ParseCertWithUnknownSanType) {
@@ -180,6 +185,28 @@
             absl::nullopt);
 }
 
+TEST(CertificateViewTest, NameAttribute) {
+  // OBJECT_IDENTIFIER { 1.2.840.113554.4.1.112411 }
+  // UTF8String { "Test" }
+  std::string unknown_oid =
+      absl::HexStringToBytes("060b2a864886f712040186ee1b0c0454657374");
+  EXPECT_EQ("1.2.840.113554.4.1.112411=Test",
+            X509NameAttributeToString(StringPieceToCbs(unknown_oid)));
+
+  // OBJECT_IDENTIFIER { 2.5.4.3 }
+  // UTF8String { "Bell: \x07" }
+  std::string non_printable =
+      absl::HexStringToBytes("06035504030c0742656c6c3a2007");
+  EXPECT_EQ(R"(CN=Bell: \x07)",
+            X509NameAttributeToString(StringPieceToCbs(non_printable)));
+
+  // OBJECT_IDENTIFIER { "\x55\x80" }
+  // UTF8String { "Test" }
+  std::string invalid_oid = absl::HexStringToBytes("060255800c0454657374");
+  EXPECT_EQ("(5580)=Test",
+            X509NameAttributeToString(StringPieceToCbs(invalid_oid)));
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic