ICE Candidate supports fingerprint

Implements the TLS candidate fingerprint extension from
draft-martinsen-ice-tls-candidates-00. TLS ICE candidates carry a
certificate fingerprint as a candidate extension (fingerprint
<hash-func>;<RFC4572-digest>).

ICE-TLS RFC Draft:
https://datatracker.ietf.org/doc/html/draft-martinsen-ice-tls-candidates-00

Bug: webrtc:459337369
Change-Id: Id211a0bb5b69ab4056744f41979a777faf0c4b0a
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/487601
Reviewed-by: Danil Chapovalov <danilchap@webrtc.org>
Reviewed-by: Björn Terelius <terelius@webrtc.org>
Commit-Queue: Danil Chapovalov <danilchap@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#48166}
diff --git a/api/BUILD.gn b/api/BUILD.gn
index 0433e60..9240a34 100644
--- a/api/BUILD.gn
+++ b/api/BUILD.gn
@@ -198,6 +198,7 @@
     "../rtc_base:net_helper",
     "../rtc_base:network_constants",
     "../rtc_base:socket_address",
+    "../rtc_base:ssl",
     "../rtc_base:stringutils",
     "../rtc_base/system:rtc_export",
     "//third_party/abseil-cpp/absl/base:core_headers",
diff --git a/api/DEPS b/api/DEPS
index 026002c..78d4ff8 100644
--- a/api/DEPS
+++ b/api/DEPS
@@ -84,6 +84,7 @@
   "candidate\\.h": [
     "+rtc_base/network_constants.h",
     "+rtc_base/socket_address.h",
+    "+rtc_base/ssl_fingerprint.h",
   ],
 
   "create_peerconnection_factory\\.h": [
diff --git a/api/candidate.cc b/api/candidate.cc
index e3e0a28..5075b89 100644
--- a/api/candidate.cc
+++ b/api/candidate.cc
@@ -15,8 +15,10 @@
 #include <cstdint>
 #include <optional>
 #include <string>
+#include <utility>
 #include <vector>
 
+#include "absl/strings/ascii.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "api/rtc_error.h"
@@ -28,6 +30,7 @@
 #include "rtc_base/net_helper.h"
 #include "rtc_base/network_constants.h"
 #include "rtc_base/socket_address.h"
+#include "rtc_base/ssl_fingerprint.h"
 #include "rtc_base/string_encode.h"
 #include "rtc_base/strings/string_builder.h"
 
@@ -43,9 +46,11 @@
 constexpr char kAttributeCandidateNetworkId[] = "network-id";
 constexpr char kAttributeCandidateNetworkCost[] = "network-cost";
 constexpr char kAttributeCandidatePwd[] = "pwd";
+constexpr char kAttributeFingerprint[] = "fingerprint";
 
 constexpr absl::string_view kSdpDelimiterColon = ":";
 constexpr char kSdpDelimiterColonChar = kSdpDelimiterColon[0];
+constexpr char kSdpDelimiterSemicolonChar = ';';
 constexpr char kSdpDelimiterSpaceChar = ' ';
 constexpr char kSdpDelimiterEqualChar = '=';
 constexpr char kNewLineChar = '\n';
@@ -96,7 +101,8 @@
   // Note that we allow the tcptype to be missing, for backwards
   // compatibility; the implementation treats this as a passive candidate.
   // TODO(bugs.webrtc.org/11466): Treat a missing tcptype as an error?
-  if (candidate.protocol() == TCP_PROTOCOL_NAME &&
+  if ((candidate.protocol() == TCP_PROTOCOL_NAME ||
+       candidate.protocol() == TLS_PROTOCOL_NAME) &&
       !candidate.tcptype().empty()) {
     os << kTcpCandidateType << " " << candidate.tcptype() << " ";
   }
@@ -113,6 +119,11 @@
     os << " " << kAttributeCandidateNetworkCost << " "
        << candidate.network_cost();
   }
+  if (candidate.fingerprint().has_value()) {
+    os << " " << kAttributeFingerprint << " "
+       << candidate.fingerprint()->algorithm << kSdpDelimiterSemicolonChar
+       << candidate.fingerprint()->GetRfc4572Fingerprint();
+  }
 
   return os.str();
 }
@@ -196,6 +207,7 @@
       break;
     case PROTO_TCP:
     case PROTO_SSLTCP:
+    case PROTO_TLS:
       tcp_protocol = true;
       break;
     default:
