diff --git a/api/BUILD.gn b/api/BUILD.gn index 214600e6ac..0352f993f3 100644 --- a/api/BUILD.gn +++ b/api/BUILD.gn @@ -133,6 +133,7 @@ rtc_library("libjingle_peerconnection_api") { sources = [ "candidate.cc", "candidate.h", + "crypto_params.h", "data_channel_interface.cc", "data_channel_interface.h", "dtls_transport_interface.cc", diff --git a/api/crypto_params.h b/api/crypto_params.h new file mode 100644 index 0000000000..5da352cbef --- /dev/null +++ b/api/crypto_params.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2004 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef API_CRYPTO_PARAMS_H_ +#define API_CRYPTO_PARAMS_H_ + +#include + +namespace cricket { + +// Parameters for SRTP negotiation, as described in RFC 4568. +// TODO(benwright) - Rename to SrtpCryptoParams as these only apply to SRTP and +// not generic crypto parameters for WebRTC. +struct CryptoParams { + CryptoParams() : tag(0) {} + CryptoParams(int t, + const std::string& cs, + const std::string& kp, + const std::string& sp) + : tag(t), cipher_suite(cs), key_params(kp), session_params(sp) {} + + bool Matches(const CryptoParams& params) const { + return (tag == params.tag && cipher_suite == params.cipher_suite); + } + + int tag; + std::string cipher_suite; + std::string key_params; + std::string session_params; +}; + +} // namespace cricket + +#endif // API_CRYPTO_PARAMS_H_ diff --git a/p2p/base/transport_description.h b/p2p/base/transport_description.h index 340edc2c58..32fdb5c9b3 100644 --- a/p2p/base/transport_description.h +++ b/p2p/base/transport_description.h @@ -24,6 +24,17 @@ namespace cricket { +// SEC_ENABLED and SEC_REQUIRED should only be used if the session +// was negotiated over TLS, to protect the inline crypto material +// exchange. +// SEC_DISABLED: No crypto in outgoing offer, ignore any supplied crypto. +// SEC_ENABLED: Crypto in outgoing offer and answer (if supplied in offer). +// SEC_REQUIRED: Crypto in outgoing offer and answer. Fail any offer with absent +// or unsupported crypto. +// TODO(deadbeef): Remove this or rename it to something more appropriate, like +// SdesPolicy. +enum SecurePolicy { SEC_DISABLED, SEC_ENABLED, SEC_REQUIRED }; + // Whether our side of the call is driving the negotiation, or the other side. enum IceRole { ICEROLE_CONTROLLING = 0, ICEROLE_CONTROLLED, ICEROLE_UNKNOWN }; diff --git a/p2p/base/transport_description_factory.cc b/p2p/base/transport_description_factory.cc index 5917713003..6beae34333 100644 --- a/p2p/base/transport_description_factory.cc +++ b/p2p/base/transport_description_factory.cc @@ -21,7 +21,8 @@ namespace cricket { -TransportDescriptionFactory::TransportDescriptionFactory() {} +TransportDescriptionFactory::TransportDescriptionFactory() + : secure_(SEC_DISABLED) {} TransportDescriptionFactory::~TransportDescriptionFactory() = default; @@ -46,7 +47,7 @@ std::unique_ptr TransportDescriptionFactory::CreateOffer( } // If we are trying to establish a secure transport, add a fingerprint. - if (IsEncrypted()) { + if (secure_ == SEC_ENABLED || secure_ == SEC_REQUIRED) { // Fail if we can't create the fingerprint. // If we are the initiator set role to "actpass". if (!SetSecurityInfo(desc.get(), CONNECTIONROLE_ACTPASS)) { @@ -89,7 +90,7 @@ std::unique_ptr TransportDescriptionFactory::CreateAnswer( // Negotiate security params. if (offer && offer->identity_fingerprint.get()) { // The offer supports DTLS, so answer with DTLS, as long as we support it. - if (IsEncrypted()) { + if (secure_ == SEC_ENABLED || secure_ == SEC_REQUIRED) { ConnectionRole role = CONNECTIONROLE_NONE; // If the offer does not constrain the role, go with preference. if (offer->connection_role == CONNECTIONROLE_ACTPASS) { @@ -115,7 +116,7 @@ std::unique_ptr TransportDescriptionFactory::CreateAnswer( return NULL; } } - } else if (require_transport_attributes && IsEncrypted()) { + } else if (require_transport_attributes && secure_ == SEC_REQUIRED) { // We require DTLS, but the other side didn't offer it. Fail. RTC_LOG(LS_WARNING) << "Failed to create TransportDescription answer " "because of incompatible security settings"; diff --git a/p2p/base/transport_description_factory.h b/p2p/base/transport_description_factory.h index 3e999a6bce..0be7f32929 100644 --- a/p2p/base/transport_description_factory.h +++ b/p2p/base/transport_description_factory.h @@ -40,20 +40,20 @@ class TransportDescriptionFactory { TransportDescriptionFactory(); ~TransportDescriptionFactory(); + SecurePolicy secure() const { return secure_; } // The certificate to use when setting up DTLS. const rtc::scoped_refptr& certificate() const { return certificate_; } - // Specifies the certificate to use. - // When a certificate is set, transport will be encrypted. + // Specifies the transport security policy to use. + void set_secure(SecurePolicy s) { secure_ = s; } + // Specifies the certificate to use (only used when secure != SEC_DISABLED). void set_certificate( const rtc::scoped_refptr& certificate) { certificate_ = certificate; } - bool IsEncrypted() const { return certificate_ != nullptr; } - // Creates a transport description suitable for use in an offer. std::unique_ptr CreateOffer( const TransportOptions& options, @@ -77,6 +77,7 @@ class TransportDescriptionFactory { bool SetSecurityInfo(TransportDescription* description, ConnectionRole role) const; + SecurePolicy secure_; rtc::scoped_refptr certificate_; }; diff --git a/p2p/base/transport_description_factory_unittest.cc b/p2p/base/transport_description_factory_unittest.cc index 45bd61f350..77a56eff26 100644 --- a/p2p/base/transport_description_factory_unittest.cc +++ b/p2p/base/transport_description_factory_unittest.cc @@ -144,19 +144,17 @@ class TransportDescriptionFactoryTest : public ::testing::Test { } protected: - void SetDtls(bool f1_dtls, bool f2_dtls) { - if (f1_dtls) { + void SetDtls(bool dtls) { + if (dtls) { + f1_.set_secure(cricket::SEC_ENABLED); + f2_.set_secure(cricket::SEC_ENABLED); f1_.set_certificate(cert1_); - } else { - f1_.set_certificate(nullptr); - } - if (f2_dtls) { f2_.set_certificate(cert2_); } else { - f2_.set_certificate(nullptr); + f1_.set_secure(cricket::SEC_DISABLED); + f2_.set_secure(cricket::SEC_DISABLED); } } - void SetDtls(bool dtls) { SetDtls(dtls, dtls); } cricket::IceCredentialsIterator ice_credentials_; TransportDescriptionFactory f1_; @@ -173,19 +171,33 @@ TEST_F(TransportDescriptionFactoryTest, TestOfferDefault) { } TEST_F(TransportDescriptionFactoryTest, TestOfferDtls) { - SetDtls(true); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_certificate(cert1_); std::string digest_alg; ASSERT_TRUE( cert1_->GetSSLCertificate().GetSignatureDigestAlgorithm(&digest_alg)); std::unique_ptr desc = f1_.CreateOffer(TransportOptions(), NULL, &ice_credentials_); CheckDesc(desc.get(), "", "", "", digest_alg); + // Ensure it also works with SEC_REQUIRED. + f1_.set_secure(cricket::SEC_REQUIRED); + desc = f1_.CreateOffer(TransportOptions(), NULL, &ice_credentials_); + CheckDesc(desc.get(), "", "", "", digest_alg); +} + +// Test generating an offer with DTLS fails with no identity. +TEST_F(TransportDescriptionFactoryTest, TestOfferDtlsWithNoIdentity) { + f1_.set_secure(cricket::SEC_ENABLED); + std::unique_ptr desc = + f1_.CreateOffer(TransportOptions(), NULL, &ice_credentials_); + ASSERT_TRUE(desc.get() == NULL); } // Test updating an offer with DTLS to pick ICE. // The ICE credentials should stay the same in the new offer. TEST_F(TransportDescriptionFactoryTest, TestOfferDtlsReofferDtls) { - SetDtls(true); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_certificate(cert1_); std::string digest_alg; ASSERT_TRUE( cert1_->GetSSLCertificate().GetSignatureDigestAlgorithm(&digest_alg)); @@ -225,7 +237,8 @@ TEST_F(TransportDescriptionFactoryTest, TestReanswer) { // Test that we handle answering an offer with DTLS with no DTLS. TEST_F(TransportDescriptionFactoryTest, TestAnswerDtlsToNoDtls) { - SetDtls(true, false); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_certificate(cert1_); std::unique_ptr offer = f1_.CreateOffer(TransportOptions(), NULL, &ice_credentials_); ASSERT_TRUE(offer.get() != NULL); @@ -234,20 +247,31 @@ TEST_F(TransportDescriptionFactoryTest, TestAnswerDtlsToNoDtls) { CheckDesc(desc.get(), "", "", "", ""); } -// Test that we reject answering an offer without DTLS if we have DTLS enabled. +// Test that we handle answering an offer without DTLS if we have DTLS enabled, +// but fail if we require DTLS. TEST_F(TransportDescriptionFactoryTest, TestAnswerNoDtlsToDtls) { - SetDtls(false, true); + f2_.set_secure(cricket::SEC_ENABLED); + f2_.set_certificate(cert2_); std::unique_ptr offer = f1_.CreateOffer(TransportOptions(), NULL, &ice_credentials_); ASSERT_TRUE(offer.get() != NULL); std::unique_ptr desc = f2_.CreateAnswer( offer.get(), TransportOptions(), true, NULL, &ice_credentials_); - ASSERT_FALSE(desc); + CheckDesc(desc.get(), "", "", "", ""); + f2_.set_secure(cricket::SEC_REQUIRED); + desc = f2_.CreateAnswer(offer.get(), TransportOptions(), true, NULL, + &ice_credentials_); + ASSERT_TRUE(desc.get() == NULL); } -// Test that we handle answering an DTLS offer with DTLS. +// Test that we handle answering an DTLS offer with DTLS, both if we have +// DTLS enabled and required. TEST_F(TransportDescriptionFactoryTest, TestAnswerDtlsToDtls) { - SetDtls(true, true); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_certificate(cert1_); + + f2_.set_secure(cricket::SEC_ENABLED); + f2_.set_certificate(cert2_); // f2_ produces the answer that is being checked in this test, so the // answer must contain fingerprint lines with cert2_'s digest algorithm. std::string digest_alg2; @@ -260,6 +284,10 @@ TEST_F(TransportDescriptionFactoryTest, TestAnswerDtlsToDtls) { std::unique_ptr desc = f2_.CreateAnswer( offer.get(), TransportOptions(), true, NULL, &ice_credentials_); CheckDesc(desc.get(), "", "", "", digest_alg2); + f2_.set_secure(cricket::SEC_REQUIRED); + desc = f2_.CreateAnswer(offer.get(), TransportOptions(), true, NULL, + &ice_credentials_); + CheckDesc(desc.get(), "", "", "", digest_alg2); } // Test that ice ufrag and password is changed in an updated offer and answer @@ -326,7 +354,11 @@ TEST_F(TransportDescriptionFactoryTest, CreateAnswerIceCredentialsIterator) { } TEST_F(TransportDescriptionFactoryTest, CreateAnswerToDtlsActpassOffer) { - SetDtls(true); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_certificate(cert1_); + + f2_.set_secure(cricket::SEC_ENABLED); + f2_.set_certificate(cert2_); cricket::TransportOptions options; std::unique_ptr offer = f1_.CreateOffer(options, nullptr, &ice_credentials_); @@ -337,7 +369,11 @@ TEST_F(TransportDescriptionFactoryTest, CreateAnswerToDtlsActpassOffer) { } TEST_F(TransportDescriptionFactoryTest, CreateAnswerToDtlsActiveOffer) { - SetDtls(true); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_certificate(cert1_); + + f2_.set_secure(cricket::SEC_ENABLED); + f2_.set_certificate(cert2_); cricket::TransportOptions options; std::unique_ptr offer = f1_.CreateOffer(options, nullptr, &ice_credentials_); @@ -349,7 +385,11 @@ TEST_F(TransportDescriptionFactoryTest, CreateAnswerToDtlsActiveOffer) { } TEST_F(TransportDescriptionFactoryTest, CreateAnswerToDtlsPassiveOffer) { - SetDtls(true); + f1_.set_secure(cricket::SEC_ENABLED); + f1_.set_certificate(cert1_); + + f2_.set_secure(cricket::SEC_ENABLED); + f2_.set_certificate(cert2_); cricket::TransportOptions options; std::unique_ptr offer = f1_.CreateOffer(options, nullptr, &ice_credentials_); diff --git a/pc/BUILD.gn b/pc/BUILD.gn index 1e8187addf..f31109af33 100644 --- a/pc/BUILD.gn +++ b/pc/BUILD.gn @@ -81,6 +81,8 @@ rtc_library("rtc_pc_base") { "sctp_transport.h", "sctp_utils.cc", "sctp_utils.h", + "srtp_filter.cc", + "srtp_filter.h", "srtp_session.cc", "srtp_session.h", "srtp_transport.cc", @@ -884,6 +886,7 @@ if (rtc_include_tests && !build_with_chromium) { "rtp_transport_unittest.cc", "sctp_transport_unittest.cc", "session_description_unittest.cc", + "srtp_filter_unittest.cc", "srtp_session_unittest.cc", "srtp_transport_unittest.cc", "test/rtp_transport_test_util.h", diff --git a/pc/channel.h b/pc/channel.h index 19e07fa86a..4628c86bd8 100644 --- a/pc/channel.h +++ b/pc/channel.h @@ -47,6 +47,7 @@ #include "pc/rtp_transport.h" #include "pc/rtp_transport_internal.h" #include "pc/session_description.h" +#include "pc/srtp_filter.h" #include "pc/srtp_transport.h" #include "rtc_base/async_packet_socket.h" #include "rtc_base/async_udp_socket.h" diff --git a/pc/dtls_srtp_transport.h b/pc/dtls_srtp_transport.h index 6daaf27e5b..da068c9b8a 100644 --- a/pc/dtls_srtp_transport.h +++ b/pc/dtls_srtp_transport.h @@ -15,6 +15,7 @@ #include #include "absl/types/optional.h" +#include "api/crypto_params.h" #include "api/dtls_transport_interface.h" #include "api/rtc_error.h" #include "p2p/base/dtls_transport_internal.h" @@ -48,6 +49,15 @@ class DtlsSrtpTransport : public SrtpTransport { void SetOnDtlsStateChange(std::function callback); + RTCError SetSrtpSendKey(const cricket::CryptoParams& params) override { + return RTCError(RTCErrorType::UNSUPPORTED_OPERATION, + "Set SRTP keys for DTLS-SRTP is not supported."); + } + RTCError SetSrtpReceiveKey(const cricket::CryptoParams& params) override { + return RTCError(RTCErrorType::UNSUPPORTED_OPERATION, + "Set SRTP keys for DTLS-SRTP is not supported."); + } + // If `active_reset_srtp_params_` is set to be true, the SRTP parameters will // be reset whenever the DtlsTransports are reset. void SetActiveResetSrtpParams(bool active_reset_srtp_params) { diff --git a/pc/jsep_transport.cc b/pc/jsep_transport.cc index e40c7b5f1b..a94af42178 100644 --- a/pc/jsep_transport.cc +++ b/pc/jsep_transport.cc @@ -37,10 +37,12 @@ JsepTransportDescription::JsepTransportDescription() {} JsepTransportDescription::JsepTransportDescription( bool rtcp_mux_enabled, + const std::vector& cryptos, const std::vector& encrypted_header_extension_ids, int rtp_abs_sendtime_extn_id, const TransportDescription& transport_desc) : rtcp_mux_enabled(rtcp_mux_enabled), + cryptos(cryptos), encrypted_header_extension_ids(encrypted_header_extension_ids), rtp_abs_sendtime_extn_id(rtp_abs_sendtime_extn_id), transport_desc(transport_desc) {} @@ -48,6 +50,7 @@ JsepTransportDescription::JsepTransportDescription( JsepTransportDescription::JsepTransportDescription( const JsepTransportDescription& from) : rtcp_mux_enabled(from.rtcp_mux_enabled), + cryptos(from.cryptos), encrypted_header_extension_ids(from.encrypted_header_extension_ids), rtp_abs_sendtime_extn_id(from.rtp_abs_sendtime_extn_id), transport_desc(from.transport_desc) {} @@ -60,6 +63,7 @@ JsepTransportDescription& JsepTransportDescription::operator=( return *this; } rtcp_mux_enabled = from.rtcp_mux_enabled; + cryptos = from.cryptos; encrypted_header_extension_ids = from.encrypted_header_extension_ids; rtp_abs_sendtime_extn_id = from.rtp_abs_sendtime_extn_id; transport_desc = from.transport_desc; @@ -85,6 +89,7 @@ JsepTransport::JsepTransport( ice_transport_(std::move(ice_transport)), rtcp_ice_transport_(std::move(rtcp_ice_transport)), unencrypted_rtp_transport_(std::move(unencrypted_rtp_transport)), + sdes_transport_(std::move(sdes_transport)), dtls_srtp_transport_(std::move(dtls_srtp_transport)), rtp_dtls_transport_(rtp_dtls_transport ? rtc::make_ref_counted( @@ -112,10 +117,15 @@ JsepTransport::JsepTransport( (rtcp_dtls_transport_ != nullptr)); // Verify the "only one out of these three can be set" invariant. if (unencrypted_rtp_transport_) { + RTC_DCHECK(!sdes_transport); + RTC_DCHECK(!dtls_srtp_transport); + } else if (sdes_transport_) { + RTC_DCHECK(!unencrypted_rtp_transport); RTC_DCHECK(!dtls_srtp_transport); } else { RTC_DCHECK(dtls_srtp_transport_); RTC_DCHECK(!unencrypted_rtp_transport); + RTC_DCHECK(!sdes_transport); } if (sctp_transport_) { @@ -162,8 +172,19 @@ webrtc::RTCError JsepTransport::SetLocalJsepTransportDescription( "Failed to setup RTCP mux."); } - if (dtls_srtp_transport_) { + // If doing SDES, setup the SDES crypto parameters. + if (sdes_transport_) { RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!dtls_srtp_transport_); + if (!SetSdes(jsep_description.cryptos, + jsep_description.encrypted_header_extension_ids, type, + ContentSource::CS_LOCAL)) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Failed to setup SDES crypto parameters."); + } + } else if (dtls_srtp_transport_) { + RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!sdes_transport_); dtls_srtp_transport_->UpdateRecvEncryptedHeaderExtensionIds( jsep_description.encrypted_header_extension_ids); } @@ -240,8 +261,21 @@ webrtc::RTCError JsepTransport::SetRemoteJsepTransportDescription( "Failed to setup RTCP mux."); } - if (dtls_srtp_transport_) { + // If doing SDES, setup the SDES crypto parameters. + if (sdes_transport_) { RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!dtls_srtp_transport_); + if (!SetSdes(jsep_description.cryptos, + jsep_description.encrypted_header_extension_ids, type, + ContentSource::CS_REMOTE)) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Failed to setup SDES crypto parameters."); + } + sdes_transport_->CacheRtpAbsSendTimeHeaderExtension( + jsep_description.rtp_abs_sendtime_extn_id); + } else if (dtls_srtp_transport_) { + RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!sdes_transport_); dtls_srtp_transport_->UpdateSendEncryptedHeaderExtensionIds( jsep_description.encrypted_header_extension_ids); dtls_srtp_transport_->CacheRtpAbsSendTimeHeaderExtension( @@ -440,11 +474,17 @@ bool JsepTransport::SetRtcpMux(bool enable, void JsepTransport::ActivateRtcpMux() { if (unencrypted_rtp_transport_) { + RTC_DCHECK(!sdes_transport_); RTC_DCHECK(!dtls_srtp_transport_); unencrypted_rtp_transport_->SetRtcpPacketTransport(nullptr); + } else if (sdes_transport_) { + RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!dtls_srtp_transport_); + sdes_transport_->SetRtcpPacketTransport(nullptr); } else if (dtls_srtp_transport_) { RTC_DCHECK(dtls_srtp_transport_); RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!sdes_transport_); dtls_srtp_transport_->SetDtlsTransports(rtp_dtls_transport(), /*rtcp_dtls_transport=*/nullptr); } @@ -453,6 +493,51 @@ void JsepTransport::ActivateRtcpMux() { rtcp_mux_active_callback_(); } +bool JsepTransport::SetSdes(const std::vector& cryptos, + const std::vector& encrypted_extension_ids, + webrtc::SdpType type, + ContentSource source) { + RTC_DCHECK_RUN_ON(network_thread_); + bool ret = false; + ret = sdes_negotiator_.Process(cryptos, type, source); + if (!ret) { + return ret; + } + + if (source == ContentSource::CS_LOCAL) { + recv_extension_ids_ = encrypted_extension_ids; + } else { + send_extension_ids_ = encrypted_extension_ids; + } + + // If setting an SDES answer succeeded, apply the negotiated parameters + // to the SRTP transport. + if ((type == SdpType::kPrAnswer || type == SdpType::kAnswer) && ret) { + if (sdes_negotiator_.send_cipher_suite() && + sdes_negotiator_.recv_cipher_suite()) { + RTC_DCHECK(send_extension_ids_); + RTC_DCHECK(recv_extension_ids_); + ret = sdes_transport_->SetRtpParams( + *(sdes_negotiator_.send_cipher_suite()), + sdes_negotiator_.send_key().data(), + static_cast(sdes_negotiator_.send_key().size()), + *(send_extension_ids_), *(sdes_negotiator_.recv_cipher_suite()), + sdes_negotiator_.recv_key().data(), + static_cast(sdes_negotiator_.recv_key().size()), + *(recv_extension_ids_)); + } else { + RTC_LOG(LS_INFO) << "No crypto keys are provided for SDES."; + if (type == SdpType::kAnswer) { + // Explicitly reset the `sdes_transport_` if no crypto param is + // provided in the answer. No need to call `ResetParams()` for + // `sdes_negotiator_` because it resets the params inside `SetAnswer`. + sdes_transport_->ResetParams(); + } + } + } + return ret; +} + webrtc::RTCError JsepTransport::NegotiateAndSetDtlsParameters( SdpType local_description_type) { RTC_DCHECK_RUN_ON(network_thread_); diff --git a/pc/jsep_transport.h b/pc/jsep_transport.h index db63aaec3b..e3e929bfd2 100644 --- a/pc/jsep_transport.h +++ b/pc/jsep_transport.h @@ -19,6 +19,7 @@ #include "absl/types/optional.h" #include "api/candidate.h" +#include "api/crypto_params.h" #include "api/ice_transport_interface.h" #include "api/jsep.h" #include "api/rtc_error.h" @@ -39,6 +40,8 @@ #include "pc/rtp_transport_internal.h" #include "pc/sctp_transport.h" #include "pc/session_description.h" +#include "pc/srtp_filter.h" +#include "pc/srtp_transport.h" #include "pc/transport_stats.h" #include "rtc_base/checks.h" #include "rtc_base/constructor_magic.h" @@ -57,6 +60,7 @@ struct JsepTransportDescription { JsepTransportDescription(); JsepTransportDescription( bool rtcp_mux_enabled, + const std::vector& cryptos, const std::vector& encrypted_header_extension_ids, int rtp_abs_sendtime_extn_id, const TransportDescription& transport_description); @@ -66,6 +70,7 @@ struct JsepTransportDescription { JsepTransportDescription& operator=(const JsepTransportDescription& from); bool rtcp_mux_enabled = true; + std::vector cryptos; std::vector encrypted_header_extension_ids; int rtp_abs_sendtime_extn_id = -1; // TODO(zhihuang): Add the ICE and DTLS related variables and methods from @@ -166,6 +171,9 @@ class JsepTransport { if (dtls_srtp_transport_) { return dtls_srtp_transport_.get(); } + if (sdes_transport_) { + return sdes_transport_.get(); + } if (unencrypted_rtp_transport_) { return unencrypted_rtp_transport_.get(); } @@ -236,6 +244,11 @@ class JsepTransport { void ActivateRtcpMux() RTC_RUN_ON(network_thread_); + bool SetSdes(const std::vector& cryptos, + const std::vector& encrypted_extension_ids, + webrtc::SdpType type, + ContentSource source); + // Negotiates and sets the DTLS parameters based on the current local and // remote transport description, such as the DTLS role to use, and whether // DTLS should be activated. @@ -287,6 +300,7 @@ class JsepTransport { // To avoid downcasting and make it type safe, keep three unique pointers for // different SRTP mode and only one of these is non-nullptr. const std::unique_ptr unencrypted_rtp_transport_; + const std::unique_ptr sdes_transport_; const std::unique_ptr dtls_srtp_transport_; const rtc::scoped_refptr rtp_dtls_transport_; @@ -299,6 +313,7 @@ class JsepTransport { sctp_data_channel_transport_; const rtc::scoped_refptr sctp_transport_; + SrtpFilter sdes_negotiator_ RTC_GUARDED_BY(network_thread_); RtcpMuxFilter rtcp_mux_negotiator_ RTC_GUARDED_BY(network_thread_); // Cache the encrypted header extension IDs for SDES negoitation. diff --git a/pc/jsep_transport_controller.cc b/pc/jsep_transport_controller.cc index 24154fd17d..b1fccbe6be 100644 --- a/pc/jsep_transport_controller.cc +++ b/pc/jsep_transport_controller.cc @@ -918,8 +918,8 @@ JsepTransportController::CreateJsepTransportDescription( : content_desc->rtcp_mux(); return cricket::JsepTransportDescription( - rtcp_mux_enabled, encrypted_extension_ids, rtp_abs_sendtime_extn_id, - transport_info.description); + rtcp_mux_enabled, content_desc->cryptos(), encrypted_extension_ids, + rtp_abs_sendtime_extn_id, transport_info.description); } std::vector JsepTransportController::GetEncryptedHeaderExtensionIds( @@ -1017,6 +1017,12 @@ RTCError JsepTransportController::MaybeCreateJsepTransport( if (transport) { return RTCError::OK(); } + const cricket::MediaContentDescription* content_desc = + content_info.media_description(); + if (certificate_ && !content_desc->cryptos().empty()) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "SDES and DTLS-SRTP cannot be enabled at the same time."); + } rtc::scoped_refptr ice = CreateIceTransport(content_info.name, /*rtcp=*/false); @@ -1044,6 +1050,10 @@ RTCError JsepTransportController::MaybeCreateJsepTransport( << "Creating UnencryptedRtpTransport, becayse encryption is disabled."; unencrypted_rtp_transport = CreateUnencryptedRtpTransport( content_info.name, rtp_dtls_transport.get(), rtcp_dtls_transport.get()); + } else if (!content_desc->cryptos().empty()) { + sdes_transport = CreateSdesTransport( + content_info.name, rtp_dtls_transport.get(), rtcp_dtls_transport.get()); + RTC_LOG(LS_INFO) << "Creating SdesTransport."; } else { RTC_LOG(LS_INFO) << "Creating DtlsSrtpTransport."; dtls_srtp_transport = CreateDtlsSrtpTransport( diff --git a/pc/jsep_transport_unittest.cc b/pc/jsep_transport_unittest.cc index d9200d94ad..41c9baba6f 100644 --- a/pc/jsep_transport_unittest.cc +++ b/pc/jsep_transport_unittest.cc @@ -30,6 +30,11 @@ static const char kIceUfrag2[] = "U002"; static const char kIcePwd2[] = "TESTIEPWD00000000000002"; static const char kTransportName[] = "Test Transport"; +enum class SrtpMode { + kSdes, + kDtlsSrtp, +}; + struct NegotiateRoleParams { ConnectionRole local_role; ConnectionRole remote_role; @@ -87,7 +92,8 @@ class JsepTransport2Test : public ::testing::Test, public sigslot::has_slots<> { // Create a new JsepTransport with a FakeDtlsTransport and a // FakeIceTransport. - std::unique_ptr CreateJsepTransport2(bool rtcp_mux_enabled) { + std::unique_ptr CreateJsepTransport2(bool rtcp_mux_enabled, + SrtpMode srtp_mode) { auto ice_internal = std::make_unique( kTransportName, ICE_CANDIDATE_COMPONENT_RTP); auto rtp_dtls_transport = @@ -107,8 +113,19 @@ class JsepTransport2Test : public ::testing::Test, public sigslot::has_slots<> { std::unique_ptr unencrypted_rtp_transport; std::unique_ptr sdes_transport; std::unique_ptr dtls_srtp_transport; - dtls_srtp_transport = CreateDtlsSrtpTransport(rtp_dtls_transport.get(), - rtcp_dtls_transport.get()); + switch (srtp_mode) { + case SrtpMode::kSdes: + sdes_transport = CreateSdesTransport(rtp_dtls_transport.get(), + rtcp_dtls_transport.get()); + sdes_transport_ = sdes_transport.get(); + break; + case SrtpMode::kDtlsSrtp: + dtls_srtp_transport = CreateDtlsSrtpTransport( + rtp_dtls_transport.get(), rtcp_dtls_transport.get()); + break; + default: + RTC_NOTREACHED(); + } auto jsep_transport = std::make_unique( kTransportName, /*local_certificate=*/nullptr, std::move(ice), @@ -167,7 +184,7 @@ class JsepTransport2WithRtcpMux : public JsepTransport2Test, // This test verifies the ICE parameters are properly applied to the transports. TEST_P(JsepTransport2WithRtcpMux, SetIceParameters) { bool rtcp_mux_enabled = GetParam(); - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); JsepTransportDescription jsep_description; jsep_description.transport_desc = TransportDescription(kIceUfrag1, kIcePwd1); @@ -213,7 +230,7 @@ TEST_P(JsepTransport2WithRtcpMux, SetIceParameters) { // Similarly, test DTLS parameters are properly applied to the transports. TEST_P(JsepTransport2WithRtcpMux, SetDtlsParameters) { bool rtcp_mux_enabled = GetParam(); - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); // Create certificates. rtc::scoped_refptr local_cert = @@ -264,7 +281,7 @@ TEST_P(JsepTransport2WithRtcpMux, SetDtlsParameters) { // CONNECTIONROLE_PASSIVE, expecting SSL_CLIENT role. TEST_P(JsepTransport2WithRtcpMux, SetDtlsParametersWithPassiveAnswer) { bool rtcp_mux_enabled = GetParam(); - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); // Create certificates. rtc::scoped_refptr local_cert = @@ -316,7 +333,7 @@ TEST_P(JsepTransport2WithRtcpMux, SetDtlsParametersWithPassiveAnswer) { // only starts returning "false" once an ICE restart has been initiated. TEST_P(JsepTransport2WithRtcpMux, NeedsIceRestart) { bool rtcp_mux_enabled = GetParam(); - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); // Use the same JsepTransportDescription for both offer and answer. JsepTransportDescription description; @@ -361,7 +378,7 @@ TEST_P(JsepTransport2WithRtcpMux, NeedsIceRestart) { TEST_P(JsepTransport2WithRtcpMux, GetStats) { bool rtcp_mux_enabled = GetParam(); - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); size_t expected_stats_size = rtcp_mux_enabled ? 1u : 2u; TransportStats stats; @@ -377,7 +394,7 @@ TEST_P(JsepTransport2WithRtcpMux, GetStats) { // certificate matches the fingerprint. TEST_P(JsepTransport2WithRtcpMux, VerifyCertificateFingerprint) { bool rtcp_mux_enabled = GetParam(); - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); EXPECT_FALSE( jsep_transport_->VerifyCertificateFingerprint(nullptr, nullptr).ok()); @@ -451,7 +468,8 @@ TEST_P(JsepTransport2WithRtcpMux, ValidDtlsRoleNegotiation) { }; for (auto& param : valid_client_params) { - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); jsep_transport_->SetLocalCertificate(certificate); local_description.transport_desc.connection_role = param.local_role; @@ -496,7 +514,8 @@ TEST_P(JsepTransport2WithRtcpMux, ValidDtlsRoleNegotiation) { }; for (auto& param : valid_server_params) { - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); jsep_transport_->SetLocalCertificate(certificate); local_description.transport_desc.connection_role = param.local_role; @@ -567,7 +586,8 @@ TEST_P(JsepTransport2WithRtcpMux, InvalidDtlsRoleNegotiation) { SdpType::kPrAnswer}}; for (auto& param : duplicate_params) { - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); jsep_transport_->SetLocalCertificate(certificate); local_description.transport_desc.connection_role = param.local_role; @@ -617,7 +637,8 @@ TEST_P(JsepTransport2WithRtcpMux, InvalidDtlsRoleNegotiation) { SdpType::kPrAnswer}}; for (auto& param : offerer_without_actpass_params) { - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); jsep_transport_->SetLocalCertificate(certificate); local_description.transport_desc.connection_role = param.local_role; @@ -663,7 +684,7 @@ TEST_F(JsepTransport2Test, ValidDtlsReofferFromAnswerer) { rtc::RTCCertificate::Create( rtc::SSLIdentity::Create("testing", rtc::KT_ECDSA)); bool rtcp_mux_enabled = true; - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); jsep_transport_->SetLocalCertificate(certificate); JsepTransportDescription local_offer = @@ -710,7 +731,7 @@ TEST_F(JsepTransport2Test, InvalidDtlsReofferFromAnswerer) { rtc::RTCCertificate::Create( rtc::SSLIdentity::Create("testing", rtc::KT_ECDSA)); bool rtcp_mux_enabled = true; - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); jsep_transport_->SetLocalCertificate(certificate); JsepTransportDescription local_offer = @@ -756,7 +777,7 @@ TEST_F(JsepTransport2Test, RemoteOfferWithCurrentNegotiatedDtlsRole) { rtc::RTCCertificate::Create( rtc::SSLIdentity::Create("testing", rtc::KT_ECDSA)); bool rtcp_mux_enabled = true; - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); jsep_transport_->SetLocalCertificate(certificate); JsepTransportDescription remote_desc = @@ -801,7 +822,7 @@ TEST_F(JsepTransport2Test, RemoteOfferThatChangesNegotiatedDtlsRole) { rtc::RTCCertificate::Create( rtc::SSLIdentity::Create("testing", rtc::KT_ECDSA)); bool rtcp_mux_enabled = true; - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); jsep_transport_->SetLocalCertificate(certificate); JsepTransportDescription remote_desc = @@ -846,7 +867,7 @@ TEST_F(JsepTransport2Test, DtlsSetupWithLegacyAsAnswerer) { rtc::RTCCertificate::Create( rtc::SSLIdentity::Create("testing", rtc::KT_ECDSA)); bool rtcp_mux_enabled = true; - jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled); + jsep_transport_ = CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); jsep_transport_->SetLocalCertificate(certificate); JsepTransportDescription remote_desc = @@ -878,7 +899,8 @@ TEST_F(JsepTransport2Test, DtlsSetupWithLegacyAsAnswerer) { // Tests that when the RTCP mux is successfully negotiated, the RTCP transport // will be destroyed and the SignalRtpMuxActive will be fired. TEST_F(JsepTransport2Test, RtcpMuxNegotiation) { - jsep_transport_ = CreateJsepTransport2(/*rtcp_mux_enabled=*/false); + jsep_transport_ = + CreateJsepTransport2(/*rtcp_mux_enabled=*/false, SrtpMode::kDtlsSrtp); JsepTransportDescription local_desc; local_desc.rtcp_mux_enabled = true; ASSERT_NE(nullptr, jsep_transport_->rtcp_dtls_transport()); @@ -900,7 +922,8 @@ TEST_F(JsepTransport2Test, RtcpMuxNegotiation) { EXPECT_TRUE(signal_rtcp_mux_active_received_); // The remote side doesn't support RTCP-mux. - jsep_transport_ = CreateJsepTransport2(/*rtcp_mux_enabled=*/false); + jsep_transport_ = + CreateJsepTransport2(/*rtcp_mux_enabled=*/false, SrtpMode::kDtlsSrtp); signal_rtcp_mux_active_received_ = false; remote_desc.rtcp_mux_enabled = false; ASSERT_TRUE( @@ -916,10 +939,87 @@ TEST_F(JsepTransport2Test, RtcpMuxNegotiation) { EXPECT_FALSE(signal_rtcp_mux_active_received_); } +TEST_F(JsepTransport2Test, SdesNegotiation) { + jsep_transport_ = + CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kSdes); + ASSERT_TRUE(sdes_transport_); + EXPECT_FALSE(sdes_transport_->IsSrtpActive()); + + JsepTransportDescription offer_desc; + offer_desc.cryptos.push_back(cricket::CryptoParams( + 1, rtc::kCsAesCm128HmacSha1_32, "inline:" + rtc::CreateRandomString(40), + std::string())); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(offer_desc, SdpType::kOffer) + .ok()); + + JsepTransportDescription answer_desc; + answer_desc.cryptos.push_back(cricket::CryptoParams( + 1, rtc::kCsAesCm128HmacSha1_32, "inline:" + rtc::CreateRandomString(40), + std::string())); + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(answer_desc, SdpType::kAnswer) + .ok()); + EXPECT_TRUE(sdes_transport_->IsSrtpActive()); +} + +TEST_F(JsepTransport2Test, SdesNegotiationWithEmptyCryptosInAnswer) { + jsep_transport_ = + CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kSdes); + ASSERT_TRUE(sdes_transport_); + EXPECT_FALSE(sdes_transport_->IsSrtpActive()); + + JsepTransportDescription offer_desc; + offer_desc.cryptos.push_back(cricket::CryptoParams( + 1, rtc::kCsAesCm128HmacSha1_32, "inline:" + rtc::CreateRandomString(40), + std::string())); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(offer_desc, SdpType::kOffer) + .ok()); + + JsepTransportDescription answer_desc; + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(answer_desc, SdpType::kAnswer) + .ok()); + // SRTP is not active because the crypto parameter is answer is empty. + EXPECT_FALSE(sdes_transport_->IsSrtpActive()); +} + +TEST_F(JsepTransport2Test, SdesNegotiationWithMismatchedCryptos) { + jsep_transport_ = + CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kSdes); + ASSERT_TRUE(sdes_transport_); + EXPECT_FALSE(sdes_transport_->IsSrtpActive()); + + JsepTransportDescription offer_desc; + offer_desc.cryptos.push_back(cricket::CryptoParams( + 1, rtc::kCsAesCm128HmacSha1_32, "inline:" + rtc::CreateRandomString(40), + std::string())); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(offer_desc, SdpType::kOffer) + .ok()); + + JsepTransportDescription answer_desc; + answer_desc.cryptos.push_back(cricket::CryptoParams( + 1, rtc::kCsAesCm128HmacSha1_80, "inline:" + rtc::CreateRandomString(40), + std::string())); + // Expected to fail because the crypto parameters don't match. + ASSERT_FALSE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(answer_desc, SdpType::kAnswer) + .ok()); +} + // Tests that the remote candidates can be added to the transports after both // local and remote descriptions are set. TEST_F(JsepTransport2Test, AddRemoteCandidates) { - jsep_transport_ = CreateJsepTransport2(/*rtcp_mux_enabled=*/true); + jsep_transport_ = + CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kDtlsSrtp); auto fake_ice_transport = static_cast( jsep_transport_->rtp_dtls_transport()->ice_transport()); @@ -943,6 +1043,7 @@ TEST_F(JsepTransport2Test, AddRemoteCandidates) { } enum class Scenario { + kSdes, kDtlsBeforeCallerSendOffer, kDtlsBeforeCallerSetAnswer, kDtlsAfterCallerSetAnswer, @@ -954,9 +1055,9 @@ class JsepTransport2HeaderExtensionTest protected: JsepTransport2HeaderExtensionTest() {} - void CreateJsepTransportPair() { - jsep_transport1_ = CreateJsepTransport2(/*rtcp_mux_enabled=*/true); - jsep_transport2_ = CreateJsepTransport2(/*rtcp_mux_enabled=*/true); + void CreateJsepTransportPair(SrtpMode mode) { + jsep_transport1_ = CreateJsepTransport2(/*rtcp_mux_enabled=*/true, mode); + jsep_transport2_ = CreateJsepTransport2(/*rtcp_mux_enabled=*/true, mode); auto fake_dtls1 = static_cast(jsep_transport1_->rtp_dtls_transport()); @@ -968,12 +1069,14 @@ class JsepTransport2HeaderExtensionTest fake_dtls2->fake_ice_transport()->SignalReadPacket.connect( this, &JsepTransport2HeaderExtensionTest::OnReadPacket2); - auto cert1 = rtc::RTCCertificate::Create( - rtc::SSLIdentity::Create("session1", rtc::KT_DEFAULT)); - jsep_transport1_->rtp_dtls_transport()->SetLocalCertificate(cert1); - auto cert2 = rtc::RTCCertificate::Create( - rtc::SSLIdentity::Create("session1", rtc::KT_DEFAULT)); - jsep_transport2_->rtp_dtls_transport()->SetLocalCertificate(cert2); + if (mode == SrtpMode::kDtlsSrtp) { + auto cert1 = rtc::RTCCertificate::Create( + rtc::SSLIdentity::Create("session1", rtc::KT_DEFAULT)); + jsep_transport1_->rtp_dtls_transport()->SetLocalCertificate(cert1); + auto cert2 = rtc::RTCCertificate::Create( + rtc::SSLIdentity::Create("session1", rtc::KT_DEFAULT)); + jsep_transport2_->rtp_dtls_transport()->SetLocalCertificate(cert2); + } } void OnReadPacket1(rtc::PacketTransportInternal* transport, @@ -1060,10 +1163,17 @@ class JsepTransport2HeaderExtensionTest TEST_P(JsepTransport2HeaderExtensionTest, EncryptedHeaderExtensionNegotiation) { Scenario scenario = std::get<0>(GetParam()); bool use_gcm = std::get<1>(GetParam()); - CreateJsepTransportPair(); + SrtpMode mode = SrtpMode ::kDtlsSrtp; + if (scenario == Scenario::kSdes) { + mode = SrtpMode::kSdes; + } + CreateJsepTransportPair(mode); recv_encrypted_headers1_.push_back(kHeaderExtensionIDs[0]); recv_encrypted_headers2_.push_back(kHeaderExtensionIDs[1]); + cricket::CryptoParams sdes_param(1, rtc::kCsAesCm128HmacSha1_80, + "inline:" + rtc::CreateRandomString(40), + std::string()); if (use_gcm) { auto fake_dtls1 = static_cast(jsep_transport1_->rtp_dtls_transport()); @@ -1080,6 +1190,9 @@ TEST_P(JsepTransport2HeaderExtensionTest, EncryptedHeaderExtensionNegotiation) { JsepTransportDescription offer_desc; offer_desc.encrypted_header_extension_ids = recv_encrypted_headers1_; + if (scenario == Scenario::kSdes) { + offer_desc.cryptos.push_back(sdes_param); + } ASSERT_TRUE( jsep_transport1_ ->SetLocalJsepTransportDescription(offer_desc, SdpType::kOffer) @@ -1091,6 +1204,9 @@ TEST_P(JsepTransport2HeaderExtensionTest, EncryptedHeaderExtensionNegotiation) { JsepTransportDescription answer_desc; answer_desc.encrypted_header_extension_ids = recv_encrypted_headers2_; + if (scenario == Scenario::kSdes) { + answer_desc.cryptos.push_back(sdes_param); + } ASSERT_TRUE( jsep_transport2_ ->SetLocalJsepTransportDescription(answer_desc, SdpType::kAnswer) @@ -1109,7 +1225,8 @@ TEST_P(JsepTransport2HeaderExtensionTest, EncryptedHeaderExtensionNegotiation) { ->SetRemoteJsepTransportDescription(answer_desc, SdpType::kAnswer) .ok()); - if (scenario == Scenario::kDtlsAfterCallerSetAnswer) { + if (scenario == Scenario::kDtlsAfterCallerSetAnswer || + scenario == Scenario::kSdes) { ConnectTransport(); } EXPECT_TRUE(jsep_transport1_->rtp_transport()->IsSrtpActive()); @@ -1148,6 +1265,7 @@ INSTANTIATE_TEST_SUITE_P( JsepTransport2Test, JsepTransport2HeaderExtensionTest, ::testing::Values( + std::make_tuple(Scenario::kSdes, false), std::make_tuple(Scenario::kDtlsBeforeCallerSendOffer, true), std::make_tuple(Scenario::kDtlsBeforeCallerSetAnswer, true), std::make_tuple(Scenario::kDtlsAfterCallerSetAnswer, true), @@ -1157,7 +1275,8 @@ INSTANTIATE_TEST_SUITE_P( // This test verifies the ICE parameters are properly applied to the transports. TEST_F(JsepTransport2Test, SetIceParametersWithRenomination) { - jsep_transport_ = CreateJsepTransport2(/* rtcp_mux_enabled= */ true); + jsep_transport_ = + CreateJsepTransport2(/* rtcp_mux_enabled= */ true, SrtpMode::kDtlsSrtp); JsepTransportDescription jsep_description; jsep_description.transport_desc = TransportDescription(kIceUfrag1, kIcePwd1); diff --git a/pc/media_session.cc b/pc/media_session.cc index 00daacd4b7..c64924a72c 100644 --- a/pc/media_session.cc +++ b/pc/media_session.cc @@ -23,6 +23,7 @@ #include "absl/strings/match.h" #include "absl/strings/string_view.h" #include "absl/types/optional.h" +#include "api/crypto_params.h" #include "api/video_codecs/h264_profile_level_id.h" #include "media/base/codec.h" #include "media/base/media_constants.h" @@ -47,6 +48,19 @@ namespace { using rtc::UniqueRandomIdGenerator; using webrtc::RtpTransceiverDirection; +const char kInline[] = "inline:"; + +void GetSupportedSdesCryptoSuiteNames( + void (*func)(const webrtc::CryptoOptions&, std::vector*), + const webrtc::CryptoOptions& crypto_options, + std::vector* names) { + std::vector crypto_suites; + func(crypto_options, &crypto_suites); + for (const auto crypto : crypto_suites) { + names->push_back(rtc::SrtpCryptoSuiteToName(crypto)); + } +} + webrtc::RtpExtension RtpExtensionFromCapability( const webrtc::RtpHeaderExtensionCapability& capability) { return webrtc::RtpExtension(capability.uri, @@ -121,6 +135,157 @@ static bool IsMediaContentOfType(const ContentInfo* content, return content->media_description()->type() == media_type; } +static bool CreateCryptoParams(int tag, + const std::string& cipher, + CryptoParams* crypto_out) { + int key_len; + int salt_len; + if (!rtc::GetSrtpKeyAndSaltLengths(rtc::SrtpCryptoSuiteFromName(cipher), + &key_len, &salt_len)) { + return false; + } + + int master_key_len = key_len + salt_len; + std::string master_key; + if (!rtc::CreateRandomData(master_key_len, &master_key)) { + return false; + } + + RTC_CHECK_EQ(master_key_len, master_key.size()); + std::string key = rtc::Base64::Encode(master_key); + + crypto_out->tag = tag; + crypto_out->cipher_suite = cipher; + crypto_out->key_params = kInline; + crypto_out->key_params += key; + return true; +} + +static bool AddCryptoParams(const std::string& cipher_suite, + CryptoParamsVec* cryptos_out) { + int size = static_cast(cryptos_out->size()); + + cryptos_out->resize(size + 1); + return CreateCryptoParams(size, cipher_suite, &cryptos_out->at(size)); +} + +void AddMediaCryptos(const CryptoParamsVec& cryptos, + MediaContentDescription* media) { + for (const CryptoParams& crypto : cryptos) { + media->AddCrypto(crypto); + } +} + +bool CreateMediaCryptos(const std::vector& crypto_suites, + MediaContentDescription* media) { + CryptoParamsVec cryptos; + for (const std::string& crypto_suite : crypto_suites) { + if (!AddCryptoParams(crypto_suite, &cryptos)) { + return false; + } + } + AddMediaCryptos(cryptos, media); + return true; +} + +const CryptoParamsVec* GetCryptos(const ContentInfo* content) { + if (!content || !content->media_description()) { + return nullptr; + } + return &content->media_description()->cryptos(); +} + +bool FindMatchingCrypto(const CryptoParamsVec& cryptos, + const CryptoParams& crypto, + CryptoParams* crypto_out) { + auto it = absl::c_find_if( + cryptos, [&crypto](const CryptoParams& c) { return crypto.Matches(c); }); + if (it == cryptos.end()) { + return false; + } + *crypto_out = *it; + return true; +} + +// For audio, HMAC 32 (if enabled) is prefered over HMAC 80 because of the +// low overhead. +void GetSupportedAudioSdesCryptoSuites( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suites) { + if (crypto_options.srtp.enable_aes128_sha1_32_crypto_cipher) { + crypto_suites->push_back(rtc::kSrtpAes128CmSha1_32); + } + crypto_suites->push_back(rtc::kSrtpAes128CmSha1_80); + if (crypto_options.srtp.enable_gcm_crypto_suites) { + crypto_suites->push_back(rtc::kSrtpAeadAes256Gcm); + crypto_suites->push_back(rtc::kSrtpAeadAes128Gcm); + } +} + +void GetSupportedAudioSdesCryptoSuiteNames( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suite_names) { + GetSupportedSdesCryptoSuiteNames(GetSupportedAudioSdesCryptoSuites, + crypto_options, crypto_suite_names); +} + +void GetSupportedVideoSdesCryptoSuites( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suites) { + crypto_suites->push_back(rtc::kSrtpAes128CmSha1_80); + if (crypto_options.srtp.enable_gcm_crypto_suites) { + crypto_suites->push_back(rtc::kSrtpAeadAes256Gcm); + crypto_suites->push_back(rtc::kSrtpAeadAes128Gcm); + } +} + +void GetSupportedVideoSdesCryptoSuiteNames( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suite_names) { + GetSupportedSdesCryptoSuiteNames(GetSupportedVideoSdesCryptoSuites, + crypto_options, crypto_suite_names); +} + +void GetSupportedDataSdesCryptoSuites( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suites) { + crypto_suites->push_back(rtc::kSrtpAes128CmSha1_80); + if (crypto_options.srtp.enable_gcm_crypto_suites) { + crypto_suites->push_back(rtc::kSrtpAeadAes256Gcm); + crypto_suites->push_back(rtc::kSrtpAeadAes128Gcm); + } +} + +void GetSupportedDataSdesCryptoSuiteNames( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suite_names) { + GetSupportedSdesCryptoSuiteNames(GetSupportedDataSdesCryptoSuites, + crypto_options, crypto_suite_names); +} + +// Support any GCM cipher (if enabled through options). For video support only +// 80-bit SHA1 HMAC. For audio 32-bit HMAC is tolerated (if enabled) unless +// bundle is enabled because it is low overhead. +// Pick the crypto in the list that is supported. +static bool SelectCrypto(const MediaContentDescription* offer, + bool bundle, + const webrtc::CryptoOptions& crypto_options, + CryptoParams* crypto_out) { + bool audio = offer->type() == MEDIA_TYPE_AUDIO; + const CryptoParamsVec& cryptos = offer->cryptos(); + + for (const CryptoParams& crypto : cryptos) { + if ((crypto_options.srtp.enable_gcm_crypto_suites && + rtc::IsGcmCryptoSuiteName(crypto.cipher_suite)) || + rtc::kCsAesCm128HmacSha1_80 == crypto.cipher_suite || + (rtc::kCsAesCm128HmacSha1_32 == crypto.cipher_suite && audio && + !bundle && crypto_options.srtp.enable_aes128_sha1_32_crypto_cipher)) { + return CreateCryptoParams(crypto.tag, crypto.cipher_suite, crypto_out); + } + } + return false; +} + // Finds all StreamParams of all media types and attach them to stream_params. static StreamParamsVec GetCurrentStreamParams( const std::vector& active_local_contents) { @@ -318,6 +483,119 @@ static bool UpdateTransportInfoForBundle(const ContentGroup& bundle_group, return true; } +// Gets the CryptoParamsVec of the given `content_name` from `sdesc`, and +// sets it to `cryptos`. +static bool GetCryptosByName(const SessionDescription* sdesc, + const std::string& content_name, + CryptoParamsVec* cryptos) { + if (!sdesc || !cryptos) { + return false; + } + const ContentInfo* content = sdesc->GetContentByName(content_name); + if (!content || !content->media_description()) { + return false; + } + *cryptos = content->media_description()->cryptos(); + return true; +} + +// Prunes the `target_cryptos` by removing the crypto params (cipher_suite) +// which are not available in `filter`. +static void PruneCryptos(const CryptoParamsVec& filter, + CryptoParamsVec* target_cryptos) { + if (!target_cryptos) { + return; + } + + target_cryptos->erase( + std::remove_if(target_cryptos->begin(), target_cryptos->end(), + // Returns true if the `crypto`'s cipher_suite is not + // found in `filter`. + [&filter](const CryptoParams& crypto) { + for (const CryptoParams& entry : filter) { + if (entry.cipher_suite == crypto.cipher_suite) + return false; + } + return true; + }), + target_cryptos->end()); +} + +static bool IsRtpContent(SessionDescription* sdesc, + const std::string& content_name) { + bool is_rtp = false; + ContentInfo* content = sdesc->GetContentByName(content_name); + if (content && content->media_description()) { + is_rtp = IsRtpProtocol(content->media_description()->protocol()); + } + return is_rtp; +} + +// Updates the crypto parameters of the `sdesc` according to the given +// `bundle_group`. The crypto parameters of all the contents within the +// `bundle_group` should be updated to use the common subset of the +// available cryptos. +static bool UpdateCryptoParamsForBundle(const ContentGroup& bundle_group, + SessionDescription* sdesc) { + // The bundle should not be empty. + if (!sdesc || !bundle_group.FirstContentName()) { + return false; + } + + bool common_cryptos_needed = false; + // Get the common cryptos. + const ContentNames& content_names = bundle_group.content_names(); + CryptoParamsVec common_cryptos; + bool first = true; + for (const std::string& content_name : content_names) { + if (!IsRtpContent(sdesc, content_name)) { + continue; + } + // The common cryptos are needed if any of the content does not have DTLS + // enabled. + if (!sdesc->GetTransportInfoByName(content_name)->description.secure()) { + common_cryptos_needed = true; + } + if (first) { + first = false; + // Initial the common_cryptos with the first content in the bundle group. + if (!GetCryptosByName(sdesc, content_name, &common_cryptos)) { + return false; + } + if (common_cryptos.empty()) { + // If there's no crypto params, we should just return. + return true; + } + } else { + CryptoParamsVec cryptos; + if (!GetCryptosByName(sdesc, content_name, &cryptos)) { + return false; + } + PruneCryptos(cryptos, &common_cryptos); + } + } + + if (common_cryptos.empty() && common_cryptos_needed) { + return false; + } + + // Update to use the common cryptos. + for (const std::string& content_name : content_names) { + if (!IsRtpContent(sdesc, content_name)) { + continue; + } + ContentInfo* content = sdesc->GetContentByName(content_name); + if (IsMediaContent(content)) { + MediaContentDescription* media_desc = content->media_description(); + if (!media_desc) { + return false; + } + media_desc->set_cryptos(common_cryptos); + } + } + return true; +} + static std::vector GetActiveContents( const SessionDescription& description, const MediaSessionOptions& session_options) { @@ -371,11 +649,17 @@ static bool IsFlexfecCodec(const C& codec) { } // Create a media content to be offered for the given `sender_options`, -// according to the given parameters. -// The created content is added to the offer. +// according to the given options.rtcp_mux, session_options.is_muc, codecs, +// secure_transport, crypto, and current_streams. If we don't currently have +// crypto (in current_cryptos) and it is enabled (in secure_policy), crypto is +// created (according to crypto_suites). The created content is added to the +// offer. static bool CreateContentOffer( const MediaDescriptionOptions& media_description_options, const MediaSessionOptions& session_options, + const SecurePolicy& secure_policy, + const CryptoParamsVec* current_cryptos, + const std::vector& crypto_suites, const RtpHeaderExtensions& rtp_extensions, UniqueRandomIdGenerator* ssrc_generator, StreamParamsVec* current_streams, @@ -402,6 +686,20 @@ static bool CreateContentOffer( AddSimulcastToMediaDescription(media_description_options, offer); + if (secure_policy != SEC_DISABLED) { + if (current_cryptos) { + AddMediaCryptos(*current_cryptos, offer); + } + if (offer->cryptos().empty()) { + if (!CreateMediaCryptos(crypto_suites, offer)) { + return false; + } + } + } + + if (secure_policy == SEC_REQUIRED && offer->cryptos().empty()) { + return false; + } return true; } template @@ -409,6 +707,9 @@ static bool CreateMediaContentOffer( const MediaDescriptionOptions& media_description_options, const MediaSessionOptions& session_options, const std::vector& codecs, + const SecurePolicy& secure_policy, + const CryptoParamsVec* current_cryptos, + const std::vector& crypto_suites, const RtpHeaderExtensions& rtp_extensions, UniqueRandomIdGenerator* ssrc_generator, StreamParamsVec* current_streams, @@ -421,6 +722,7 @@ static bool CreateMediaContentOffer( } return CreateContentOffer(media_description_options, session_options, + secure_policy, current_cryptos, crypto_suites, rtp_extensions, ssrc_generator, current_streams, offer); } @@ -1042,13 +1344,17 @@ static bool SetCodecsInAnswer( // Create a media content to be answered for the given `sender_options` // according to the given session_options.rtcp_mux, session_options.streams, -// codecs, and current_streams. The codecs and rtcp_mux are all +// codecs, crypto, and current_streams. If we don't currently have crypto (in +// current_cryptos) and it is enabled (in secure_policy), crypto is created +// (according to crypto_suites). The codecs, rtcp_mux, and crypto are all // negotiated with the offer. If the negotiation fails, this method returns // false. The created content is added to the offer. static bool CreateMediaContentAnswer( const MediaContentDescription* offer, const MediaDescriptionOptions& media_description_options, const MediaSessionOptions& session_options, + const SecurePolicy& sdes_policy, + const CryptoParamsVec* current_cryptos, const RtpHeaderExtensions& local_rtp_extensions, UniqueRandomIdGenerator* ssrc_generator, bool enable_encrypted_rtp_header_extensions, @@ -1073,6 +1379,21 @@ static bool CreateMediaContentAnswer( answer->set_remote_estimate(offer->remote_estimate()); + if (sdes_policy != SEC_DISABLED) { + CryptoParams crypto; + if (SelectCrypto(offer, bundle_enabled, session_options.crypto_options, + &crypto)) { + if (current_cryptos) { + FindMatchingCrypto(*current_cryptos, crypto, &crypto); + } + answer->AddCrypto(crypto); + } + } + + if (answer->cryptos().empty() && sdes_policy == SEC_REQUIRED) { + return false; + } + AddSimulcastToMediaDescription(media_description_options, answer); answer->set_direction(NegotiateRtpTransceiverDirection( @@ -1112,7 +1433,9 @@ static bool IsMediaProtocolSupported(MediaType type, static void SetMediaProtocol(bool secure_transport, MediaContentDescription* desc) { - if (secure_transport) + if (!desc->cryptos().empty()) + desc->set_protocol(kMediaProtocolSavpf); + else if (secure_transport) desc->set_protocol(kMediaProtocolDtlsSavpf); else desc->set_protocol(kMediaProtocolAvpf); @@ -1134,6 +1457,23 @@ static const TransportDescription* GetTransportDescription( return desc; } +// Gets the current DTLS state from the transport description. +static bool IsDtlsActive(const ContentInfo* content, + const SessionDescription* current_description) { + if (!content) { + return false; + } + + size_t msection_index = content - ¤t_description->contents()[0]; + + if (current_description->transport_infos().size() <= msection_index) { + return false; + } + + return current_description->transport_infos()[msection_index] + .description.secure(); +} + void MediaDescriptionOptions::AddAudioSender( const std::string& track_id, const std::vector& stream_ids) { @@ -1372,6 +1712,11 @@ std::unique_ptr MediaSessionDescriptionFactory::CreateOffer( << "CreateOffer failed to UpdateTransportInfoForBundle."; return nullptr; } + if (!UpdateCryptoParamsForBundle(offer_bundle, offer.get())) { + RTC_LOG(LS_ERROR) + << "CreateOffer failed to UpdateCryptoParamsForBundle."; + return nullptr; + } } } @@ -1462,21 +1807,16 @@ MediaSessionDescriptionFactory::CreateAnswer( RTC_DCHECK(media_description_options.mid == offer_content->name); // Get the index of the BUNDLE group that this MID belongs to, if any. absl::optional bundle_index; - bool require_transport_attributes = true; for (size_t i = 0; i < offer_bundles.size(); ++i) { if (offer_bundles[i]->HasContentName(media_description_options.mid)) { bundle_index = i; - if (offer_bundles[i]->FirstContentName() && - *offer_bundles[i]->FirstContentName() != - media_description_options.mid) { - require_transport_attributes = false; - } break; } } TransportInfo* bundle_transport = bundle_index.has_value() ? bundle_transports[bundle_index.value()].get() : nullptr; + const ContentInfo* current_content = nullptr; if (current_description && msection_index < current_description->contents().size()) { @@ -1490,9 +1830,8 @@ MediaSessionDescriptionFactory::CreateAnswer( if (!AddAudioContentForAnswer( media_description_options, session_options, offer_content, offer, current_content, current_description, bundle_transport, - require_transport_attributes, answer_audio_codecs, - header_extensions, ¤t_streams, answer.get(), - &ice_credentials)) { + answer_audio_codecs, header_extensions, ¤t_streams, + answer.get(), &ice_credentials)) { return nullptr; } break; @@ -1500,9 +1839,8 @@ MediaSessionDescriptionFactory::CreateAnswer( if (!AddVideoContentForAnswer( media_description_options, session_options, offer_content, offer, current_content, current_description, bundle_transport, - require_transport_attributes, answer_video_codecs, - header_extensions, ¤t_streams, answer.get(), - &ice_credentials)) { + answer_video_codecs, header_extensions, ¤t_streams, + answer.get(), &ice_credentials)) { return nullptr; } break; @@ -1510,8 +1848,7 @@ MediaSessionDescriptionFactory::CreateAnswer( if (!AddDataContentForAnswer( media_description_options, session_options, offer_content, offer, current_content, current_description, bundle_transport, - require_transport_attributes, ¤t_streams, answer.get(), - &ice_credentials)) { + ¤t_streams, answer.get(), &ice_credentials)) { return nullptr; } break; @@ -1519,7 +1856,7 @@ MediaSessionDescriptionFactory::CreateAnswer( if (!AddUnsupportedContentForAnswer( media_description_options, session_options, offer_content, offer, current_content, current_description, bundle_transport, - require_transport_attributes, answer.get(), &ice_credentials)) { + answer.get(), &ice_credentials)) { return nullptr; } break; @@ -1558,6 +1895,12 @@ MediaSessionDescriptionFactory::CreateAnswer( << "CreateAnswer failed to UpdateTransportInfoForBundle."; return NULL; } + + if (!UpdateCryptoParamsForBundle(answer_bundle, answer.get())) { + RTC_LOG(LS_ERROR) + << "CreateAnswer failed to UpdateCryptoParamsForBundle."; + return NULL; + } } } } @@ -1951,14 +2294,23 @@ bool MediaSessionDescriptionFactory::AddAudioContentForOffer( StripCNCodecs(&filtered_codecs); } + cricket::SecurePolicy sdes_policy = + IsDtlsActive(current_content, current_description) ? cricket::SEC_DISABLED + : secure(); + auto audio = std::make_unique(); + std::vector crypto_suites; + GetSupportedAudioSdesCryptoSuiteNames(session_options.crypto_options, + &crypto_suites); if (!CreateMediaContentOffer(media_description_options, session_options, - filtered_codecs, audio_rtp_extensions, - ssrc_generator_, current_streams, audio.get())) { + filtered_codecs, sdes_policy, + GetCryptos(current_content), crypto_suites, + audio_rtp_extensions, ssrc_generator_, + current_streams, audio.get())) { return false; } - bool secure_transport = transport_desc_factory_->IsEncrypted(); + bool secure_transport = (transport_desc_factory_->secure() != SEC_DISABLED); SetMediaProtocol(secure_transport, audio.get()); audio->set_direction(media_description_options.direction); @@ -2036,16 +2388,24 @@ bool MediaSessionDescriptionFactory::AddVideoContentForOffer( } } + cricket::SecurePolicy sdes_policy = + IsDtlsActive(current_content, current_description) ? cricket::SEC_DISABLED + : secure(); auto video = std::make_unique(); + std::vector crypto_suites; + GetSupportedVideoSdesCryptoSuiteNames(session_options.crypto_options, + &crypto_suites); if (!CreateMediaContentOffer(media_description_options, session_options, - filtered_codecs, video_rtp_extensions, - ssrc_generator_, current_streams, video.get())) { + filtered_codecs, sdes_policy, + GetCryptos(current_content), crypto_suites, + video_rtp_extensions, ssrc_generator_, + current_streams, video.get())) { return false; } video->set_bandwidth(kAutoBandwidth); - bool secure_transport = transport_desc_factory_->IsEncrypted(); + bool secure_transport = (transport_desc_factory_->secure() != SEC_DISABLED); SetMediaProtocol(secure_transport, video.get()); video->set_direction(media_description_options.direction); @@ -2071,8 +2431,15 @@ bool MediaSessionDescriptionFactory::AddDataContentForOffer( IceCredentialsIterator* ice_credentials) const { auto data = std::make_unique(); - bool secure_transport = transport_desc_factory_->IsEncrypted(); + bool secure_transport = (transport_desc_factory_->secure() != SEC_DISABLED); + cricket::SecurePolicy sdes_policy = + IsDtlsActive(current_content, current_description) ? cricket::SEC_DISABLED + : secure(); + std::vector crypto_suites; + // SDES doesn't make sense for SCTP, so we disable it, and we only + // get SDES crypto suites for RTP-based data channels. + sdes_policy = cricket::SEC_DISABLED; // Unlike SetMediaProtocol below, we need to set the protocol // before we call CreateMediaContentOffer. Otherwise, // CreateMediaContentOffer won't know this is SCTP and will @@ -2083,7 +2450,8 @@ bool MediaSessionDescriptionFactory::AddDataContentForOffer( data->set_max_message_size(kSctpSendBufferSize); if (!CreateContentOffer(media_description_options, session_options, - RtpHeaderExtensions(), ssrc_generator_, + sdes_policy, GetCryptos(current_content), + crypto_suites, RtpHeaderExtensions(), ssrc_generator_, current_streams, data.get())) { return false; } @@ -2143,7 +2511,6 @@ bool MediaSessionDescriptionFactory::AddAudioContentForAnswer( const ContentInfo* current_content, const SessionDescription* current_description, const TransportInfo* bundle_transport, - bool require_transport_attributes, const AudioCodecs& audio_codecs, const RtpHeaderExtensions& default_audio_rtp_header_extensions, StreamParamsVec* current_streams, @@ -2156,7 +2523,7 @@ bool MediaSessionDescriptionFactory::AddAudioContentForAnswer( std::unique_ptr audio_transport = CreateTransportAnswer( media_description_options.mid, offer_description, media_description_options.transport_options, current_description, - require_transport_attributes, ice_credentials); + bundle_transport != nullptr, ice_credentials); if (!audio_transport) { return false; } @@ -2211,6 +2578,9 @@ bool MediaSessionDescriptionFactory::AddAudioContentForAnswer( bool bundle_enabled = offer_description->HasGroup(GROUP_TYPE_BUNDLE) && session_options.bundle_enabled; auto audio_answer = std::make_unique(); + // Do not require or create SDES cryptos if DTLS is used. + cricket::SecurePolicy sdes_policy = + audio_transport->secure() ? cricket::SEC_DISABLED : secure(); if (!SetCodecsInAnswer(offer_audio_description, filtered_codecs, media_description_options, session_options, ssrc_generator_, current_streams, @@ -2219,6 +2589,7 @@ bool MediaSessionDescriptionFactory::AddAudioContentForAnswer( } if (!CreateMediaContentAnswer( offer_audio_description, media_description_options, session_options, + sdes_policy, GetCryptos(current_content), filtered_rtp_header_extensions(default_audio_rtp_header_extensions), ssrc_generator_, enable_encrypted_rtp_header_extensions_, current_streams, bundle_enabled, audio_answer.get())) { @@ -2256,7 +2627,6 @@ bool MediaSessionDescriptionFactory::AddVideoContentForAnswer( const ContentInfo* current_content, const SessionDescription* current_description, const TransportInfo* bundle_transport, - bool require_transport_attributes, const VideoCodecs& video_codecs, const RtpHeaderExtensions& default_video_rtp_header_extensions, StreamParamsVec* current_streams, @@ -2269,7 +2639,7 @@ bool MediaSessionDescriptionFactory::AddVideoContentForAnswer( std::unique_ptr video_transport = CreateTransportAnswer( media_description_options.mid, offer_description, media_description_options.transport_options, current_description, - require_transport_attributes, ice_credentials); + bundle_transport != nullptr, ice_credentials); if (!video_transport) { return false; } @@ -2328,6 +2698,9 @@ bool MediaSessionDescriptionFactory::AddVideoContentForAnswer( bool bundle_enabled = offer_description->HasGroup(GROUP_TYPE_BUNDLE) && session_options.bundle_enabled; auto video_answer = std::make_unique(); + // Do not require or create SDES cryptos if DTLS is used. + cricket::SecurePolicy sdes_policy = + video_transport->secure() ? cricket::SEC_DISABLED : secure(); if (!SetCodecsInAnswer(offer_video_description, filtered_codecs, media_description_options, session_options, ssrc_generator_, current_streams, @@ -2336,6 +2709,7 @@ bool MediaSessionDescriptionFactory::AddVideoContentForAnswer( } if (!CreateMediaContentAnswer( offer_video_description, media_description_options, session_options, + sdes_policy, GetCryptos(current_content), filtered_rtp_header_extensions(default_video_rtp_header_extensions), ssrc_generator_, enable_encrypted_rtp_header_extensions_, current_streams, bundle_enabled, video_answer.get())) { @@ -2371,18 +2745,20 @@ bool MediaSessionDescriptionFactory::AddDataContentForAnswer( const ContentInfo* current_content, const SessionDescription* current_description, const TransportInfo* bundle_transport, - bool require_transport_attributes, StreamParamsVec* current_streams, SessionDescription* answer, IceCredentialsIterator* ice_credentials) const { std::unique_ptr data_transport = CreateTransportAnswer( media_description_options.mid, offer_description, media_description_options.transport_options, current_description, - require_transport_attributes, ice_credentials); + bundle_transport != nullptr, ice_credentials); if (!data_transport) { return false; } + // Do not require or create SDES cryptos if DTLS is used. + cricket::SecurePolicy sdes_policy = + data_transport->secure() ? cricket::SEC_DISABLED : secure(); bool bundle_enabled = offer_description->HasGroup(GROUP_TYPE_BUNDLE) && session_options.bundle_enabled; RTC_CHECK(IsMediaContentOfType(offer_content, MEDIA_TYPE_DATA)); @@ -2407,9 +2783,9 @@ bool MediaSessionDescriptionFactory::AddDataContentForAnswer( } if (!CreateMediaContentAnswer( offer_data_description, media_description_options, session_options, - RtpHeaderExtensions(), ssrc_generator_, - enable_encrypted_rtp_header_extensions_, current_streams, - bundle_enabled, data_answer.get())) { + sdes_policy, GetCryptos(current_content), RtpHeaderExtensions(), + ssrc_generator_, enable_encrypted_rtp_header_extensions_, + current_streams, bundle_enabled, data_answer.get())) { return false; // Fails the session setup. } // Respond with sctpmap if the offer uses sctpmap. @@ -2444,13 +2820,12 @@ bool MediaSessionDescriptionFactory::AddUnsupportedContentForAnswer( const ContentInfo* current_content, const SessionDescription* current_description, const TransportInfo* bundle_transport, - bool require_transport_attributes, SessionDescription* answer, IceCredentialsIterator* ice_credentials) const { std::unique_ptr unsupported_transport = CreateTransportAnswer(media_description_options.mid, offer_description, media_description_options.transport_options, - current_description, require_transport_attributes, + current_description, bundle_transport != nullptr, ice_credentials); if (!unsupported_transport) { return false; diff --git a/pc/media_session.h b/pc/media_session.h index b648ba6de1..bb97f42b27 100644 --- a/pc/media_session.h +++ b/pc/media_session.h @@ -156,6 +156,8 @@ class MediaSessionDescriptionFactory { const VideoCodecs& recv_codecs); RtpHeaderExtensions filtered_rtp_header_extensions( RtpHeaderExtensions extensions) const; + SecurePolicy secure() const { return secure_; } + void set_secure(SecurePolicy s) { secure_ = s; } void set_enable_encrypted_rtp_header_extensions(bool enable) { enable_encrypted_rtp_header_extensions_ = enable; @@ -272,7 +274,6 @@ class MediaSessionDescriptionFactory { const ContentInfo* current_content, const SessionDescription* current_description, const TransportInfo* bundle_transport, - bool require_transport_attributes, const AudioCodecs& audio_codecs, const RtpHeaderExtensions& default_audio_rtp_header_extensions, StreamParamsVec* current_streams, @@ -287,7 +288,6 @@ class MediaSessionDescriptionFactory { const ContentInfo* current_content, const SessionDescription* current_description, const TransportInfo* bundle_transport, - bool require_transport_attributes, const VideoCodecs& video_codecs, const RtpHeaderExtensions& default_video_rtp_header_extensions, StreamParamsVec* current_streams, @@ -302,7 +302,6 @@ class MediaSessionDescriptionFactory { const ContentInfo* current_content, const SessionDescription* current_description, const TransportInfo* bundle_transport, - bool require_transport_attributes, StreamParamsVec* current_streams, SessionDescription* answer, IceCredentialsIterator* ice_credentials) const; @@ -315,7 +314,6 @@ class MediaSessionDescriptionFactory { const ContentInfo* current_content, const SessionDescription* current_description, const TransportInfo* bundle_transport, - bool require_transport_attributes, SessionDescription* answer, IceCredentialsIterator* ice_credentials) const; @@ -339,6 +337,9 @@ class MediaSessionDescriptionFactory { // This object is not owned by the channel so it must outlive it. rtc::UniqueRandomIdGenerator* const ssrc_generator_; bool enable_encrypted_rtp_header_extensions_ = false; + // TODO(zhihuang): Rename secure_ to sdec_policy_; rename the related getter + // and setter. + SecurePolicy secure_ = SEC_DISABLED; const TransportDescriptionFactory* transport_desc_factory_; }; @@ -382,6 +383,26 @@ VideoContentDescription* GetFirstVideoContentDescription( SctpDataContentDescription* GetFirstSctpDataContentDescription( SessionDescription* sdesc); +// Helper functions to return crypto suites used for SDES. +void GetSupportedAudioSdesCryptoSuites( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suites); +void GetSupportedVideoSdesCryptoSuites( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suites); +void GetSupportedDataSdesCryptoSuites( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suites); +void GetSupportedAudioSdesCryptoSuiteNames( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suite_names); +void GetSupportedVideoSdesCryptoSuiteNames( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suite_names); +void GetSupportedDataSdesCryptoSuiteNames( + const webrtc::CryptoOptions& crypto_options, + std::vector* crypto_suite_names); + } // namespace cricket #endif // PC_MEDIA_SESSION_H_ diff --git a/pc/media_session_unittest.cc b/pc/media_session_unittest.cc index 9adc23140c..a02b4c1415 100644 --- a/pc/media_session_unittest.cc +++ b/pc/media_session_unittest.cc @@ -26,6 +26,7 @@ #include "p2p/base/transport_description.h" #include "p2p/base/transport_info.h" #include "pc/rtp_media_utils.h" +#include "pc/srtp_filter.h" #include "rtc_base/checks.h" #include "rtc_base/fake_ssl_identity.h" #include "rtc_base/gunit.h" @@ -36,11 +37,16 @@ #include "test/field_trial.h" #include "test/gmock.h" +#define ASSERT_CRYPTO(cd, s, cs) \ + ASSERT_EQ(s, cd->cryptos().size()); \ + ASSERT_EQ(cs, cd->cryptos()[0].cipher_suite) + typedef std::vector Candidates; using cricket::AudioCodec; using cricket::AudioContentDescription; using cricket::ContentInfo; +using cricket::CryptoParamsVec; using cricket::GetFirstAudioContent; using cricket::GetFirstAudioContentDescription; using cricket::GetFirstDataContent; @@ -59,6 +65,9 @@ using cricket::MediaType; using cricket::RidDescription; using cricket::RidDirection; using cricket::SctpDataContentDescription; +using cricket::SEC_DISABLED; +using cricket::SEC_ENABLED; +using cricket::SEC_REQUIRED; using cricket::SessionDescription; using cricket::SimulcastDescription; using cricket::SimulcastLayer; @@ -256,6 +265,11 @@ static const char* kMediaProtocolsDtls[] = { "TCP/TLS/RTP/SAVPF", "TCP/TLS/RTP/SAVP", "UDP/TLS/RTP/SAVPF", "UDP/TLS/RTP/SAVP"}; +// SRTP cipher name negotiated by the tests. This must be updated if the +// default changes. +static const char* kDefaultSrtpCryptoSuite = kCsAesCm128HmacSha1_80; +static const char* kDefaultSrtpCryptoSuiteGcm = kCsAeadAes256Gcm; + // These constants are used to make the code using "AddMediaDescriptionOptions" // more readable. static constexpr bool kStopped = true; @@ -389,6 +403,17 @@ static MediaSessionOptions CreatePlanBMediaSessionOptions() { return session_options; } +// prefers GCM SDES crypto suites by removing non-GCM defaults. +void PreferGcmCryptoParameters(CryptoParamsVec* cryptos) { + cryptos->erase( + std::remove_if(cryptos->begin(), cryptos->end(), + [](const cricket::CryptoParams& crypto) { + return crypto.cipher_suite != kCsAeadAes256Gcm && + crypto.cipher_suite != kCsAeadAes128Gcm; + }), + cryptos->end()); +} + // TODO(zhihuang): Most of these tests were written while MediaSessionOptions // was designed for Plan B SDP, where only one audio "m=" section and one video // "m=" section could be generated, and ordering couldn't be controlled. Many of @@ -405,7 +430,10 @@ class MediaSessionDescriptionFactoryTest : public ::testing::Test { MAKE_VECTOR(kAudioCodecs2)); f2_.set_video_codecs(MAKE_VECTOR(kVideoCodecs2), MAKE_VECTOR(kVideoCodecs2)); - SetDtls(true); + tdf1_.set_certificate(rtc::RTCCertificate::Create( + std::unique_ptr(new rtc::FakeSSLIdentity("id1")))); + tdf2_.set_certificate(rtc::RTCCertificate::Create( + std::unique_ptr(new rtc::FakeSSLIdentity("id2")))); } // Create a video StreamParamsVec object with: @@ -435,6 +463,18 @@ class MediaSessionDescriptionFactoryTest : public ::testing::Test { return video_streams; } + bool CompareCryptoParams(const CryptoParamsVec& c1, + const CryptoParamsVec& c2) { + if (c1.size() != c2.size()) + return false; + for (size_t i = 0; i < c1.size(); ++i) + if (c1[i].tag != c2[i].tag || c1[i].cipher_suite != c2[i].cipher_suite || + c1[i].key_params != c2[i].key_params || + c1[i].session_params != c2[i].session_params) + return false; + return true; + } + // Returns true if the transport info contains "renomination" as an // ICE option. bool GetIceRenomination(const TransportInfo* transport_info) { @@ -547,6 +587,50 @@ class MediaSessionDescriptionFactoryTest : public ::testing::Test { } } + void TestCryptoWithBundle(bool offer) { + f1_.set_secure(SEC_ENABLED); + MediaSessionOptions options; + AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &options); + std::unique_ptr ref_desc; + std::unique_ptr desc; + if (offer) { + options.bundle_enabled = false; + ref_desc = f1_.CreateOffer(options, NULL); + options.bundle_enabled = true; + desc = f1_.CreateOffer(options, ref_desc.get()); + } else { + options.bundle_enabled = true; + ref_desc = f1_.CreateOffer(options, NULL); + desc = f1_.CreateAnswer(ref_desc.get(), options, NULL); + } + ASSERT_TRUE(desc); + const cricket::MediaContentDescription* audio_media_desc = + desc->GetContentDescriptionByName("audio"); + ASSERT_TRUE(audio_media_desc); + const cricket::MediaContentDescription* video_media_desc = + desc->GetContentDescriptionByName("video"); + ASSERT_TRUE(video_media_desc); + EXPECT_TRUE(CompareCryptoParams(audio_media_desc->cryptos(), + video_media_desc->cryptos())); + EXPECT_EQ(1u, audio_media_desc->cryptos().size()); + EXPECT_EQ(kDefaultSrtpCryptoSuite, + audio_media_desc->cryptos()[0].cipher_suite); + + // Verify the selected crypto is one from the reference audio + // media content. + const cricket::MediaContentDescription* ref_audio_media_desc = + ref_desc->GetContentDescriptionByName("audio"); + bool found = false; + for (size_t i = 0; i < ref_audio_media_desc->cryptos().size(); ++i) { + if (ref_audio_media_desc->cryptos()[i].Matches( + audio_media_desc->cryptos()[0])) { + found = true; + break; + } + } + EXPECT_TRUE(found); + } + // This test that the audio and video media direction is set to // `expected_direction_in_answer` in an answer if the offer direction is set // to `direction_in_offer` and the answer is willing to both send and receive. @@ -590,6 +674,59 @@ class MediaSessionDescriptionFactoryTest : public ::testing::Test { return true; } + void TestVideoGcmCipher(bool gcm_offer, bool gcm_answer) { + MediaSessionOptions offer_opts; + AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &offer_opts); + offer_opts.crypto_options.srtp.enable_gcm_crypto_suites = gcm_offer; + + MediaSessionOptions answer_opts; + AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &answer_opts); + answer_opts.crypto_options.srtp.enable_gcm_crypto_suites = gcm_answer; + + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + std::unique_ptr offer = + f1_.CreateOffer(offer_opts, NULL); + ASSERT_TRUE(offer.get() != NULL); + if (gcm_offer && gcm_answer) { + for (cricket::ContentInfo& content : offer->contents()) { + auto cryptos = content.media_description()->cryptos(); + PreferGcmCryptoParameters(&cryptos); + content.media_description()->set_cryptos(cryptos); + } + } + std::unique_ptr answer = + f2_.CreateAnswer(offer.get(), answer_opts, NULL); + const ContentInfo* ac = answer->GetContentByName("audio"); + const ContentInfo* vc = answer->GetContentByName("video"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc != NULL); + EXPECT_EQ(MediaProtocolType::kRtp, ac->type); + EXPECT_EQ(MediaProtocolType::kRtp, vc->type); + const AudioContentDescription* acd = ac->media_description()->as_audio(); + const VideoContentDescription* vcd = vc->media_description()->as_video(); + EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); + EXPECT_THAT(acd->codecs(), ElementsAreArray(kAudioCodecsAnswer)); + EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // negotiated auto bw + EXPECT_EQ(0U, acd->first_ssrc()); // no sender is attached + EXPECT_TRUE(acd->rtcp_mux()); // negotiated rtcp-mux + if (gcm_offer && gcm_answer) { + ASSERT_CRYPTO(acd, 1U, kDefaultSrtpCryptoSuiteGcm); + } else { + ASSERT_CRYPTO(acd, 1U, kDefaultSrtpCryptoSuite); + } + EXPECT_EQ(MEDIA_TYPE_VIDEO, vcd->type()); + EXPECT_THAT(vcd->codecs(), ElementsAreArray(kVideoCodecsAnswer)); + EXPECT_EQ(0U, vcd->first_ssrc()); // no sender is attached + EXPECT_TRUE(vcd->rtcp_mux()); // negotiated rtcp-mux + if (gcm_offer && gcm_answer) { + ASSERT_CRYPTO(vcd, 1U, kDefaultSrtpCryptoSuiteGcm); + } else { + ASSERT_CRYPTO(vcd, 1U, kDefaultSrtpCryptoSuite); + } + EXPECT_EQ(cricket::kMediaProtocolSavpf, vcd->protocol()); + } + void TestTransportSequenceNumberNegotiation( const cricket::RtpHeaderExtensions& local, const cricket::RtpHeaderExtensions& offered, @@ -644,24 +781,6 @@ class MediaSessionDescriptionFactoryTest : public ::testing::Test { } protected: - // Helper to turn connection encryption with DTLS on or off. - // Default state is on. - void SetDtls(bool dtls_f1, bool dtls_f2) { - if (dtls_f1) { - tdf1_.set_certificate(rtc::RTCCertificate::Create( - std::unique_ptr(new rtc::FakeSSLIdentity("id1")))); - } else { - tdf1_.set_certificate(nullptr); - } - if (dtls_f2) { - tdf2_.set_certificate(rtc::RTCCertificate::Create( - std::unique_ptr(new rtc::FakeSSLIdentity("id2")))); - } else { - tdf2_.set_certificate(nullptr); - } - } - void SetDtls(bool dtls) { SetDtls(dtls, dtls); } - UniqueRandomIdGenerator ssrc_generator1; UniqueRandomIdGenerator ssrc_generator2; MediaSessionDescriptionFactory f1_; @@ -672,6 +791,7 @@ class MediaSessionDescriptionFactoryTest : public ::testing::Test { // Create a typical audio offer, and ensure it matches what we expect. TEST_F(MediaSessionDescriptionFactoryTest, TestCreateAudioOffer) { + f1_.set_secure(SEC_ENABLED); std::unique_ptr offer = f1_.CreateOffer(CreatePlanBMediaSessionOptions(), NULL); ASSERT_TRUE(offer.get() != NULL); @@ -686,13 +806,15 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateAudioOffer) { EXPECT_EQ(0U, acd->first_ssrc()); // no sender is attached. EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // default bandwidth (auto) EXPECT_TRUE(acd->rtcp_mux()); // rtcp-mux defaults on - EXPECT_EQ(cricket::kMediaProtocolDtlsSavpf, acd->protocol()); + ASSERT_CRYPTO(acd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_EQ(cricket::kMediaProtocolSavpf, acd->protocol()); } // Create a typical video offer, and ensure it matches what we expect. TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoOffer) { MediaSessionOptions opts; AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &opts); + f1_.set_secure(SEC_ENABLED); std::unique_ptr offer = f1_.CreateOffer(opts, NULL); ASSERT_TRUE(offer.get() != NULL); const ContentInfo* ac = offer->GetContentByName("audio"); @@ -708,13 +830,15 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoOffer) { EXPECT_EQ(0U, acd->first_ssrc()); // no sender is attached EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // default bandwidth (auto) EXPECT_TRUE(acd->rtcp_mux()); // rtcp-mux defaults on - EXPECT_EQ(cricket::kMediaProtocolDtlsSavpf, acd->protocol()); + ASSERT_CRYPTO(acd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_EQ(cricket::kMediaProtocolSavpf, acd->protocol()); EXPECT_EQ(MEDIA_TYPE_VIDEO, vcd->type()); EXPECT_EQ(f1_.video_sendrecv_codecs(), vcd->codecs()); EXPECT_EQ(0U, vcd->first_ssrc()); // no sender is attached EXPECT_EQ(kAutoBandwidth, vcd->bandwidth()); // default bandwidth (auto) EXPECT_TRUE(vcd->rtcp_mux()); // rtcp-mux defaults on - EXPECT_EQ(cricket::kMediaProtocolDtlsSavpf, vcd->protocol()); + ASSERT_CRYPTO(vcd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_EQ(cricket::kMediaProtocolSavpf, vcd->protocol()); } // Test creating an offer with bundle where the Codecs have the same dynamic @@ -744,6 +868,8 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestBundleOfferWithSameCodecPlType) { // after an audio only session has been negotiated. TEST_F(MediaSessionDescriptionFactoryTest, TestCreateUpdatedVideoOfferWithBundle) { + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); MediaSessionOptions opts; AddMediaDescriptionOptions(MEDIA_TYPE_AUDIO, "audio", RtpTransceiverDirection::kRecvOnly, kActive, @@ -769,16 +895,18 @@ TEST_F(MediaSessionDescriptionFactoryTest, EXPECT_TRUE(NULL != vcd); EXPECT_TRUE(NULL != acd); - EXPECT_EQ(cricket::kMediaProtocolDtlsSavpf, acd->protocol()); - EXPECT_EQ(cricket::kMediaProtocolDtlsSavpf, vcd->protocol()); + ASSERT_CRYPTO(acd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_EQ(cricket::kMediaProtocolSavpf, acd->protocol()); + ASSERT_CRYPTO(vcd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_EQ(cricket::kMediaProtocolSavpf, vcd->protocol()); } // Create an SCTP data offer with bundle without error. TEST_F(MediaSessionDescriptionFactoryTest, TestCreateSctpDataOffer) { - SetDtls(false); MediaSessionOptions opts; opts.bundle_enabled = true; AddDataSection(RtpTransceiverDirection::kSendRecv, &opts); + f1_.set_secure(SEC_ENABLED); std::unique_ptr offer = f1_.CreateOffer(opts, NULL); EXPECT_TRUE(offer.get() != NULL); EXPECT_TRUE(offer->GetContentByName("data") != NULL); @@ -793,6 +921,8 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateSecureSctpDataOffer) { MediaSessionOptions opts; opts.bundle_enabled = true; AddDataSection(RtpTransceiverDirection::kSendRecv, &opts); + f1_.set_secure(SEC_ENABLED); + tdf1_.set_secure(SEC_ENABLED); std::unique_ptr offer = f1_.CreateOffer(opts, NULL); EXPECT_TRUE(offer.get() != NULL); EXPECT_TRUE(offer->GetContentByName("data") != NULL); @@ -804,10 +934,10 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateSecureSctpDataOffer) { // Test creating an sctp data channel from an already generated offer. TEST_F(MediaSessionDescriptionFactoryTest, TestCreateImplicitSctpDataOffer) { - SetDtls(false); MediaSessionOptions opts; opts.bundle_enabled = true; AddDataSection(RtpTransceiverDirection::kSendRecv, &opts); + f1_.set_secure(SEC_ENABLED); std::unique_ptr offer1(f1_.CreateOffer(opts, NULL)); ASSERT_TRUE(offer1.get() != NULL); const ContentInfo* data = offer1->GetContentByName("data"); @@ -1108,6 +1238,8 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateOfferContentOrder) { // Create a typical audio answer, and ensure it matches what we expect. TEST_F(MediaSessionDescriptionFactoryTest, TestCreateAudioAnswer) { + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); std::unique_ptr offer = f1_.CreateOffer(CreatePlanBMediaSessionOptions(), NULL); ASSERT_TRUE(offer.get() != NULL); @@ -1124,13 +1256,47 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateAudioAnswer) { EXPECT_EQ(0U, acd->first_ssrc()); // no sender is attached EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // negotiated auto bw EXPECT_TRUE(acd->rtcp_mux()); // negotiated rtcp-mux - EXPECT_EQ(cricket::kMediaProtocolDtlsSavpf, acd->protocol()); + ASSERT_CRYPTO(acd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_EQ(cricket::kMediaProtocolSavpf, acd->protocol()); +} + +// Create a typical audio answer with GCM ciphers enabled, and ensure it +// matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateAudioAnswerGcm) { + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + MediaSessionOptions opts = CreatePlanBMediaSessionOptions(); + opts.crypto_options.srtp.enable_gcm_crypto_suites = true; + std::unique_ptr offer = f1_.CreateOffer(opts, NULL); + ASSERT_TRUE(offer.get() != NULL); + for (cricket::ContentInfo& content : offer->contents()) { + auto cryptos = content.media_description()->cryptos(); + PreferGcmCryptoParameters(&cryptos); + content.media_description()->set_cryptos(cryptos); + } + std::unique_ptr answer = + f2_.CreateAnswer(offer.get(), opts, NULL); + const ContentInfo* ac = answer->GetContentByName("audio"); + const ContentInfo* vc = answer->GetContentByName("video"); + ASSERT_TRUE(ac != NULL); + ASSERT_TRUE(vc == NULL); + EXPECT_EQ(MediaProtocolType::kRtp, ac->type); + const AudioContentDescription* acd = ac->media_description()->as_audio(); + EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); + EXPECT_THAT(acd->codecs(), ElementsAreArray(kAudioCodecsAnswer)); + EXPECT_EQ(0U, acd->first_ssrc()); // no sender is attached + EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // negotiated auto bw + EXPECT_TRUE(acd->rtcp_mux()); // negotiated rtcp-mux + ASSERT_CRYPTO(acd, 1U, kDefaultSrtpCryptoSuiteGcm); + EXPECT_EQ(cricket::kMediaProtocolSavpf, acd->protocol()); } // Create a typical video answer, and ensure it matches what we expect. TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoAnswer) { MediaSessionOptions opts; AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &opts); + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); std::unique_ptr offer = f1_.CreateOffer(opts, NULL); ASSERT_TRUE(offer.get() != NULL); std::unique_ptr answer = @@ -1148,11 +1314,31 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoAnswer) { EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // negotiated auto bw EXPECT_EQ(0U, acd->first_ssrc()); // no sender is attached EXPECT_TRUE(acd->rtcp_mux()); // negotiated rtcp-mux + ASSERT_CRYPTO(acd, 1U, kDefaultSrtpCryptoSuite); EXPECT_EQ(MEDIA_TYPE_VIDEO, vcd->type()); EXPECT_THAT(vcd->codecs(), ElementsAreArray(kVideoCodecsAnswer)); EXPECT_EQ(0U, vcd->first_ssrc()); // no sender is attached EXPECT_TRUE(vcd->rtcp_mux()); // negotiated rtcp-mux - EXPECT_EQ(cricket::kMediaProtocolDtlsSavpf, vcd->protocol()); + ASSERT_CRYPTO(vcd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_EQ(cricket::kMediaProtocolSavpf, vcd->protocol()); +} + +// Create a typical video answer with GCM ciphers enabled, and ensure it +// matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoAnswerGcm) { + TestVideoGcmCipher(true, true); +} + +// Create a typical video answer with GCM ciphers enabled for the offer only, +// and ensure it matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoAnswerGcmOffer) { + TestVideoGcmCipher(true, false); +} + +// Create a typical video answer with GCM ciphers enabled for the answer only, +// and ensure it matches what we expect. +TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoAnswerGcmAnswer) { + TestVideoGcmCipher(false, true); } // The use_sctpmap flag should be set in an Sctp DataContentDescription by @@ -1202,6 +1388,13 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateDataAnswerWithoutSctpmap) { // and "TCP/DTLS/SCTP" offers. TEST_F(MediaSessionDescriptionFactoryTest, TestCreateDataAnswerToDifferentOfferedProtos) { + // Need to enable DTLS offer/answer generation (disabled by default in this + // test). + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + tdf1_.set_secure(SEC_ENABLED); + tdf2_.set_secure(SEC_ENABLED); + MediaSessionOptions opts; AddDataSection(RtpTransceiverDirection::kSendRecv, &opts); std::unique_ptr offer = f1_.CreateOffer(opts, nullptr); @@ -1229,6 +1422,13 @@ TEST_F(MediaSessionDescriptionFactoryTest, TEST_F(MediaSessionDescriptionFactoryTest, TestCreateDataAnswerToOfferWithDefinedMessageSize) { + // Need to enable DTLS offer/answer generation (disabled by default in this + // test). + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + tdf1_.set_secure(SEC_ENABLED); + tdf2_.set_secure(SEC_ENABLED); + MediaSessionOptions opts; AddDataSection(RtpTransceiverDirection::kSendRecv, &opts); std::unique_ptr offer = f1_.CreateOffer(opts, nullptr); @@ -1251,6 +1451,13 @@ TEST_F(MediaSessionDescriptionFactoryTest, TEST_F(MediaSessionDescriptionFactoryTest, TestCreateDataAnswerToOfferWithZeroMessageSize) { + // Need to enable DTLS offer/answer generation (disabled by default in this + // test). + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + tdf1_.set_secure(SEC_ENABLED); + tdf2_.set_secure(SEC_ENABLED); + MediaSessionOptions opts; AddDataSection(RtpTransceiverDirection::kSendRecv, &opts); std::unique_ptr offer = f1_.CreateOffer(opts, nullptr); @@ -1337,10 +1544,13 @@ TEST_F(MediaSessionDescriptionFactoryTest, CreateAnswerToInactiveOffer) { RtpTransceiverDirection::kInactive); } -// Test that the media protocol is RTP/AVPF if DTLS is disabled. +// Test that the media protocol is RTP/AVPF if DTLS and SDES are disabled. TEST_F(MediaSessionDescriptionFactoryTest, AudioOfferAnswerWithCryptoDisabled) { MediaSessionOptions opts = CreatePlanBMediaSessionOptions(); - SetDtls(false); + f1_.set_secure(SEC_DISABLED); + f2_.set_secure(SEC_DISABLED); + tdf1_.set_secure(SEC_DISABLED); + tdf2_.set_secure(SEC_DISABLED); std::unique_ptr offer = f1_.CreateOffer(opts, NULL); const AudioContentDescription* offer_acd = @@ -2102,6 +2312,7 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateMultiStreamVideoOffer) { AttachSenderToMediaDescriptionOptions("audio", MEDIA_TYPE_AUDIO, kAudioTrack2, {kMediaStream1}, 1, &opts); + f1_.set_secure(SEC_ENABLED); std::unique_ptr offer = f1_.CreateOffer(opts, NULL); ASSERT_TRUE(offer.get() != NULL); @@ -2126,9 +2337,11 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateMultiStreamVideoOffer) { EXPECT_EQ(kAutoBandwidth, acd->bandwidth()); // default bandwidth (auto) EXPECT_TRUE(acd->rtcp_mux()); // rtcp-mux defaults on + ASSERT_CRYPTO(acd, 1U, kDefaultSrtpCryptoSuite); EXPECT_EQ(MEDIA_TYPE_VIDEO, vcd->type()); EXPECT_EQ(f1_.video_sendrecv_codecs(), vcd->codecs()); + ASSERT_CRYPTO(vcd, 1U, kDefaultSrtpCryptoSuite); const StreamParamsVec& video_streams = vcd->streams(); ASSERT_EQ(1U, video_streams.size()); @@ -2161,6 +2374,10 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateMultiStreamVideoOffer) { EXPECT_EQ(acd->codecs(), updated_acd->codecs()); EXPECT_EQ(vcd->type(), updated_vcd->type()); EXPECT_EQ(vcd->codecs(), updated_vcd->codecs()); + ASSERT_CRYPTO(updated_acd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_TRUE(CompareCryptoParams(acd->cryptos(), updated_acd->cryptos())); + ASSERT_CRYPTO(updated_vcd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_TRUE(CompareCryptoParams(vcd->cryptos(), updated_vcd->cryptos())); const StreamParamsVec& updated_audio_streams = updated_acd->streams(); ASSERT_EQ(2U, updated_audio_streams.size()); @@ -2378,6 +2595,8 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateMultiStreamVideoAnswer) { AddMediaDescriptionOptions(MEDIA_TYPE_VIDEO, "video", RtpTransceiverDirection::kRecvOnly, kActive, &offer_opts); + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); std::unique_ptr offer = f1_.CreateOffer(offer_opts, NULL); MediaSessionOptions answer_opts; @@ -2404,6 +2623,8 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateMultiStreamVideoAnswer) { ASSERT_TRUE(vc != NULL); const AudioContentDescription* acd = ac->media_description()->as_audio(); const VideoContentDescription* vcd = vc->media_description()->as_video(); + ASSERT_CRYPTO(acd, 1U, kDefaultSrtpCryptoSuite); + ASSERT_CRYPTO(vcd, 1U, kDefaultSrtpCryptoSuite); EXPECT_EQ(MEDIA_TYPE_AUDIO, acd->type()); EXPECT_THAT(acd->codecs(), ElementsAreArray(kAudioCodecsAnswer)); @@ -2449,6 +2670,11 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCreateMultiStreamVideoAnswer) { const VideoContentDescription* updated_vcd = vc->media_description()->as_video(); + ASSERT_CRYPTO(updated_acd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_TRUE(CompareCryptoParams(acd->cryptos(), updated_acd->cryptos())); + ASSERT_CRYPTO(updated_vcd, 1U, kDefaultSrtpCryptoSuite); + EXPECT_TRUE(CompareCryptoParams(vcd->cryptos(), updated_vcd->cryptos())); + EXPECT_EQ(acd->type(), updated_acd->type()); EXPECT_EQ(acd->codecs(), updated_acd->codecs()); EXPECT_EQ(vcd->type(), updated_vcd->type()); @@ -3380,11 +3606,27 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestTransportInfo(false, options, true); } +// Create an offer with bundle enabled and verify the crypto parameters are +// the common set of the available cryptos. +TEST_F(MediaSessionDescriptionFactoryTest, TestCryptoWithOfferBundle) { + TestCryptoWithBundle(true); +} + +// Create an answer with bundle enabled and verify the crypto parameters are +// the common set of the available cryptos. +TEST_F(MediaSessionDescriptionFactoryTest, TestCryptoWithAnswerBundle) { + TestCryptoWithBundle(false); +} + // Verifies that creating answer fails if the offer has UDP/TLS/RTP/SAVPF but // DTLS is not enabled locally. TEST_F(MediaSessionDescriptionFactoryTest, TestOfferDtlsSavpfWithoutDtlsFailed) { - SetDtls(true, false); + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + tdf1_.set_secure(SEC_DISABLED); + tdf2_.set_secure(SEC_DISABLED); + std::unique_ptr offer = f1_.CreateOffer(CreatePlanBMediaSessionOptions(), NULL); ASSERT_TRUE(offer.get() != NULL); @@ -3406,6 +3648,11 @@ TEST_F(MediaSessionDescriptionFactoryTest, // Offers UDP/TLS/RTP/SAVPF and verifies the answer can be created and contains // UDP/TLS/RTP/SAVPF. TEST_F(MediaSessionDescriptionFactoryTest, TestOfferDtlsSavpfCreateAnswer) { + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + tdf1_.set_secure(SEC_ENABLED); + tdf2_.set_secure(SEC_ENABLED); + std::unique_ptr offer = f1_.CreateOffer(CreatePlanBMediaSessionOptions(), NULL); ASSERT_TRUE(offer.get() != NULL); @@ -3428,9 +3675,120 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestOfferDtlsSavpfCreateAnswer) { EXPECT_EQ(cricket::kMediaProtocolDtlsSavpf, answer_audio_desc->protocol()); } +// Test that we include both SDES and DTLS in the offer, but only include SDES +// in the answer if DTLS isn't negotiated. +TEST_F(MediaSessionDescriptionFactoryTest, TestCryptoDtls) { + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); + tdf1_.set_secure(SEC_ENABLED); + tdf2_.set_secure(SEC_DISABLED); + MediaSessionOptions options; + AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &options); + std::unique_ptr offer, answer; + const cricket::MediaContentDescription* audio_media_desc; + const cricket::MediaContentDescription* video_media_desc; + const cricket::TransportDescription* audio_trans_desc; + const cricket::TransportDescription* video_trans_desc; + + // Generate an offer with SDES and DTLS support. + offer = f1_.CreateOffer(options, NULL); + ASSERT_TRUE(offer.get() != NULL); + + audio_media_desc = offer->GetContentDescriptionByName("audio"); + ASSERT_TRUE(audio_media_desc != NULL); + video_media_desc = offer->GetContentDescriptionByName("video"); + ASSERT_TRUE(video_media_desc != NULL); + EXPECT_EQ(1u, audio_media_desc->cryptos().size()); + EXPECT_EQ(1u, video_media_desc->cryptos().size()); + + audio_trans_desc = offer->GetTransportDescriptionByName("audio"); + ASSERT_TRUE(audio_trans_desc != NULL); + video_trans_desc = offer->GetTransportDescriptionByName("video"); + ASSERT_TRUE(video_trans_desc != NULL); + ASSERT_TRUE(audio_trans_desc->identity_fingerprint.get() != NULL); + ASSERT_TRUE(video_trans_desc->identity_fingerprint.get() != NULL); + + // Generate an answer with only SDES support, since tdf2 has crypto disabled. + answer = f2_.CreateAnswer(offer.get(), options, NULL); + ASSERT_TRUE(answer.get() != NULL); + + audio_media_desc = answer->GetContentDescriptionByName("audio"); + ASSERT_TRUE(audio_media_desc != NULL); + video_media_desc = answer->GetContentDescriptionByName("video"); + ASSERT_TRUE(video_media_desc != NULL); + EXPECT_EQ(1u, audio_media_desc->cryptos().size()); + EXPECT_EQ(1u, video_media_desc->cryptos().size()); + + audio_trans_desc = answer->GetTransportDescriptionByName("audio"); + ASSERT_TRUE(audio_trans_desc != NULL); + video_trans_desc = answer->GetTransportDescriptionByName("video"); + ASSERT_TRUE(video_trans_desc != NULL); + ASSERT_TRUE(audio_trans_desc->identity_fingerprint.get() == NULL); + ASSERT_TRUE(video_trans_desc->identity_fingerprint.get() == NULL); + + // Enable DTLS; the answer should now only have DTLS support. + tdf2_.set_secure(SEC_ENABLED); + answer = f2_.CreateAnswer(offer.get(), options, NULL); + ASSERT_TRUE(answer.get() != NULL); + + audio_media_desc = answer->GetContentDescriptionByName("audio"); + ASSERT_TRUE(audio_media_desc != NULL); + video_media_desc = answer->GetContentDescriptionByName("video"); + ASSERT_TRUE(video_media_desc != NULL); + EXPECT_TRUE(audio_media_desc->cryptos().empty()); + EXPECT_TRUE(video_media_desc->cryptos().empty()); + EXPECT_EQ(cricket::kMediaProtocolSavpf, audio_media_desc->protocol()); + EXPECT_EQ(cricket::kMediaProtocolSavpf, video_media_desc->protocol()); + + audio_trans_desc = answer->GetTransportDescriptionByName("audio"); + ASSERT_TRUE(audio_trans_desc != NULL); + video_trans_desc = answer->GetTransportDescriptionByName("video"); + ASSERT_TRUE(video_trans_desc != NULL); + ASSERT_TRUE(audio_trans_desc->identity_fingerprint.get() != NULL); + ASSERT_TRUE(video_trans_desc->identity_fingerprint.get() != NULL); + + // Try creating offer again. DTLS enabled now, crypto's should be empty + // in new offer. + offer = f1_.CreateOffer(options, offer.get()); + ASSERT_TRUE(offer.get() != NULL); + audio_media_desc = offer->GetContentDescriptionByName("audio"); + ASSERT_TRUE(audio_media_desc != NULL); + video_media_desc = offer->GetContentDescriptionByName("video"); + ASSERT_TRUE(video_media_desc != NULL); + EXPECT_TRUE(audio_media_desc->cryptos().empty()); + EXPECT_TRUE(video_media_desc->cryptos().empty()); + + audio_trans_desc = offer->GetTransportDescriptionByName("audio"); + ASSERT_TRUE(audio_trans_desc != NULL); + video_trans_desc = offer->GetTransportDescriptionByName("video"); + ASSERT_TRUE(video_trans_desc != NULL); + ASSERT_TRUE(audio_trans_desc->identity_fingerprint.get() != NULL); + ASSERT_TRUE(video_trans_desc->identity_fingerprint.get() != NULL); +} + +// Test that an answer can't be created if cryptos are required but the offer is +// unsecure. +TEST_F(MediaSessionDescriptionFactoryTest, TestSecureAnswerToUnsecureOffer) { + MediaSessionOptions options = CreatePlanBMediaSessionOptions(); + f1_.set_secure(SEC_DISABLED); + tdf1_.set_secure(SEC_DISABLED); + f2_.set_secure(SEC_REQUIRED); + tdf1_.set_secure(SEC_ENABLED); + + std::unique_ptr offer = f1_.CreateOffer(options, NULL); + ASSERT_TRUE(offer.get() != NULL); + std::unique_ptr answer = + f2_.CreateAnswer(offer.get(), options, NULL); + EXPECT_TRUE(answer.get() == NULL); +} + // Test that we accept a DTLS offer without SDES and create an appropriate // answer. TEST_F(MediaSessionDescriptionFactoryTest, TestCryptoOfferDtlsButNotSdes) { + f1_.set_secure(SEC_DISABLED); + f2_.set_secure(SEC_ENABLED); + tdf1_.set_secure(SEC_ENABLED); + tdf2_.set_secure(SEC_ENABLED); MediaSessionOptions options; AddAudioVideoSections(RtpTransceiverDirection::kRecvOnly, &options); @@ -3438,6 +3796,13 @@ TEST_F(MediaSessionDescriptionFactoryTest, TestCryptoOfferDtlsButNotSdes) { std::unique_ptr offer = f1_.CreateOffer(options, NULL); ASSERT_TRUE(offer.get() != NULL); + const AudioContentDescription* audio_offer = + GetFirstAudioContentDescription(offer.get()); + ASSERT_TRUE(audio_offer->cryptos().empty()); + const VideoContentDescription* video_offer = + GetFirstVideoContentDescription(offer.get()); + ASSERT_TRUE(video_offer->cryptos().empty()); + const cricket::TransportDescription* audio_offer_trans_desc = offer->GetTransportDescriptionByName("audio"); ASSERT_TRUE(audio_offer_trans_desc->identity_fingerprint.get() != NULL); @@ -3964,10 +4329,14 @@ class MediaProtocolTest : public ::testing::TestWithParam { MAKE_VECTOR(kAudioCodecs2)); f2_.set_video_codecs(MAKE_VECTOR(kVideoCodecs2), MAKE_VECTOR(kVideoCodecs2)); + f1_.set_secure(SEC_ENABLED); + f2_.set_secure(SEC_ENABLED); tdf1_.set_certificate(rtc::RTCCertificate::Create( std::unique_ptr(new rtc::FakeSSLIdentity("id1")))); tdf2_.set_certificate(rtc::RTCCertificate::Create( std::unique_ptr(new rtc::FakeSSLIdentity("id2")))); + tdf1_.set_secure(SEC_ENABLED); + tdf2_.set_secure(SEC_ENABLED); } protected: diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc index de13d4ce02..c02518ce84 100644 --- a/pc/peer_connection.cc +++ b/pc/peer_connection.cc @@ -2634,7 +2634,9 @@ void PeerConnection::ReportRemoteIceCandidateAdded( bool PeerConnection::SrtpRequired() const { RTC_DCHECK_RUN_ON(signaling_thread()); - return dtls_enabled_; + return (dtls_enabled_ || + sdp_handler_->webrtc_session_desc_factory()->SdesPolicy() == + cricket::SEC_REQUIRED); } void PeerConnection::OnTransportControllerGatheringState( diff --git a/pc/peer_connection_crypto_unittest.cc b/pc/peer_connection_crypto_unittest.cc index bb669304f4..c0c328161a 100644 --- a/pc/peer_connection_crypto_unittest.cc +++ b/pc/peer_connection_crypto_unittest.cc @@ -128,6 +128,13 @@ SdpContentPredicate HaveDtlsFingerprint() { }; } +SdpContentPredicate HaveSdesCryptos() { + return [](const cricket::ContentInfo* content, + const cricket::TransportInfo* transport) { + return !content->media_description()->cryptos().empty(); + }; +} + SdpContentPredicate HaveProtocol(const std::string& protocol) { return [protocol](const cricket::ContentInfo* content, const cricket::TransportInfo* transport) { @@ -135,6 +142,22 @@ SdpContentPredicate HaveProtocol(const std::string& protocol) { }; } +SdpContentPredicate HaveSdesGcmCryptos(size_t num_crypto_suites) { + return [num_crypto_suites](const cricket::ContentInfo* content, + const cricket::TransportInfo* transport) { + const auto& cryptos = content->media_description()->cryptos(); + if (cryptos.size() != num_crypto_suites) { + return false; + } + for (size_t i = 0; i < cryptos.size(); ++i) { + if (cryptos[i].key_params.size() == 67U && + cryptos[i].cipher_suite == "AEAD_AES_256_GCM") + return true; + } + return false; + }; +} + class PeerConnectionCryptoTest : public PeerConnectionCryptoBaseTest, public ::testing::WithParamInterface { @@ -142,13 +165,20 @@ class PeerConnectionCryptoTest PeerConnectionCryptoTest() : PeerConnectionCryptoBaseTest(GetParam()) {} }; +SdpContentMutator RemoveSdesCryptos() { + return [](cricket::ContentInfo* content, cricket::TransportInfo* transport) { + content->media_description()->set_cryptos({}); + }; +} + SdpContentMutator RemoveDtlsFingerprint() { return [](cricket::ContentInfo* content, cricket::TransportInfo* transport) { transport->description.identity_fingerprint.reset(); }; } -// When DTLS is enabled, the SDP offer/answer should have a DTLS fingerprint. +// When DTLS is enabled, the SDP offer/answer should have a DTLS fingerprint and +// no SDES cryptos. TEST_P(PeerConnectionCryptoTest, CorrectCryptoInOfferWhenDtlsEnabled) { RTCConfiguration config; auto caller = CreatePeerConnectionWithAudioVideo(config); @@ -158,6 +188,7 @@ TEST_P(PeerConnectionCryptoTest, CorrectCryptoInOfferWhenDtlsEnabled) { ASSERT_FALSE(offer->description()->contents().empty()); EXPECT_TRUE(SdpContentsAll(HaveDtlsFingerprint(), offer->description())); + EXPECT_TRUE(SdpContentsNone(HaveSdesCryptos(), offer->description())); EXPECT_TRUE(SdpContentsAll(HaveProtocol(cricket::kMediaProtocolDtlsSavpf), offer->description())); } @@ -172,6 +203,7 @@ TEST_P(PeerConnectionCryptoTest, CorrectCryptoInAnswerWhenDtlsEnabled) { ASSERT_FALSE(answer->description()->contents().empty()); EXPECT_TRUE(SdpContentsAll(HaveDtlsFingerprint(), answer->description())); + EXPECT_TRUE(SdpContentsNone(HaveSdesCryptos(), answer->description())); EXPECT_TRUE(SdpContentsAll(HaveProtocol(cricket::kMediaProtocolDtlsSavpf), answer->description())); } @@ -190,6 +222,7 @@ TEST_P(PeerConnectionCryptoTest, CorrectCryptoInOfferWhenEncryptionDisabled) { ASSERT_TRUE(offer); ASSERT_FALSE(offer->description()->contents().empty()); + EXPECT_TRUE(SdpContentsNone(HaveSdesCryptos(), offer->description())); EXPECT_TRUE(SdpContentsNone(HaveDtlsFingerprint(), offer->description())); EXPECT_TRUE(SdpContentsAll(HaveProtocol(cricket::kMediaProtocolAvpf), offer->description())); @@ -208,6 +241,7 @@ TEST_P(PeerConnectionCryptoTest, CorrectCryptoInAnswerWhenEncryptionDisabled) { ASSERT_TRUE(answer); ASSERT_FALSE(answer->description()->contents().empty()); + EXPECT_TRUE(SdpContentsNone(HaveSdesCryptos(), answer->description())); EXPECT_TRUE(SdpContentsNone(HaveDtlsFingerprint(), answer->description())); EXPECT_TRUE(SdpContentsAll(HaveProtocol(cricket::kMediaProtocolAvpf), answer->description())); diff --git a/pc/peer_connection_interface_unittest.cc b/pc/peer_connection_interface_unittest.cc index 4731cc8090..54522388a2 100644 --- a/pc/peer_connection_interface_unittest.cc +++ b/pc/peer_connection_interface_unittest.cc @@ -2077,13 +2077,10 @@ TEST_P(PeerConnectionInterfaceTest, ReceiveFireFoxOffer) { #endif } -// Test that SDP containing both a=fingerprint and a=crypto is handled -// by ignoring the a=crypto part. -// Prior to 2017, such an SDP would be accepted with SDES crypto, but -// the fallback was removed. -// Prior to 2021, such an SDP would be rejected because of the mixture. -// Post 2021, a=crypto lines are totally ignored by the SDP parser. -TEST_P(PeerConnectionInterfaceTest, SdesIgnored) { +// Test that fallback from DTLS to SDES is not supported. +// The fallback was previously supported but was removed to simplify the code +// and because it's non-standard. +TEST_P(PeerConnectionInterfaceTest, DtlsSdesFallbackNotSupported) { RTCConfiguration rtc_config; CreatePeerConnection(rtc_config); // Wait for fake certificate to be generated. Previously, this is what caused @@ -2096,7 +2093,7 @@ TEST_P(PeerConnectionInterfaceTest, SdesIgnored) { std::unique_ptr desc( webrtc::CreateSessionDescription(SdpType::kOffer, kDtlsSdesFallbackSdp, nullptr)); - EXPECT_TRUE(DoSetSessionDescription(std::move(desc), /*local=*/false)); + EXPECT_FALSE(DoSetSessionDescription(std::move(desc), /*local=*/false)); } // Test that we can create an audio only offer and receive an answer with a diff --git a/pc/peer_connection_signaling_unittest.cc b/pc/peer_connection_signaling_unittest.cc index 1dbbe7bc09..13b54d995d 100644 --- a/pc/peer_connection_signaling_unittest.cc +++ b/pc/peer_connection_signaling_unittest.cc @@ -852,9 +852,6 @@ TEST_P(PeerConnectionSignalingTest, UnsupportedContentType) { "s=-\r\n" "t=0 0\r\n" "m=bogus 9 FOO 0 8\r\n" - "a=fingerprint:sha-256 " - "D8:6C:3D:FA:23:E2:2C:63:11:2D:D0:86:BE:C4:D0:65:F9:42:F7:1C:06:04:27:E6:" - "1C:2C:74:01:8D:50:67:23\r\n" "c=IN IP4 0.0.0.0\r\n" "a=mid:bogusmid\r\n"; std::unique_ptr remote_description = @@ -864,7 +861,6 @@ TEST_P(PeerConnectionSignalingTest, UnsupportedContentType) { // Assert we respond back with something meaningful. auto answer = caller->CreateAnswer(); - ASSERT_TRUE(answer); ASSERT_EQ(answer->description()->contents().size(), 1u); EXPECT_NE(answer->description() ->contents()[0] diff --git a/pc/sdp_offer_answer.cc b/pc/sdp_offer_answer.cc index 2d0c027a85..1795cde540 100644 --- a/pc/sdp_offer_answer.cc +++ b/pc/sdp_offer_answer.cc @@ -108,6 +108,7 @@ const char kSdpWithoutIceUfragPwd[] = "Called with SDP without ice-ufrag and ice-pwd."; const char kSdpWithoutDtlsFingerprint[] = "Called with SDP without DTLS fingerprint."; +const char kSdpWithoutSdesCrypto[] = "Called with SDP without SDES crypto."; const char kSessionError[] = "Session error code: "; const char kSessionErrorDesc[] = "Session error description: "; @@ -345,13 +346,14 @@ bool MediaSectionsHaveSameCount(const SessionDescription& desc1, const SessionDescription& desc2) { return desc1.contents().size() == desc2.contents().size(); } -// Checks that each non-rejected content has a DTLS +// Checks that each non-rejected content has SDES crypto keys or a DTLS // fingerprint, unless it's in a BUNDLE group, in which case only the // BUNDLE-tag section (first media section/description in the BUNDLE group) // needs a ufrag and pwd. Mismatches, such as replying with a DTLS fingerprint // to SDES keys, will be caught in JsepTransport negotiation, and backstopped // by Channel's `srtp_required` check. RTCError VerifyCrypto(const SessionDescription* desc, + bool dtls_enabled, const std::map& bundle_groups_by_mid) { for (const cricket::ContentInfo& content_info : desc->contents()) { @@ -359,8 +361,8 @@ RTCError VerifyCrypto(const SessionDescription* desc, continue; } // Note what media is used with each crypto protocol, for all sections. - // We now support only DTLS, so this metric can be retired when expiring. - NoteKeyProtocolAndMedia(webrtc::kEnumCounterKeyProtocolDtls, + NoteKeyProtocolAndMedia(dtls_enabled ? webrtc::kEnumCounterKeyProtocolDtls + : webrtc::kEnumCounterKeyProtocolSdes, content_info.media_description()->type()); const std::string& mid = content_info.name; auto it = bundle_groups_by_mid.find(mid); @@ -381,10 +383,20 @@ RTCError VerifyCrypto(const SessionDescription* desc, // Something is not right. LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER, kInvalidSdp); } - if (!tinfo->description.identity_fingerprint) { - RTC_LOG(LS_WARNING) << "Session description must have DTLS fingerprint"; - return RTCError(RTCErrorType::INVALID_PARAMETER, - kSdpWithoutDtlsFingerprint); + if (dtls_enabled) { + if (!tinfo->description.identity_fingerprint) { + RTC_LOG(LS_WARNING) + << "Session description must have DTLS fingerprint if " + "DTLS enabled."; + return RTCError(RTCErrorType::INVALID_PARAMETER, + kSdpWithoutDtlsFingerprint); + } + } else { + if (media->cryptos().empty()) { + RTC_LOG(LS_WARNING) + << "Session description must have SDES when DTLS disabled."; + return RTCError(RTCErrorType::INVALID_PARAMETER, kSdpWithoutSdesCrypto); + } } } return RTCError::OK(); @@ -988,6 +1000,10 @@ void SdpOfferAnswerHandler::Initialize( transport_controller()->SetLocalCertificate(certificate); }); + if (pc_->options()->disable_encryption) { + webrtc_session_desc_factory_->SetSdesPolicy(cricket::SEC_DISABLED); + } + webrtc_session_desc_factory_->set_enable_encrypted_rtp_header_extensions( pc_->GetCryptoOptions().srtp.enable_encrypted_rtp_header_extensions); webrtc_session_desc_factory_->set_is_unified_plan(IsUnifiedPlan()); @@ -3025,9 +3041,10 @@ RTCError SdpOfferAnswerHandler::ValidateSessionDescription( // Verify crypto settings. std::string crypto_error; - if (pc_->dtls_enabled()) { - RTCError crypto_error = - VerifyCrypto(sdesc->description(), bundle_groups_by_mid); + if (webrtc_session_desc_factory_->SdesPolicy() == cricket::SEC_REQUIRED || + pc_->dtls_enabled()) { + RTCError crypto_error = VerifyCrypto( + sdesc->description(), pc_->dtls_enabled(), bundle_groups_by_mid); if (!crypto_error.ok()) { return crypto_error; } diff --git a/pc/session_description.h b/pc/session_description.h index db393abb19..ee7a91c84c 100644 --- a/pc/session_description.h +++ b/pc/session_description.h @@ -22,6 +22,7 @@ #include #include "absl/memory/memory.h" +#include "api/crypto_params.h" #include "api/media_types.h" #include "api/rtp_parameters.h" #include "api/rtp_transceiver_direction.h" @@ -43,6 +44,7 @@ namespace cricket { typedef std::vector AudioCodecs; typedef std::vector VideoCodecs; +typedef std::vector CryptoParamsVec; typedef std::vector RtpHeaderExtensions; // Options to control how session descriptions are generated. @@ -126,6 +128,14 @@ class MediaContentDescription { bandwidth_type_ = bandwidth_type; } + virtual const std::vector& cryptos() const { return cryptos_; } + virtual void AddCrypto(const CryptoParams& params) { + cryptos_.push_back(params); + } + virtual void set_cryptos(const std::vector& cryptos) { + cryptos_ = cryptos; + } + // List of RTP header extensions. URIs are **NOT** guaranteed to be unique // as they can appear twice when both encrypted and non-encrypted extensions // are present. @@ -249,6 +259,7 @@ class MediaContentDescription { int bandwidth_ = kAutoBandwidth; std::string bandwidth_type_ = kApplicationSpecificBandwidth; std::string protocol_; + std::vector cryptos_; std::vector rtp_header_extensions_; bool rtp_header_extensions_set_ = false; StreamParamsVec send_streams_; diff --git a/pc/srtp_filter.cc b/pc/srtp_filter.cc new file mode 100644 index 0000000000..c48dfdb4cd --- /dev/null +++ b/pc/srtp_filter.cc @@ -0,0 +1,280 @@ +/* + * Copyright 2009 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "pc/srtp_filter.h" + +#include +#include +#include + +#include "absl/strings/match.h" +#include "rtc_base/logging.h" +#include "rtc_base/ssl_stream_adapter.h" +#include "rtc_base/third_party/base64/base64.h" +#include "rtc_base/zero_memory.h" + +namespace cricket { + +SrtpFilter::SrtpFilter() {} + +SrtpFilter::~SrtpFilter() {} + +bool SrtpFilter::IsActive() const { + return state_ >= ST_ACTIVE; +} + +bool SrtpFilter::Process(const std::vector& cryptos, + webrtc::SdpType type, + ContentSource source) { + bool ret = false; + switch (type) { + case webrtc::SdpType::kOffer: + ret = SetOffer(cryptos, source); + break; + case webrtc::SdpType::kPrAnswer: + ret = SetProvisionalAnswer(cryptos, source); + break; + case webrtc::SdpType::kAnswer: + ret = SetAnswer(cryptos, source); + break; + default: + break; + } + + if (!ret) { + return false; + } + + return true; +} + +bool SrtpFilter::SetOffer(const std::vector& offer_params, + ContentSource source) { + if (!ExpectOffer(source)) { + RTC_LOG(LS_ERROR) << "Wrong state to update SRTP offer"; + return false; + } + return StoreParams(offer_params, source); +} + +bool SrtpFilter::SetAnswer(const std::vector& answer_params, + ContentSource source) { + return DoSetAnswer(answer_params, source, true); +} + +bool SrtpFilter::SetProvisionalAnswer( + const std::vector& answer_params, + ContentSource source) { + return DoSetAnswer(answer_params, source, false); +} + +bool SrtpFilter::ExpectOffer(ContentSource source) { + return ((state_ == ST_INIT) || (state_ == ST_ACTIVE) || + (state_ == ST_SENTOFFER && source == CS_LOCAL) || + (state_ == ST_SENTUPDATEDOFFER && source == CS_LOCAL) || + (state_ == ST_RECEIVEDOFFER && source == CS_REMOTE) || + (state_ == ST_RECEIVEDUPDATEDOFFER && source == CS_REMOTE)); +} + +bool SrtpFilter::StoreParams(const std::vector& params, + ContentSource source) { + offer_params_ = params; + if (state_ == ST_INIT) { + state_ = (source == CS_LOCAL) ? ST_SENTOFFER : ST_RECEIVEDOFFER; + } else if (state_ == ST_ACTIVE) { + state_ = + (source == CS_LOCAL) ? ST_SENTUPDATEDOFFER : ST_RECEIVEDUPDATEDOFFER; + } + return true; +} + +bool SrtpFilter::ExpectAnswer(ContentSource source) { + return ((state_ == ST_SENTOFFER && source == CS_REMOTE) || + (state_ == ST_RECEIVEDOFFER && source == CS_LOCAL) || + (state_ == ST_SENTUPDATEDOFFER && source == CS_REMOTE) || + (state_ == ST_RECEIVEDUPDATEDOFFER && source == CS_LOCAL) || + (state_ == ST_SENTPRANSWER_NO_CRYPTO && source == CS_LOCAL) || + (state_ == ST_SENTPRANSWER && source == CS_LOCAL) || + (state_ == ST_RECEIVEDPRANSWER_NO_CRYPTO && source == CS_REMOTE) || + (state_ == ST_RECEIVEDPRANSWER && source == CS_REMOTE)); +} + +bool SrtpFilter::DoSetAnswer(const std::vector& answer_params, + ContentSource source, + bool final) { + if (!ExpectAnswer(source)) { + RTC_LOG(LS_ERROR) << "Invalid state for SRTP answer"; + return false; + } + + // If the answer doesn't requests crypto complete the negotiation of an + // unencrypted session. + // Otherwise, finalize the parameters and apply them. + if (answer_params.empty()) { + if (final) { + return ResetParams(); + } else { + // Need to wait for the final answer to decide if + // we should go to Active state. + state_ = (source == CS_LOCAL) ? ST_SENTPRANSWER_NO_CRYPTO + : ST_RECEIVEDPRANSWER_NO_CRYPTO; + return true; + } + } + CryptoParams selected_params; + if (!NegotiateParams(answer_params, &selected_params)) + return false; + + const CryptoParams& new_send_params = + (source == CS_REMOTE) ? selected_params : answer_params[0]; + const CryptoParams& new_recv_params = + (source == CS_REMOTE) ? answer_params[0] : selected_params; + if (!ApplySendParams(new_send_params) || !ApplyRecvParams(new_recv_params)) { + return false; + } + applied_send_params_ = new_send_params; + applied_recv_params_ = new_recv_params; + + if (final) { + offer_params_.clear(); + state_ = ST_ACTIVE; + } else { + state_ = (source == CS_LOCAL) ? ST_SENTPRANSWER : ST_RECEIVEDPRANSWER; + } + return true; +} + +bool SrtpFilter::NegotiateParams(const std::vector& answer_params, + CryptoParams* selected_params) { + // We're processing an accept. We should have exactly one set of params, + // unless the offer didn't mention crypto, in which case we shouldn't be here. + bool ret = (answer_params.size() == 1U && !offer_params_.empty()); + if (ret) { + // We should find a match between the answer params and the offered params. + std::vector::const_iterator it; + for (it = offer_params_.begin(); it != offer_params_.end(); ++it) { + if (answer_params[0].Matches(*it)) { + break; + } + } + + if (it != offer_params_.end()) { + *selected_params = *it; + } else { + ret = false; + } + } + + if (!ret) { + RTC_LOG(LS_WARNING) << "Invalid parameters in SRTP answer"; + } + return ret; +} + +bool SrtpFilter::ResetParams() { + offer_params_.clear(); + applied_send_params_ = CryptoParams(); + applied_recv_params_ = CryptoParams(); + send_cipher_suite_ = absl::nullopt; + recv_cipher_suite_ = absl::nullopt; + send_key_.Clear(); + recv_key_.Clear(); + state_ = ST_INIT; + return true; +} + +bool SrtpFilter::ApplySendParams(const CryptoParams& send_params) { + if (applied_send_params_.cipher_suite == send_params.cipher_suite && + applied_send_params_.key_params == send_params.key_params) { + RTC_LOG(LS_INFO) << "Applying the same SRTP send parameters again. No-op."; + + // We do not want to reset the ROC if the keys are the same. So just return. + return true; + } + + send_cipher_suite_ = rtc::SrtpCryptoSuiteFromName(send_params.cipher_suite); + if (send_cipher_suite_ == rtc::kSrtpInvalidCryptoSuite) { + RTC_LOG(LS_WARNING) << "Unknown crypto suite(s) received:" + " send cipher_suite " + << send_params.cipher_suite; + return false; + } + + int send_key_len, send_salt_len; + if (!rtc::GetSrtpKeyAndSaltLengths(*send_cipher_suite_, &send_key_len, + &send_salt_len)) { + RTC_LOG(LS_ERROR) << "Could not get lengths for crypto suite(s):" + " send cipher_suite " + << send_params.cipher_suite; + return false; + } + + send_key_ = rtc::ZeroOnFreeBuffer(send_key_len + send_salt_len); + return ParseKeyParams(send_params.key_params, send_key_.data(), + send_key_.size()); +} + +bool SrtpFilter::ApplyRecvParams(const CryptoParams& recv_params) { + if (applied_recv_params_.cipher_suite == recv_params.cipher_suite && + applied_recv_params_.key_params == recv_params.key_params) { + RTC_LOG(LS_INFO) << "Applying the same SRTP recv parameters again. No-op."; + + // We do not want to reset the ROC if the keys are the same. So just return. + return true; + } + + recv_cipher_suite_ = rtc::SrtpCryptoSuiteFromName(recv_params.cipher_suite); + if (recv_cipher_suite_ == rtc::kSrtpInvalidCryptoSuite) { + RTC_LOG(LS_WARNING) << "Unknown crypto suite(s) received:" + " recv cipher_suite " + << recv_params.cipher_suite; + return false; + } + + int recv_key_len, recv_salt_len; + if (!rtc::GetSrtpKeyAndSaltLengths(*recv_cipher_suite_, &recv_key_len, + &recv_salt_len)) { + RTC_LOG(LS_ERROR) << "Could not get lengths for crypto suite(s):" + " recv cipher_suite " + << recv_params.cipher_suite; + return false; + } + + recv_key_ = rtc::ZeroOnFreeBuffer(recv_key_len + recv_salt_len); + return ParseKeyParams(recv_params.key_params, recv_key_.data(), + recv_key_.size()); +} + +bool SrtpFilter::ParseKeyParams(const std::string& key_params, + uint8_t* key, + size_t len) { + // example key_params: "inline:YUJDZGVmZ2hpSktMbW9QUXJzVHVWd3l6MTIzNDU2" + + // Fail if key-method is wrong. + if (!absl::StartsWith(key_params, "inline:")) { + return false; + } + + // Fail if base64 decode fails, or the key is the wrong size. + std::string key_b64(key_params.substr(7)), key_str; + if (!rtc::Base64::Decode(key_b64, rtc::Base64::DO_STRICT, &key_str, + nullptr) || + key_str.size() != len) { + return false; + } + + memcpy(key, key_str.c_str(), len); + // TODO(bugs.webrtc.org/8905): Switch to ZeroOnFreeBuffer for storing + // sensitive data. + rtc::ExplicitZeroMemory(&key_str[0], key_str.size()); + return true; +} + +} // namespace cricket diff --git a/pc/srtp_filter.h b/pc/srtp_filter.h new file mode 100644 index 0000000000..f1e164936c --- /dev/null +++ b/pc/srtp_filter.h @@ -0,0 +1,148 @@ +/* + * Copyright 2009 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef PC_SRTP_FILTER_H_ +#define PC_SRTP_FILTER_H_ + +#include +#include + +#include +#include +#include +#include +#include + +#include "absl/types/optional.h" +#include "api/array_view.h" +#include "api/crypto_params.h" +#include "api/jsep.h" +#include "api/sequence_checker.h" +#include "pc/session_description.h" +#include "rtc_base/buffer.h" +#include "rtc_base/constructor_magic.h" +#include "rtc_base/ssl_stream_adapter.h" + +// Forward declaration to avoid pulling in libsrtp headers here +struct srtp_event_data_t; +struct srtp_ctx_t_; + +namespace cricket { + +// A helper class used to negotiate SDES crypto params. +// TODO(zhihuang): Find a better name for this class, like "SdesNegotiator". +class SrtpFilter { + public: + enum Mode { PROTECT, UNPROTECT }; + enum Error { + ERROR_NONE, + ERROR_FAIL, + ERROR_AUTH, + ERROR_REPLAY, + }; + + SrtpFilter(); + ~SrtpFilter(); + + // Whether the filter is active (i.e. crypto has been properly negotiated). + bool IsActive() const; + + // Handle the offer/answer negotiation of the crypto parameters internally. + // TODO(zhihuang): Make SetOffer/ProvisionalAnswer/Answer private as helper + // methods once start using Process. + bool Process(const std::vector& cryptos, + webrtc::SdpType type, + ContentSource source); + + // Indicates which crypto algorithms and keys were contained in the offer. + // offer_params should contain a list of available parameters to use, or none, + // if crypto is not desired. This must be called before SetAnswer. + bool SetOffer(const std::vector& offer_params, + ContentSource source); + // Same as SetAnwer. But multiple calls are allowed to SetProvisionalAnswer + // after a call to SetOffer. + bool SetProvisionalAnswer(const std::vector& answer_params, + ContentSource source); + // Indicates which crypto algorithms and keys were contained in the answer. + // answer_params should contain the negotiated parameters, which may be none, + // if crypto was not desired or could not be negotiated (and not required). + // This must be called after SetOffer. If crypto negotiation completes + // successfully, this will advance the filter to the active state. + bool SetAnswer(const std::vector& answer_params, + ContentSource source); + + bool ResetParams(); + + static bool ParseKeyParams(const std::string& params, + uint8_t* key, + size_t len); + + absl::optional send_cipher_suite() { return send_cipher_suite_; } + absl::optional recv_cipher_suite() { return recv_cipher_suite_; } + + rtc::ArrayView send_key() { return send_key_; } + rtc::ArrayView recv_key() { return recv_key_; } + + protected: + bool ExpectOffer(ContentSource source); + + bool StoreParams(const std::vector& params, + ContentSource source); + + bool ExpectAnswer(ContentSource source); + + bool DoSetAnswer(const std::vector& answer_params, + ContentSource source, + bool final); + + bool NegotiateParams(const std::vector& answer_params, + CryptoParams* selected_params); + + private: + bool ApplySendParams(const CryptoParams& send_params); + + bool ApplyRecvParams(const CryptoParams& recv_params); + + enum State { + ST_INIT, // SRTP filter unused. + ST_SENTOFFER, // Offer with SRTP parameters sent. + ST_RECEIVEDOFFER, // Offer with SRTP parameters received. + ST_SENTPRANSWER_NO_CRYPTO, // Sent provisional answer without crypto. + // Received provisional answer without crypto. + ST_RECEIVEDPRANSWER_NO_CRYPTO, + ST_ACTIVE, // Offer and answer set. + // SRTP filter is active but new parameters are offered. + // When the answer is set, the state transitions to ST_ACTIVE or ST_INIT. + ST_SENTUPDATEDOFFER, + // SRTP filter is active but new parameters are received. + // When the answer is set, the state transitions back to ST_ACTIVE. + ST_RECEIVEDUPDATEDOFFER, + // SRTP filter is active but the sent answer is only provisional. + // When the final answer is set, the state transitions to ST_ACTIVE or + // ST_INIT. + ST_SENTPRANSWER, + // SRTP filter is active but the received answer is only provisional. + // When the final answer is set, the state transitions to ST_ACTIVE or + // ST_INIT. + ST_RECEIVEDPRANSWER + }; + State state_ = ST_INIT; + std::vector offer_params_; + CryptoParams applied_send_params_; + CryptoParams applied_recv_params_; + absl::optional send_cipher_suite_; + absl::optional recv_cipher_suite_; + rtc::ZeroOnFreeBuffer send_key_; + rtc::ZeroOnFreeBuffer recv_key_; +}; + +} // namespace cricket + +#endif // PC_SRTP_FILTER_H_ diff --git a/pc/srtp_filter_unittest.cc b/pc/srtp_filter_unittest.cc new file mode 100644 index 0000000000..eadaad68af --- /dev/null +++ b/pc/srtp_filter_unittest.cc @@ -0,0 +1,472 @@ +/* + * Copyright 2004 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "pc/srtp_filter.h" + +#include + +#include "api/crypto_params.h" +#include "rtc_base/ssl_stream_adapter.h" +#include "test/gtest.h" + +using cricket::CryptoParams; +using cricket::CS_LOCAL; +using cricket::CS_REMOTE; + +namespace rtc { + +static const char kTestKeyParams1[] = + "inline:WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz"; +static const char kTestKeyParams2[] = + "inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR"; +static const char kTestKeyParams3[] = + "inline:1234X19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz"; +static const char kTestKeyParams4[] = + "inline:4567QCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR"; +static const char kTestKeyParamsGcm1[] = + "inline:e166KFlKzJsGW0d5apX+rrI05vxbrvMJEzFI14aTDCa63IRTlLK4iH66uOI="; +static const char kTestKeyParamsGcm2[] = + "inline:6X0oCd55zfz4VgtOwsuqcFq61275PDYN5uwuu3p7ZUHbfUY2FMpdP4m2PEo="; +static const char kTestKeyParamsGcm3[] = + "inline:YKlABGZWMgX32xuMotrG0v0T7G83veegaVzubQ=="; +static const char kTestKeyParamsGcm4[] = + "inline:gJ6tWoUym2v+/F6xjr7xaxiS3QbJJozl3ZD/0A=="; +static const cricket::CryptoParams kTestCryptoParams1(1, + "AES_CM_128_HMAC_SHA1_80", + kTestKeyParams1, + ""); +static const cricket::CryptoParams kTestCryptoParams2(1, + "AES_CM_128_HMAC_SHA1_80", + kTestKeyParams2, + ""); +static const cricket::CryptoParams kTestCryptoParamsGcm1(1, + "AEAD_AES_256_GCM", + kTestKeyParamsGcm1, + ""); +static const cricket::CryptoParams kTestCryptoParamsGcm2(1, + "AEAD_AES_256_GCM", + kTestKeyParamsGcm2, + ""); +static const cricket::CryptoParams kTestCryptoParamsGcm3(1, + "AEAD_AES_128_GCM", + kTestKeyParamsGcm3, + ""); +static const cricket::CryptoParams kTestCryptoParamsGcm4(1, + "AEAD_AES_128_GCM", + kTestKeyParamsGcm4, + ""); + +class SrtpFilterTest : public ::testing::Test { + protected: + SrtpFilterTest() {} + static std::vector MakeVector(const CryptoParams& params) { + std::vector vec; + vec.push_back(params); + return vec; + } + + void TestSetParams(const std::vector& params1, + const std::vector& params2) { + EXPECT_TRUE(f1_.SetOffer(params1, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(params1, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_FALSE(f2_.IsActive()); + EXPECT_TRUE(f2_.SetAnswer(params2, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(params2, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + } + + void VerifyKeysAreEqual(ArrayView key1, + ArrayView key2) { + EXPECT_EQ(key1.size(), key2.size()); + EXPECT_EQ(0, memcmp(key1.data(), key2.data(), key1.size())); + } + + void VerifyCryptoParamsMatch(const std::string& cs1, const std::string& cs2) { + EXPECT_EQ(rtc::SrtpCryptoSuiteFromName(cs1), f1_.send_cipher_suite()); + EXPECT_EQ(rtc::SrtpCryptoSuiteFromName(cs2), f2_.send_cipher_suite()); + VerifyKeysAreEqual(f1_.send_key(), f2_.recv_key()); + VerifyKeysAreEqual(f2_.send_key(), f1_.recv_key()); + } + + cricket::SrtpFilter f1_; + cricket::SrtpFilter f2_; +}; + +// Test that we can set up the session and keys properly. +TEST_F(SrtpFilterTest, TestGoodSetupOneCipherSuite) { + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); +} + +TEST_F(SrtpFilterTest, TestGoodSetupOneCipherSuiteGcm) { + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParamsGcm1), CS_LOCAL)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParamsGcm2), CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); +} + +// Test that we can set up things with multiple params. +TEST_F(SrtpFilterTest, TestGoodSetupMultipleCipherSuites) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + offer.push_back(kTestCryptoParams1); + offer[1].tag = 2; + offer[1].cipher_suite = kCsAesCm128HmacSha1_32; + answer[0].tag = 2; + answer[0].cipher_suite = kCsAesCm128HmacSha1_32; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); +} + +TEST_F(SrtpFilterTest, TestGoodSetupMultipleCipherSuitesGcm) { + std::vector offer(MakeVector(kTestCryptoParamsGcm1)); + std::vector answer(MakeVector(kTestCryptoParamsGcm3)); + offer.push_back(kTestCryptoParamsGcm4); + offer[1].tag = 2; + answer[0].tag = 2; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); +} + +// Test that we handle the cases where crypto is not desired. +TEST_F(SrtpFilterTest, TestGoodSetupNoCipherSuites) { + std::vector offer, answer; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we handle the cases where crypto is not desired by the remote side. +TEST_F(SrtpFilterTest, TestGoodSetupNoAnswerCipherSuites) { + std::vector answer; + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail if we call the functions the wrong way. +TEST_F(SrtpFilterTest, TestBadSetup) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_LOCAL)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we can set offer multiple times from the same source. +TEST_F(SrtpFilterTest, TestGoodSetupMultipleOffers) { + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams2), CS_LOCAL)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams2), CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams1), CS_REMOTE)); + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_FALSE(f2_.IsActive()); + EXPECT_TRUE(f2_.SetAnswer(MakeVector(kTestCryptoParams2), CS_LOCAL)); + EXPECT_TRUE(f2_.IsActive()); + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams1), CS_REMOTE)); + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_TRUE(f2_.SetAnswer(MakeVector(kTestCryptoParams2), CS_LOCAL)); +} +// Test that we can't set offer multiple times from different sources. +TEST_F(SrtpFilterTest, TestBadSetupMultipleOffers) { + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_FALSE(f1_.SetOffer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParams1), CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams2), CS_LOCAL)); + EXPECT_FALSE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_REMOTE)); + EXPECT_TRUE(f1_.SetAnswer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_FALSE(f2_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_FALSE(f2_.IsActive()); + EXPECT_TRUE(f2_.SetAnswer(MakeVector(kTestCryptoParams2), CS_LOCAL)); + EXPECT_TRUE(f2_.IsActive()); + EXPECT_TRUE(f2_.SetOffer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_FALSE(f2_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_TRUE(f2_.SetAnswer(MakeVector(kTestCryptoParams2), CS_LOCAL)); +} + +// Test that we fail if we have params in the answer when none were offered. +TEST_F(SrtpFilterTest, TestNoAnswerCipherSuites) { + std::vector offer; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(MakeVector(kTestCryptoParams2), CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail if we have too many params in our answer. +TEST_F(SrtpFilterTest, TestMultipleAnswerCipherSuites) { + std::vector answer(MakeVector(kTestCryptoParams2)); + answer.push_back(kTestCryptoParams2); + answer[1].tag = 2; + answer[1].cipher_suite = kCsAesCm128HmacSha1_32; + EXPECT_TRUE(f1_.SetOffer(MakeVector(kTestCryptoParams1), CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail if we don't support the cipher-suite. +TEST_F(SrtpFilterTest, TestInvalidCipherSuite) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + offer[0].cipher_suite = answer[0].cipher_suite = "FOO"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail if we can't agree on a tag. +TEST_F(SrtpFilterTest, TestNoMatchingTag) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].tag = 99; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail if we can't agree on a cipher-suite. +TEST_F(SrtpFilterTest, TestNoMatchingCipherSuite) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].tag = 2; + answer[0].cipher_suite = "FOO"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail keys with bad base64 content. +TEST_F(SrtpFilterTest, TestInvalidKeyData) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].key_params = "inline:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail keys with the wrong key-method. +TEST_F(SrtpFilterTest, TestWrongKeyMethod) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].key_params = "outline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail keys of the wrong length. +TEST_F(SrtpFilterTest, TestKeyTooShort) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].key_params = "inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtx"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail keys of the wrong length. +TEST_F(SrtpFilterTest, TestKeyTooLong) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].key_params = "inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBRABCD"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we fail keys with lifetime or MKI set (since we don't support) +TEST_F(SrtpFilterTest, TestUnsupportedOptions) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + answer[0].key_params = + "inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4"; + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); +} + +// Test that we can encrypt/decrypt after negotiating AES_CM_128_HMAC_SHA1_80. +TEST_F(SrtpFilterTest, TestProtect_AES_CM_128_HMAC_SHA1_80) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + offer.push_back(kTestCryptoParams1); + offer[1].tag = 2; + offer[1].cipher_suite = kCsAesCm128HmacSha1_32; + TestSetParams(offer, answer); + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_80, kCsAesCm128HmacSha1_80); +} + +// Test that we can encrypt/decrypt after negotiating AES_CM_128_HMAC_SHA1_32. +TEST_F(SrtpFilterTest, TestProtect_AES_CM_128_HMAC_SHA1_32) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + offer.push_back(kTestCryptoParams1); + offer[1].tag = 2; + offer[1].cipher_suite = kCsAesCm128HmacSha1_32; + answer[0].tag = 2; + answer[0].cipher_suite = kCsAesCm128HmacSha1_32; + TestSetParams(offer, answer); + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_32, kCsAesCm128HmacSha1_32); +} + +// Test that we can change encryption parameters. +TEST_F(SrtpFilterTest, TestChangeParameters) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + + TestSetParams(offer, answer); + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_80, kCsAesCm128HmacSha1_80); + + // Change the key parameters and cipher_suite. + offer[0].key_params = kTestKeyParams3; + offer[0].cipher_suite = kCsAesCm128HmacSha1_32; + answer[0].key_params = kTestKeyParams4; + answer[0].cipher_suite = kCsAesCm128HmacSha1_32; + + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f1_.IsActive()); + + // Test that the old keys are valid until the negotiation is complete. + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_80, kCsAesCm128HmacSha1_80); + + // Complete the negotiation and test that we can still understand each other. + EXPECT_TRUE(f2_.SetAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_32, kCsAesCm128HmacSha1_32); +} + +// Test that we can send and receive provisional answers with crypto enabled. +// Also test that we can change the crypto. +TEST_F(SrtpFilterTest, TestProvisionalAnswer) { + std::vector offer(MakeVector(kTestCryptoParams1)); + offer.push_back(kTestCryptoParams1); + offer[1].tag = 2; + offer[1].cipher_suite = kCsAesCm128HmacSha1_32; + std::vector answer(MakeVector(kTestCryptoParams2)); + + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_FALSE(f2_.IsActive()); + EXPECT_TRUE(f2_.SetProvisionalAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetProvisionalAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_80, kCsAesCm128HmacSha1_80); + + answer[0].key_params = kTestKeyParams4; + answer[0].tag = 2; + answer[0].cipher_suite = kCsAesCm128HmacSha1_32; + EXPECT_TRUE(f2_.SetAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_32, kCsAesCm128HmacSha1_32); +} + +// Test that a provisional answer doesn't need to contain a crypto. +TEST_F(SrtpFilterTest, TestProvisionalAnswerWithoutCrypto) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer; + + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_FALSE(f2_.IsActive()); + EXPECT_TRUE(f2_.SetProvisionalAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetProvisionalAnswer(answer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_FALSE(f2_.IsActive()); + + answer.push_back(kTestCryptoParams2); + EXPECT_TRUE(f2_.SetAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_80, kCsAesCm128HmacSha1_80); +} + +// Test that if we get a new local offer after a provisional answer +// with no crypto, that we are in an inactive state. +TEST_F(SrtpFilterTest, TestLocalOfferAfterProvisionalAnswerWithoutCrypto) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer; + + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_TRUE(f1_.SetProvisionalAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f2_.SetProvisionalAnswer(answer, CS_LOCAL)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_FALSE(f2_.IsActive()); + // The calls to set an offer after a provisional answer fail, so the + // state doesn't change. + EXPECT_FALSE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_FALSE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_FALSE(f1_.IsActive()); + EXPECT_FALSE(f2_.IsActive()); + + answer.push_back(kTestCryptoParams2); + EXPECT_TRUE(f2_.SetAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_80, kCsAesCm128HmacSha1_80); +} + +// Test that we can disable encryption. +TEST_F(SrtpFilterTest, TestDisableEncryption) { + std::vector offer(MakeVector(kTestCryptoParams1)); + std::vector answer(MakeVector(kTestCryptoParams2)); + + TestSetParams(offer, answer); + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_80, kCsAesCm128HmacSha1_80); + + offer.clear(); + answer.clear(); + EXPECT_TRUE(f1_.SetOffer(offer, CS_LOCAL)); + EXPECT_TRUE(f2_.SetOffer(offer, CS_REMOTE)); + EXPECT_TRUE(f1_.IsActive()); + EXPECT_TRUE(f2_.IsActive()); + + // Test that the old keys are valid until the negotiation is complete. + VerifyCryptoParamsMatch(kCsAesCm128HmacSha1_80, kCsAesCm128HmacSha1_80); + + // Complete the negotiation. + EXPECT_TRUE(f2_.SetAnswer(answer, CS_LOCAL)); + EXPECT_TRUE(f1_.SetAnswer(answer, CS_REMOTE)); + + EXPECT_FALSE(f1_.IsActive()); + EXPECT_FALSE(f2_.IsActive()); +} + +} // namespace rtc diff --git a/pc/srtp_transport.cc b/pc/srtp_transport.cc index fa2fc3b46c..230c1a347b 100644 --- a/pc/srtp_transport.cc +++ b/pc/srtp_transport.cc @@ -37,6 +37,86 @@ namespace webrtc { SrtpTransport::SrtpTransport(bool rtcp_mux_enabled) : RtpTransport(rtcp_mux_enabled) {} +RTCError SrtpTransport::SetSrtpSendKey(const cricket::CryptoParams& params) { + if (send_params_) { + LOG_AND_RETURN_ERROR( + webrtc::RTCErrorType::UNSUPPORTED_OPERATION, + "Setting the SRTP send key twice is currently unsupported."); + } + if (recv_params_ && recv_params_->cipher_suite != params.cipher_suite) { + LOG_AND_RETURN_ERROR( + webrtc::RTCErrorType::UNSUPPORTED_OPERATION, + "The send key and receive key must have the same cipher suite."); + } + + send_cipher_suite_ = rtc::SrtpCryptoSuiteFromName(params.cipher_suite); + if (*send_cipher_suite_ == rtc::kSrtpInvalidCryptoSuite) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "Invalid SRTP crypto suite"); + } + + int send_key_len, send_salt_len; + if (!rtc::GetSrtpKeyAndSaltLengths(*send_cipher_suite_, &send_key_len, + &send_salt_len)) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "Could not get lengths for crypto suite(s):" + " send cipher_suite "); + } + + send_key_ = rtc::ZeroOnFreeBuffer(send_key_len + send_salt_len); + if (!ParseKeyParams(params.key_params, send_key_.data(), send_key_.size())) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "Failed to parse the crypto key params"); + } + + if (!MaybeSetKeyParams()) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "Failed to set the crypto key params"); + } + send_params_ = params; + return RTCError::OK(); +} + +RTCError SrtpTransport::SetSrtpReceiveKey(const cricket::CryptoParams& params) { + if (recv_params_) { + LOG_AND_RETURN_ERROR( + webrtc::RTCErrorType::UNSUPPORTED_OPERATION, + "Setting the SRTP send key twice is currently unsupported."); + } + if (send_params_ && send_params_->cipher_suite != params.cipher_suite) { + LOG_AND_RETURN_ERROR( + webrtc::RTCErrorType::UNSUPPORTED_OPERATION, + "The send key and receive key must have the same cipher suite."); + } + + recv_cipher_suite_ = rtc::SrtpCryptoSuiteFromName(params.cipher_suite); + if (*recv_cipher_suite_ == rtc::kSrtpInvalidCryptoSuite) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "Invalid SRTP crypto suite"); + } + + int recv_key_len, recv_salt_len; + if (!rtc::GetSrtpKeyAndSaltLengths(*recv_cipher_suite_, &recv_key_len, + &recv_salt_len)) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "Could not get lengths for crypto suite(s):" + " recv cipher_suite "); + } + + recv_key_ = rtc::ZeroOnFreeBuffer(recv_key_len + recv_salt_len); + if (!ParseKeyParams(params.key_params, recv_key_.data(), recv_key_.size())) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "Failed to parse the crypto key params"); + } + + if (!MaybeSetKeyParams()) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "Failed to set the crypto key params"); + } + recv_params_ = params; + return RTCError::OK(); +} + bool SrtpTransport::SendRtpPacket(rtc::CopyOnWriteBuffer* packet, const rtc::PacketOptions& options, int flags) { diff --git a/pc/srtp_transport.h b/pc/srtp_transport.h index 03fb1469a7..4bc028d68e 100644 --- a/pc/srtp_transport.h +++ b/pc/srtp_transport.h @@ -19,6 +19,7 @@ #include #include "absl/types/optional.h" +#include "api/crypto_params.h" #include "api/rtc_error.h" #include "p2p/base/packet_transport_internal.h" #include "pc/rtp_transport.h" @@ -39,6 +40,10 @@ class SrtpTransport : public RtpTransport { virtual ~SrtpTransport() = default; + // SrtpTransportInterface specific implementation. + virtual RTCError SetSrtpSendKey(const cricket::CryptoParams& params); + virtual RTCError SetSrtpReceiveKey(const cricket::CryptoParams& params); + bool SendRtpPacket(rtc::CopyOnWriteBuffer* packet, const rtc::PacketOptions& options, int flags) override; @@ -148,6 +153,8 @@ class SrtpTransport : public RtpTransport { std::unique_ptr send_rtcp_session_; std::unique_ptr recv_rtcp_session_; + absl::optional send_params_; + absl::optional recv_params_; absl::optional send_cipher_suite_; absl::optional recv_cipher_suite_; rtc::ZeroOnFreeBuffer send_key_; diff --git a/pc/webrtc_sdp.cc b/pc/webrtc_sdp.cc index ba58f4315f..6def54a346 100644 --- a/pc/webrtc_sdp.cc +++ b/pc/webrtc_sdp.cc @@ -26,6 +26,7 @@ #include "absl/algorithm/container.h" #include "api/candidate.h" +#include "api/crypto_params.h" #include "api/jsep_ice_candidate.h" #include "api/jsep_session_description.h" #include "api/media_types.h" @@ -69,6 +70,7 @@ using cricket::AudioContentDescription; using cricket::Candidate; using cricket::Candidates; using cricket::ContentInfo; +using cricket::CryptoParams; using cricket::ICE_CANDIDATE_COMPONENT_RTCP; using cricket::ICE_CANDIDATE_COMPONENT_RTP; using cricket::kApplicationSpecificBandwidth; @@ -152,6 +154,7 @@ static const char kNoStreamMsid[] = "-"; static const char kSsrcAttributeMslabel[] = "mslabel"; static const char kSSrcAttributeLabel[] = "label"; static const char kAttributeSsrcGroup[] = "ssrc-group"; +static const char kAttributeCrypto[] = "crypto"; static const char kAttributeCandidate[] = "candidate"; static const char kAttributeCandidateTyp[] = "typ"; static const char kAttributeCandidateRaddr[] = "raddr"; @@ -329,6 +332,9 @@ static bool ParseSsrcAttribute(const std::string& line, static bool ParseSsrcGroupAttribute(const std::string& line, SsrcGroupVec* ssrc_groups, SdpParseError* error); +static bool ParseCryptoAttribute(const std::string& line, + MediaContentDescription* media_desc, + SdpParseError* error); static bool ParseRtpmapAttribute(const std::string& line, const cricket::MediaType media_type, const std::vector& payload_types, @@ -1671,6 +1677,18 @@ void BuildRtpContentAttributes(const MediaContentDescription* media_desc, AddLine(os.str(), message); } + // RFC 4568 + // a=crypto: [] + for (const CryptoParams& crypto_params : media_desc->cryptos()) { + InitAttrLine(kAttributeCrypto, &os); + os << kSdpDelimiterColon << crypto_params.tag << " " + << crypto_params.cipher_suite << " " << crypto_params.key_params; + if (!crypto_params.session_params.empty()) { + os << " " << crypto_params.session_params; + } + AddLine(os.str(), message); + } + // RFC 4566 // a=rtpmap: / // [/] @@ -3152,6 +3170,10 @@ bool ParseContent(const std::string& message, if (!ParseSsrcAttribute(line, &ssrc_infos, msid_signaling, error)) { return false; } + } else if (HasAttribute(line, kAttributeCrypto)) { + if (!ParseCryptoAttribute(line, media_desc, error)) { + return false; + } } else if (HasAttribute(line, kAttributeRtpmap)) { if (!ParseRtpmapAttribute(line, media_type, payload_types, media_desc, error)) { @@ -3481,6 +3503,36 @@ bool ParseSsrcGroupAttribute(const std::string& line, return true; } +bool ParseCryptoAttribute(const std::string& line, + MediaContentDescription* media_desc, + SdpParseError* error) { + std::vector fields; + rtc::split(line.substr(kLinePrefixLength), kSdpDelimiterSpaceChar, &fields); + // RFC 4568 + // a=crypto: [] + const size_t expected_min_fields = 3; + if (fields.size() < expected_min_fields) { + return ParseFailedExpectMinFieldNum(line, expected_min_fields, error); + } + std::string tag_value; + if (!GetValue(fields[0], kAttributeCrypto, &tag_value, error)) { + return false; + } + int tag = 0; + if (!GetValueFromString(line, tag_value, &tag, error)) { + return false; + } + const std::string& crypto_suite = fields[1]; + const std::string& key_params = fields[2]; + std::string session_params; + if (fields.size() > 3) { + session_params = fields[3]; + } + media_desc->AddCrypto( + CryptoParams(tag, crypto_suite, key_params, session_params)); + return true; +} + // Updates or creates a new codec entry in the audio description with according // to `name`, `clockrate`, `bitrate`, and `channels`. void UpdateCodec(int payload_type, diff --git a/pc/webrtc_sdp_unittest.cc b/pc/webrtc_sdp_unittest.cc index 1daac0f99e..a2a884a460 100644 --- a/pc/webrtc_sdp_unittest.cc +++ b/pc/webrtc_sdp_unittest.cc @@ -23,6 +23,7 @@ #include "absl/memory/memory.h" #include "absl/strings/str_replace.h" #include "api/array_view.h" +#include "api/crypto_params.h" #include "api/jsep_session_description.h" #include "api/media_types.h" #include "api/rtp_parameters.h" @@ -54,6 +55,7 @@ using cricket::AudioContentDescription; using cricket::Candidate; using cricket::ContentGroup; using cricket::ContentInfo; +using cricket::CryptoParams; using cricket::ICE_CANDIDATE_COMPONENT_RTCP; using cricket::ICE_CANDIDATE_COMPONENT_RTP; using cricket::kFecSsrcGroupSemantics; @@ -139,9 +141,9 @@ struct CodecParams { int maxaveragebitrate; }; -// Note: The reference strings do not contain a=fingerprint, which is -// required for DTLS negotiation. -// Neither do they contain the obsolete a=crypto lines. +// TODO(deadbeef): In these reference strings, use "a=fingerprint" by default +// instead of "a=crypto", and have an explicit test for adding "a=crypto". +// Currently it's the other way around. // Reference sdp string static const char kSdpFullString[] = @@ -173,6 +175,9 @@ static const char kSdpFullString[] = "a=sendrecv\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -198,6 +203,8 @@ static const char kSdpFullString[] = "a=ice-ufrag:ufrag_video\r\na=ice-pwd:pwd_video\r\n" "a=mid:video_content_name\r\n" "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" "a=rtpmap:120 VP8/90000\r\n" "a=ssrc-group:FEC 2 3\r\n" "a=ssrc:2 cname:stream_1_cname\r\n" @@ -225,6 +232,9 @@ static const char kSdpString[] = "a=sendrecv\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -238,6 +248,8 @@ static const char kSdpString[] = "a=ice-ufrag:ufrag_video\r\na=ice-pwd:pwd_video\r\n" "a=mid:video_content_name\r\n" "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" "a=rtpmap:120 VP8/90000\r\n" "a=ssrc-group:FEC 2 3\r\n" "a=ssrc:2 cname:stream_1_cname\r\n" @@ -370,6 +382,9 @@ static const char kBundleOnlySdpFullString[] = "a=sendrecv\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -383,6 +398,8 @@ static const char kBundleOnlySdpFullString[] = "a=bundle-only\r\n" "a=mid:video_content_name\r\n" "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" "a=rtpmap:120 VP8/90000\r\n" "a=ssrc-group:FEC 2 3\r\n" "a=ssrc:2 cname:stream_1_cname\r\n" @@ -425,6 +442,9 @@ static const char kPlanBSdpFullString[] = "a=sendrecv\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -454,6 +474,8 @@ static const char kPlanBSdpFullString[] = "a=ice-ufrag:ufrag_video\r\na=ice-pwd:pwd_video\r\n" "a=mid:video_content_name\r\n" "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" "a=rtpmap:120 VP8/90000\r\n" "a=ssrc-group:FEC 2 3\r\n" "a=ssrc:2 cname:stream_1_cname\r\n" @@ -506,6 +528,9 @@ static const char kUnifiedPlanSdpFullString[] = "a=sendrecv\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -530,6 +555,8 @@ static const char kUnifiedPlanSdpFullString[] = "a=mid:video_content_name\r\n" "a=msid:local_stream_1 video_track_id_1\r\n" "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" "a=rtpmap:120 VP8/90000\r\n" "a=ssrc-group:FEC 2 3\r\n" "a=ssrc:2 cname:stream_1_cname\r\n" @@ -544,6 +571,9 @@ static const char kUnifiedPlanSdpFullString[] = "a=sendrecv\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -556,6 +586,8 @@ static const char kUnifiedPlanSdpFullString[] = "a=mid:video_content_name_2\r\n" "a=msid:local_stream_2 video_track_id_2\r\n" "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" "a=rtpmap:120 VP8/90000\r\n" "a=ssrc:5 cname:stream_2_cname\r\n" // Video track 3, stream 2. @@ -566,6 +598,8 @@ static const char kUnifiedPlanSdpFullString[] = "a=mid:video_content_name_3\r\n" "a=msid:local_stream_2 video_track_id_3\r\n" "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" "a=rtpmap:120 VP8/90000\r\n" "a=ssrc:6 cname:stream_2_cname\r\n"; @@ -607,6 +641,9 @@ static const char kUnifiedPlanSdpFullStringWithSpecialMsid[] = "a=msid:local_stream_1 audio_track_id_1\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -625,6 +662,9 @@ static const char kUnifiedPlanSdpFullStringWithSpecialMsid[] = "a=msid:local_stream_2 audio_track_id_2\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -644,6 +684,9 @@ static const char kUnifiedPlanSdpFullStringWithSpecialMsid[] = "a=msid:- audio_track_id_3\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -683,6 +726,9 @@ static const char kUnifiedPlanSdpFullStringNoSsrc[] = "a=sendrecv\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -706,6 +752,8 @@ static const char kUnifiedPlanSdpFullStringNoSsrc[] = "a=mid:video_content_name\r\n" "a=msid:local_stream_1 video_track_id_1\r\n" "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" "a=rtpmap:120 VP8/90000\r\n" // Audio track 2, stream 2. "m=audio 9 RTP/SAVPF 111 103 104\r\n" @@ -717,6 +765,9 @@ static const char kUnifiedPlanSdpFullStringNoSsrc[] = "a=sendrecv\r\n" "a=rtcp-mux\r\n" "a=rtcp-rsize\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_32 " + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 " + "dummy_session_params\r\n" "a=rtpmap:111 opus/48000/2\r\n" "a=rtpmap:103 ISAC/16000\r\n" "a=rtpmap:104 ISAC/32000\r\n" @@ -728,6 +779,8 @@ static const char kUnifiedPlanSdpFullStringNoSsrc[] = "a=mid:video_content_name_2\r\n" "a=msid:local_stream_2 video_track_id_2\r\n" "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" "a=rtpmap:120 VP8/90000\r\n" // Video track 3, stream 2. "m=video 9 RTP/SAVPF 120\r\n" @@ -737,6 +790,8 @@ static const char kUnifiedPlanSdpFullStringNoSsrc[] = "a=mid:video_content_name_3\r\n" "a=msid:local_stream_2 video_track_id_3\r\n" "a=sendrecv\r\n" + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 " + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n" "a=rtpmap:120 VP8/90000\r\n"; // One candidate reference string as per W3c spec. @@ -1216,6 +1271,10 @@ class WebRtcSdpTest : public ::testing::Test { AudioContentDescription* audio = new AudioContentDescription(); audio->set_rtcp_mux(true); audio->set_rtcp_reduced_size(true); + audio->AddCrypto(CryptoParams( + 1, "AES_CM_128_HMAC_SHA1_32", + "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32", + "dummy_session_params")); audio->set_protocol(cricket::kMediaProtocolSavpf); audio->AddCodec(AudioCodec(111, "opus", 48000, 0, 2)); audio->AddCodec(AudioCodec(103, "ISAC", 16000, 0, 1)); @@ -1289,6 +1348,9 @@ class WebRtcSdpTest : public ::testing::Test { // configuration. VideoContentDescription* CreateVideoContentDescription() { VideoContentDescription* video = new VideoContentDescription(); + video->AddCrypto(CryptoParams( + 1, "AES_CM_128_HMAC_SHA1_80", + "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32", "")); video->set_protocol(cricket::kMediaProtocolSavpf); video->AddCodec( VideoCodec(120, JsepSessionDescription::kDefaultVideoCodecName)); @@ -1309,6 +1371,20 @@ class WebRtcSdpTest : public ::testing::Test { // rtcp_reduced_size EXPECT_EQ(cd1->rtcp_reduced_size(), cd2->rtcp_reduced_size()); + // cryptos + EXPECT_EQ(cd1->cryptos().size(), cd2->cryptos().size()); + if (cd1->cryptos().size() != cd2->cryptos().size()) { + ADD_FAILURE(); + return; + } + for (size_t i = 0; i < cd1->cryptos().size(); ++i) { + const CryptoParams c1 = cd1->cryptos().at(i); + const CryptoParams c2 = cd2->cryptos().at(i); + EXPECT_TRUE(c1.Matches(c2)); + EXPECT_EQ(c1.key_params, c2.key_params); + EXPECT_EQ(c1.session_params, c2.session_params); + } + // protocol // Use an equivalence class here, for old and new versions of the // protocol description. @@ -1486,11 +1562,6 @@ class WebRtcSdpTest : public ::testing::Test { const JsepSessionDescription& desc2) { EXPECT_EQ(desc1.session_id(), desc2.session_id()); EXPECT_EQ(desc1.session_version(), desc2.session_version()); - EXPECT_TRUE(desc1.description()); - EXPECT_TRUE(desc2.description()); - if (!desc1.description() || !desc2.description()) { - return false; - } CompareSessionDescription(*desc1.description(), *desc2.description()); if (desc1.number_of_mediasections() != desc2.number_of_mediasections()) return false; @@ -1604,6 +1675,11 @@ class WebRtcSdpTest : public ::testing::Test { absl::WrapUnique(video_desc_)); } + void RemoveCryptos() { + audio_desc_->set_cryptos(std::vector()); + video_desc_->set_cryptos(std::vector()); + } + // Removes everything in StreamParams from the session description that is // used for a=ssrc lines. void RemoveSsrcSignalingFromStreamParams() { @@ -2007,7 +2083,7 @@ TEST_F(WebRtcSdpTest, SerializeSessionDescriptionEmpty) { EXPECT_EQ("", webrtc::SdpSerialize(jdesc_empty)); } -// This tests serialization of SDP with a=fingerprint, as would be +// This tests serialization of SDP with a=crypto and a=fingerprint, as would be // the case in a DTLS offer. TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithFingerprint) { AddFingerprint(); @@ -2026,6 +2102,7 @@ TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithFingerprint) { // be the case in a DTLS answer. TEST_F(WebRtcSdpTest, SerializeSessionDescriptionWithFingerprintNoCryptos) { AddFingerprint(); + RemoveCryptos(); JsepSessionDescription jdesc_with_fingerprint(kDummyType); MakeDescriptionWithoutCandidates(&jdesc_with_fingerprint); std::string message = webrtc::SdpSerialize(jdesc_with_fingerprint); @@ -3178,6 +3255,8 @@ TEST_F(WebRtcSdpTest, DeserializeSdpWithInvalidAttributeValue) { // ssrc ExpectParseFailure("a=ssrc:1", "a=ssrc:badvalue"); ExpectParseFailure("a=ssrc-group:FEC 2 3", "a=ssrc-group:FEC badvalue 3"); + // crypto + ExpectParseFailure("a=crypto:1 ", "a=crypto:badvalue "); // rtpmap ExpectParseFailure("a=rtpmap:111 ", "a=rtpmap:badvalue "); ExpectParseFailure("opus/48000/2", "opus/badvalue/2"); @@ -3728,7 +3807,7 @@ TEST_F(WebRtcSdpTest, DeserializeUnifiedPlanSessionDescription) { MakeUnifiedPlanDescription(); JsepSessionDescription deserialized_description(kDummyType); - ASSERT_TRUE( + EXPECT_TRUE( SdpDeserialize(kUnifiedPlanSdpFullString, &deserialized_description)); EXPECT_TRUE(CompareSessionDescription(jdesc_, deserialized_description)); diff --git a/pc/webrtc_session_description_factory.cc b/pc/webrtc_session_description_factory.cc index c52ff2d7f6..995ef5e780 100644 --- a/pc/webrtc_session_description_factory.cc +++ b/pc/webrtc_session_description_factory.cc @@ -152,10 +152,13 @@ WebRtcSessionDescriptionFactory::WebRtcSessionDescriptionFactory( RTC_DCHECK(signaling_thread_); if (!dtls_enabled) { - RTC_LOG(LS_INFO) << "DTLS is disabled, no encryption applied"; + SetSdesPolicy(cricket::SEC_REQUIRED); + RTC_LOG(LS_VERBOSE) << "DTLS-SRTP disabled."; return; } - RTC_DCHECK(certificate || cert_generator_); + + // SRTP-SDES is disabled if DTLS is on. + SetSdesPolicy(cricket::SEC_DISABLED); if (certificate) { // Use `certificate`. certificate_request_state_ = CERTIFICATE_WAITING; @@ -286,6 +289,15 @@ void WebRtcSessionDescriptionFactory::CreateAnswer( } } +void WebRtcSessionDescriptionFactory::SetSdesPolicy( + cricket::SecurePolicy secure_policy) { + session_desc_factory_.set_secure(secure_policy); +} + +cricket::SecurePolicy WebRtcSessionDescriptionFactory::SdesPolicy() const { + return session_desc_factory_.secure(); +} + void WebRtcSessionDescriptionFactory::OnMessage(rtc::Message* msg) { switch (msg->message_id) { case MSG_CREATE_SESSIONDESCRIPTION_SUCCESS: { @@ -482,6 +494,7 @@ void WebRtcSessionDescriptionFactory::SetCertificate( on_certificate_ready_(certificate); transport_desc_factory_.set_certificate(certificate); + transport_desc_factory_.set_secure(cricket::SEC_ENABLED); while (!create_session_description_requests_.empty()) { if (create_session_description_requests_.front().type == diff --git a/pc/webrtc_session_description_factory.h b/pc/webrtc_session_description_factory.h index 60c8bc4784..8e80fb556d 100644 --- a/pc/webrtc_session_description_factory.h +++ b/pc/webrtc_session_description_factory.h @@ -104,6 +104,9 @@ class WebRtcSessionDescriptionFactory : public rtc::MessageHandler, void CreateAnswer(CreateSessionDescriptionObserver* observer, const cricket::MediaSessionOptions& session_options); + void SetSdesPolicy(cricket::SecurePolicy secure_policy); + cricket::SecurePolicy SdesPolicy() const; + void set_enable_encrypted_rtp_header_extensions(bool enable) { session_desc_factory_.set_enable_encrypted_rtp_header_extensions(enable); }