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(