@@ -268,6 +280,7 @@
   uint32_t generation = 0;
   uint16_t network_id = 0;
   uint16_t network_cost = 0;
+  std::optional<SSLFingerprint> fingerprint;
   for (size_t i = current_position; i + 1 < fields.size(); ++i) {
     // RFC 5245
     // *(SP extension-att-name SP extension-att-value)
@@ -293,15 +306,38 @@
             absl::StrCat("Invalid ", kAttributeCandidateNetworkCost));
       }
       network_cost = std::min(network_cost, kNetworkCostMax);
+    } else if (fields[i] == kAttributeFingerprint) {
+      absl::string_view fp_data = fields[++i];
+      // Fingerprint extension format: algorithm;digest
+      std::vector<absl::string_view> fp_fields =
+          split(fp_data, kSdpDelimiterSemicolonChar);
+      if (fp_fields.size() != 2) {
+        return RTCError(RTCErrorType::SYNTAX_ERROR,
+                        absl::StrCat("Invalid ", kAttributeFingerprint));
+      }
+
+      fingerprint = SSLFingerprint::CreateOptionalFromRfc4572(
+          absl::AsciiStrToLower(fp_fields[0]), fp_fields[1]);
+      if (!fingerprint.has_value()) {
+        return RTCError(RTCErrorType::SYNTAX_ERROR,
+                        "Failed to create fingerprint from the digest.");
+      }
     } else {
       // Skip the unknown extension.
       ++i;
     }
   }
 
+  if (*protocol == PROTO_TLS && !fingerprint.has_value()) {
+    return RTCError(RTCErrorType::SYNTAX_ERROR,
+                    absl::StrCat("Missing ", kAttributeFingerprint,
+                                 " extension for TLS candidate"));
+  }
+
   Candidate candidate(component_id, ProtoToString(*protocol), address, priority,
                       username, password, candidate_type, generation,
-                      foundation, network_id, network_cost);
+                      foundation, network_id, network_cost,
+                      std::move(fingerprint));
   candidate.set_related_address(related_address);
   candidate.set_tcptype(tcptype);
   return candidate;
@@ -353,17 +389,19 @@
       network_cost_(0),
       network_slice_(NetworkSlice::NO_SLICE) {}
 
-Candidate::Candidate(int component,
-                     absl::string_view protocol,
-                     const SocketAddress& address,
-                     uint32_t priority,
-                     absl::string_view username,
-                     absl::string_view password,
-                     IceCandidateType type,
-                     uint32_t generation,
-                     absl::string_view foundation,
-                     uint16_t network_id /*= 0*/,
-                     uint16_t network_cost /*= 0*/)
+Candidate::Candidate(
+    int component,
+    absl::string_view protocol,
+    const SocketAddress& address,
+    uint32_t priority,
+    absl::string_view username,
+    absl::string_view password,
+    IceCandidateType type,
+    uint32_t generation,
+    absl::string_view foundation,
+    uint16_t network_id /*= 0*/,
+    uint16_t network_cost /*= 0*/,
+    std::optional<SSLFingerprint> fingerprint /*= std::nullopt*/)
     : id_(CreateRandomString(8)),
       component_(component),
       protocol_(protocol),
@@ -378,11 +416,8 @@
       foundation_(foundation),
       network_id_(network_id),
       network_cost_(network_cost),
-      network_slice_(NetworkSlice::NO_SLICE) {}
-
-Candidate::Candidate(const Candidate&) = default;
-
-Candidate::~Candidate() = default;
+      network_slice_(NetworkSlice::NO_SLICE),
+      fingerprint_(std::move(fingerprint)) {}
 
 void Candidate::generate_id() {
   id_ = CreateRandomString(8);
@@ -414,7 +449,7 @@
          (password_ == c.password_) && (type_ == c.type_) &&
          (generation_ == c.generation_) && (foundation_ == c.foundation_) &&
          (related_address_ == c.related_address_) &&
-         (network_id_ == c.network_id_);
+         (network_id_ == c.network_id_) && (fingerprint_ == c.fingerprint_);
 }
 
 bool Candidate::MatchesForRemoval(const Candidate& c) const {
@@ -491,7 +526,8 @@
          network_type_ == o.network_type_ && generation_ == o.generation_ &&
          foundation_ == o.foundation_ &&
          related_address_ == o.related_address_ && tcptype_ == o.tcptype_ &&
-         network_id_ == o.network_id_ && network_slice_ == o.network_slice_;
+         network_id_ == o.network_id_ && network_slice_ == o.network_slice_ &&
+         fingerprint_ == o.fingerprint_;
 }
 
 bool Candidate::operator!=(const Candidate& o) const {
diff --git a/api/candidate.h b/api/candidate.h
index 4a8f3a3..d3531d9 100644
--- a/api/candidate.h
+++ b/api/candidate.h
@@ -16,12 +16,14 @@
 
 #include <optional>
 #include <string>
+#include <utility>
 
 #include "absl/strings/string_view.h"
 #include "api/rtc_error.h"
 #include "rtc_base/checks.h"
 #include "rtc_base/network_constants.h"
 #include "rtc_base/socket_address.h"
+#include "rtc_base/ssl_fingerprint.h"
 #include "rtc_base/system/rtc_export.h"
 
 namespace webrtc {
@@ -55,9 +57,13 @@
             uint32_t generation,
             absl::string_view foundation,
             uint16_t network_id = 0,
-            uint16_t network_cost = 0);
-  Candidate(const Candidate&);
-  ~Candidate();
+            uint16_t network_cost = 0,
+            std::optional<SSLFingerprint> fingerprint = std::nullopt);
+  Candidate(const Candidate&) = default;
+  Candidate(Candidate&&) = default;
+  Candidate& operator=(const Candidate&) = default;
+  Candidate& operator=(Candidate&&) = default;
+  ~Candidate() = default;
 
   // Parses the `candidate-attribute` as described in:
   // https://www.rfc-editor.org/rfc/rfc5245#section-15.1
@@ -207,6 +213,13 @@
     network_slice_ = network_slice;
   }
 
+  const std::optional<SSLFingerprint>& fingerprint() const {
+    return fingerprint_;
+  }
+  void set_fingerprint(std::optional<SSLFingerprint> fingerprint) {
+    fingerprint_ = std::move(fingerprint);
+  }
+
   // Determines whether this candidate is equivalent to the given one.
   bool IsEquivalent(const Candidate& c) const;
 
@@ -293,6 +306,7 @@
   uint16_t network_cost_;
   std::string url_;
   NetworkSlice network_slice_;
+  std::optional<SSLFingerprint> fingerprint_;
 };
 
 }  //  namespace webrtc
diff --git a/api/candidate_unittest.cc b/api/candidate_unittest.cc
index 9dd6b0a..3d28fa2 100644
--- a/api/candidate_unittest.cc
+++ b/api/candidate_unittest.cc
@@ -13,16 +13,22 @@
 #include <cstdint>
 #include <optional>
 #include <string>
+#include <utility>
 
 #include "absl/strings/string_view.h"
 #include "api/rtc_error.h"
 #include "p2p/base/p2p_constants.h"
 #include "rtc_base/network_constants.h"
 #include "rtc_base/socket_address.h"
+#include "rtc_base/ssl_fingerprint.h"
+#include "test/gmock.h"
 #include "test/gtest.h"
 
 namespace webrtc {
 namespace {
+
+using ::testing::SizeIs;
+
 constexpr absl::string_view kRawCandidate =
     "candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host generation 2";
 constexpr absl::string_view kRawHostnameCandidate =
@@ -155,6 +161,24 @@
   EXPECT_EQ(candidate.ToCandidateAttribute(true), kSdpTcpActiveCandidate);
 }
 
+TEST(CandidateTest, ToCandidateAttributeTlsCandidates) {
+  const uint8_t kDigest[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+                             0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+                             0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+                             0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};
+  Candidate candidate(
+      ICE_CANDIDATE_COMPONENT_RTP, "tls", SocketAddress("192.168.1.5", 443),
+      kCandidatePriority, "", "", IceCandidateType::kHost, kCandidateGeneration,
+      kCandidateFoundation1, 0, 0, SSLFingerprint("sha-256", kDigest));
+  candidate.set_tcptype(TCPTYPE_PASSIVE_STR);
+  EXPECT_EQ(
+      candidate.ToCandidateAttribute(true),
+      "candidate:a0+B/1 1 tls 2130706432 192.168.1.5 443 typ host tcptype "
+      "passive generation 2 fingerprint sha-256;"
+      "01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F:10:"
+      "11:12:13:14:15:16:17:18:19:1A:1B:1C:1D:1E:1F:20");
+}
+
 TEST(CandidateTest, TypeToString) {
   EXPECT_EQ(IceCandidateTypeToString(IceCandidateType::kHost), "host");
   EXPECT_EQ(IceCandidateTypeToString(IceCandidateType::kSrflx), "srflx");
@@ -326,4 +350,312 @@
   }
 }
 
+TEST(CandidateTest, FingerprintIsAbsentByDefault) {
+  Candidate c;
+  EXPECT_FALSE(c.fingerprint().has_value());
+}
+
+TEST(CandidateTest, SetAndGetFingerprint) {
+  const uint8_t kDigest[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11,
+                             0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99,
+                             0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11,
+                             0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99};
+  Candidate c;
+  c.set_fingerprint(SSLFingerprint("sha-256", kDigest));
+  ASSERT_TRUE(c.fingerprint().has_value());
+  EXPECT_EQ(c.fingerprint()->algorithm, "sha-256");
+  EXPECT_THAT(c.fingerprint()->digest, SizeIs(32));
+}
+
+TEST(CandidateTest, CopyConstructorDeepCopiesFingerprint) {
+  const uint8_t kDigest[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+                             0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+                             0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+                             0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};
+  Candidate original;
+  original.set_fingerprint(SSLFingerprint("sha-256", kDigest));
+  Candidate duplicate(original);
+
+  ASSERT_TRUE(original.fingerprint().has_value());
+  ASSERT_TRUE(duplicate.fingerprint().has_value());
+  EXPECT_EQ(*duplicate.fingerprint(), *original.fingerprint());
+}
+
+TEST(CandidateTest, AssignmentOperatorDeepCopiesFingerprint) {
+  const uint8_t kDigest[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04,
+                             0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
+                             0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
+                             0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C};
+  Candidate original;
+  original.set_fingerprint(SSLFingerprint("sha-256", kDigest));
+
+  Candidate assigned;
+  EXPECT_FALSE(assigned.fingerprint().has_value());
+  assigned = original;
+
+  ASSERT_TRUE(original.fingerprint().has_value());
+  ASSERT_TRUE(assigned.fingerprint().has_value());
+  EXPECT_EQ(*assigned.fingerprint(), *original.fingerprint());
+}
+
+TEST(CandidateTest, CopyOfCandidateWithoutFingerprintHasNoFingerprint) {
+  Candidate original;
+  EXPECT_FALSE(original.fingerprint().has_value());
+
+  Candidate duplicate(original);
+  EXPECT_FALSE(duplicate.fingerprint().has_value());
+
+  Candidate assigned;
+  assigned = original;
+  EXPECT_FALSE(assigned.fingerprint().has_value());
+}
+
+TEST(CandidateTest, ParseTlsCandidateWithFingerprint) {
+  // TLS candidate with fingerprint extension in "algorithm;digest" format.
+  constexpr char kTlsCandidate[] =
+      "candidate:a0+B/1 1 tls 2130706432 192.168.1.5 443 typ host "
+      "generation 2 "
+      "fingerprint sha-256;"
+      "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:"
+      "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99";
+  RTCErrorOr<Candidate> ret = Candidate::ParseCandidateString(kTlsCandidate);
+  ASSERT_TRUE(ret.ok()) << ret.error().message();
+  Candidate c = ret.MoveValue();
+
+  EXPECT_EQ(c.protocol(), "tls");
+  EXPECT_EQ(c.address().ToString(), "192.168.1.5:443");
+  ASSERT_TRUE(c.fingerprint().has_value());
+  EXPECT_EQ(c.fingerprint()->algorithm, "sha-256");
+  EXPECT_THAT(c.fingerprint()->digest, SizeIs(32));
+}
+
+TEST(CandidateTest, ParseTlsCandidateWithoutFingerprint) {
+  constexpr char kTlsCandidate[] =
+      "candidate:a0+B/1 1 tls 2130706432 192.168.1.5 443 typ host "
+      "generation 2";
+  RTCErrorOr<Candidate> ret = Candidate::ParseCandidateString(kTlsCandidate);
+  ASSERT_FALSE(ret.ok());
+  EXPECT_EQ(ret.error().type(), RTCErrorType::SYNTAX_ERROR);
+  EXPECT_STREQ(ret.error().message(),
+               "Missing fingerprint extension for TLS candidate");
+}
+
+TEST(CandidateTest, ParseTlsCandidateWithInvalidFingerprintFormat) {
+  constexpr char kTlsCandidate[] =
+      "candidate:a0+B/1 1 tls 2130706432 192.168.1.5 443 typ host "
+      "generation 2 fingerprint sha-256";
+  RTCErrorOr<Candidate> ret = Candidate::ParseCandidateString(kTlsCandidate);
+  ASSERT_FALSE(ret.ok());
+  EXPECT_EQ(ret.error().type(), RTCErrorType::SYNTAX_ERROR);
+  EXPECT_STREQ(ret.error().message(), "Invalid fingerprint");
+}
+
+TEST(CandidateTest, ParseTlsCandidateWithInvalidFingerprintAlgorithm) {
+  constexpr char kTlsCandidate[] =
+      "candidate:a0+B/1 1 tls 2130706432 192.168.1.5 443 typ host "
+      "generation 2 "
+      "fingerprint md5;"
+      "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:"
+      "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99";
+  RTCErrorOr<Candidate> ret = Candidate::ParseCandidateString(kTlsCandidate);
+  ASSERT_FALSE(ret.ok());
+  EXPECT_EQ(ret.error().type(), RTCErrorType::SYNTAX_ERROR);
+  EXPECT_STREQ(ret.error().message(),
+               "Failed to create fingerprint from the digest.");
+}
+
+TEST(CandidateTest, ParseTlsCandidateWithUppercaseFingerprintAlgorithm) {
+  constexpr char kTlsCandidate[] =
+      "candidate:a0+B/1 1 tls 2130706432 192.168.1.5 443 typ host "
+      "generation 2 "
+      "fingerprint SHA-256;"
+      "01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F:10:"
+      "11:12:13:14:15:16:17:18:19:1A:1B:1C:1D:1E:1F:20";
+  RTCErrorOr<Candidate> ret = Candidate::ParseCandidateString(kTlsCandidate);
+  ASSERT_TRUE(ret.ok()) << ret.error().message();
+  Candidate c = ret.MoveValue();
+
+  ASSERT_TRUE(c.fingerprint().has_value());
+  EXPECT_EQ(c.fingerprint()->algorithm, "sha-256");
+}
+
+TEST(CandidateTest, ParseCandidateWithFingerprintRoundTrip) {
+  // Parse a TLS candidate with fingerprint, serialize it, and parse again
+  // to verify the fingerprint survives a full round trip.
+  constexpr char kTlsCandidate[] =
+      "candidate:a0+B/1 1 tls 2130706432 192.168.1.5 443 typ host "
+      "generation 2 "
+      "fingerprint sha-256;"
+      "01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F:10:"
+      "11:12:13:14:15:16:17:18:19:1A:1B:1C:1D:1E:1F:20";
+  RTCErrorOr<Candidate> ret = Candidate::ParseCandidateString(kTlsCandidate);
+  ASSERT_TRUE(ret.ok());
+  Candidate original = ret.MoveValue();
+
+  std::string serialized =
+      original.ToCandidateAttribute(/*include_ufrag=*/false);
+  RTCErrorOr<Candidate> reparsed = Candidate::ParseCandidateString(serialized);
+  ASSERT_TRUE(reparsed.ok()) << reparsed.error().message();
+  Candidate result = reparsed.MoveValue();
+
+  ASSERT_TRUE(original.fingerprint().has_value());
+  ASSERT_TRUE(result.fingerprint().has_value());
+  EXPECT_EQ(result.fingerprint()->algorithm, "sha-256");
+  EXPECT_EQ(*result.fingerprint(), *original.fingerprint());
+}
+
+TEST(CandidateTest, ConstructorWithFingerprint) {
+  const uint8_t kDigest[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+                             0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+                             0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+                             0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};
+  auto fp = SSLFingerprint("sha-256", kDigest);
+  SocketAddress address("192.168.1.5", 443);
+  Candidate c(ICE_CANDIDATE_COMPONENT_RTP, "tls", address, kCandidatePriority,
+              "", "", IceCandidateType::kHost, kCandidateGeneration,
+              kCandidateFoundation1, 0, 0, std::move(fp));
+  ASSERT_TRUE(c.fingerprint().has_value());
+  EXPECT_EQ(c.fingerprint()->algorithm, "sha-256");
+  EXPECT_THAT(c.fingerprint()->digest, SizeIs(32));
+}
+
+TEST(CandidateTest, EqualityBothFingerprintsNull) {
+  Candidate a;
+  a.set_address(SocketAddress("1.2.3.4", 1234));
+  Candidate b(a);
+  EXPECT_FALSE(a.fingerprint().has_value());
+  EXPECT_FALSE(b.fingerprint().has_value());
+  EXPECT_EQ(a, b);
+}
+
+TEST(CandidateTest, EqualityBothFingerprintsSameValue) {
+  const uint8_t kDigest[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+                             0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+                             0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+                             0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};
+  Candidate a;
+  a.set_address(SocketAddress("1.2.3.4", 1234));
+  a.set_fingerprint(SSLFingerprint("sha-256", kDigest));
+  Candidate b(a);
+  EXPECT_EQ(a, b);
+}
+
+TEST(CandidateTest, EqualityDifferentFingerprints) {
+  const uint8_t kDigest1[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+                              0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+                              0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+                              0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};
+  const uint8_t kDigest2[] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8,
+                              0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2, 0xF1, 0xF0,
+                              0xEF, 0xEE, 0xED, 0xEC, 0xEB, 0xEA, 0xE9, 0xE8,
+                              0xE7, 0xE6, 0xE5, 0xE4, 0xE3, 0xE2, 0xE1, 0xE0};
+  Candidate a;
+  a.set_address(SocketAddress("1.2.3.4", 1234));
+  a.set_fingerprint(SSLFingerprint("sha-256", kDigest1));
+  Candidate b(a);
+  b.set_fingerprint(SSLFingerprint("sha-256", kDigest2));
+  EXPECT_NE(a, b);
+}
+
+TEST(CandidateTest, EqualityOneFingerprintNull) {
+  const uint8_t kDigest[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+                             0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+                             0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+                             0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};
+  Candidate a;
+  a.set_address(SocketAddress("1.2.3.4", 1234));
+  Candidate b(a);
+  b.set_fingerprint(SSLFingerprint("sha-256", kDigest));
+  EXPECT_NE(a, b);
+  EXPECT_NE(b, a);
+}
+
+TEST(CandidateTest, IsEquivalentSameFingerprint) {
+  const uint8_t kDigest[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+                             0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+                             0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+                             0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};
+  Candidate a(ICE_CANDIDATE_COMPONENT_RTP, "tls",
+              SocketAddress("192.168.1.5", 443), kCandidatePriority, "", "",
+              IceCandidateType::kHost, kCandidateGeneration,
+              kCandidateFoundation1, 0, 0, SSLFingerprint("sha-256", kDigest));
+  Candidate b(a);
+  EXPECT_TRUE(a.IsEquivalent(b));
+}
+
+TEST(CandidateTest, WithDifferentFingerprintsAreNotEquivalent) {
+  const uint8_t kDigest1[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+                              0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+                              0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+                              0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};
+  const uint8_t kDigest2[] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8,
+                              0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2, 0xF1, 0xF0,
+                              0xEF, 0xEE, 0xED, 0xEC, 0xEB, 0xEA, 0xE9, 0xE8,
+                              0xE7, 0xE6, 0xE5, 0xE4, 0xE3, 0xE2, 0xE1, 0xE0};
+  Candidate a(ICE_CANDIDATE_COMPONENT_RTP, "tls",
+              SocketAddress("192.168.1.5", 443), kCandidatePriority, "", "",
+              IceCandidateType::kHost, kCandidateGeneration,
+              kCandidateFoundation1, 0, 0, SSLFingerprint("sha-256", kDigest1));
+  Candidate b(a);
+  b.set_fingerprint(SSLFingerprint("sha-256", kDigest2));
+  EXPECT_FALSE(a.IsEquivalent(b));
+}
+
+TEST(CandidateTest, IsEquivalentOneFingerprintNull) {
+  const uint8_t kDigest[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+                             0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+                             0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+                             0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20};
+  Candidate a(ICE_CANDIDATE_COMPONENT_RTP, "tls",
+              SocketAddress("192.168.1.5", 443), kCandidatePriority, "", "",
+              IceCandidateType::kHost, kCandidateGeneration,
+              kCandidateFoundation1);
+  Candidate b(a);
+  b.set_fingerprint(SSLFingerprint("sha-256", kDigest));
+  EXPECT_FALSE(a.IsEquivalent(b));
+}
+
+TEST(CandidateTest, SerializationRoundTripWithFingerprint) {
+  const uint8_t kDigest[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11,
+                             0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99,
+                             0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11,
+                             0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99};
+  Candidate original(ICE_CANDIDATE_COMPONENT_RTP, "tls",
+                     SocketAddress("192.168.1.5", 443), kCandidatePriority,
+                     "user", "pass", IceCandidateType::kHost,
+                     kCandidateGeneration, kCandidateFoundation1, 0, 0,
+                     SSLFingerprint("sha-256", kDigest));
+  original.set_tcptype("passive");
+
+  // Serialize to candidate attribute string and parse back.
+  std::string serialized =
+      original.ToCandidateAttribute(/*include_ufrag=*/true);
+  RTCErrorOr<Candidate> parsed = Candidate::ParseCandidateString(serialized);
+  ASSERT_TRUE(parsed.ok()) << parsed.error().message();
+  Candidate result = parsed.MoveValue();
+
+  EXPECT_EQ(result.protocol(), "tls");
+  EXPECT_EQ(result.tcptype(), TCPTYPE_PASSIVE_STR);
+  ASSERT_TRUE(original.fingerprint().has_value());
+  ASSERT_TRUE(result.fingerprint().has_value());
+  EXPECT_EQ(result.fingerprint()->algorithm, "sha-256");
+  EXPECT_EQ(*result.fingerprint(), *original.fingerprint());
+}
+
+TEST(CandidateTest, SerializeTlsCandidateWithoutFingerprintFailsOnParse) {
+  Candidate original(ICE_CANDIDATE_COMPONENT_RTP, "tls",
+                     SocketAddress("192.168.1.5", 443), kCandidatePriority,
+                     "user", "pass", IceCandidateType::kHost,
+                     kCandidateGeneration, kCandidateFoundation1);
+  original.set_tcptype("passive");
+
+  std::string serialized =
+      original.ToCandidateAttribute(/*include_ufrag=*/true);
+  RTCErrorOr<Candidate> parsed = Candidate::ParseCandidateString(serialized);
+  ASSERT_FALSE(parsed.ok());
+  EXPECT_EQ(parsed.error().type(), RTCErrorType::SYNTAX_ERROR);
+  EXPECT_STREQ(parsed.error().message(),
+               "Missing fingerprint extension for TLS candidate");
+}
+
 }  // namespace webrtc
diff --git a/rtc_base/ssl_fingerprint.cc b/rtc_base/ssl_fingerprint.cc
index 54daa1b..94889f7 100644
--- a/rtc_base/ssl_fingerprint.cc
+++ b/rtc_base/ssl_fingerprint.cc
@@ -14,8 +14,10 @@
 #include <cstddef>
 #include <cstdint>
 #include <memory>
+#include <optional>
 #include <span>
 #include <string>
+#include <utility>
 
 #include "absl/algorithm/container.h"
 #include "absl/strings/string_view.h"
@@ -66,6 +68,15 @@
       algorithm, AsUint8Span(std::span(value, value_len)));
 }
 
+std::optional<SSLFingerprint> SSLFingerprint::CreateOptionalFromRfc4572(
+    absl::string_view algorithm,
+    absl::string_view fingerprint) {
+  if (auto parsed = CreateFromRfc4572(algorithm, fingerprint)) {
+    return std::move(*parsed);
+  }
+  return std::nullopt;
+}
+
 std::unique_ptr<SSLFingerprint> SSLFingerprint::CreateFromCertificate(
     const RTCCertificate& cert) {
   std::string digest_alg;
diff --git a/rtc_base/ssl_fingerprint.h b/rtc_base/ssl_fingerprint.h
index e51580b..bfd50ed 100644
--- a/rtc_base/ssl_fingerprint.h
+++ b/rtc_base/ssl_fingerprint.h
@@ -15,6 +15,7 @@
 #include <stdint.h>
 
 #include <memory>
+#include <optional>
 #include <span>
 #include <string>
 
@@ -42,6 +43,10 @@
       absl::string_view algorithm,
       absl::string_view fingerprint);
 
+  static std::optional<SSLFingerprint> CreateOptionalFromRfc4572(
+      absl::string_view algorithm,
+      absl::string_view fingerprint);
+
   // Creates a fingerprint from a certificate, using the same digest algorithm
   // as the certificate's signature.
   static absl_nullable std::unique_ptr<SSLFingerprint> CreateFromCertificate(