Bug 996238 - ALPN support for WebRTC. r=ekr

This commit is contained in:
Martin Thomson 2015-04-01 11:21:06 -07:00
parent c792a525fe
commit 7b12c6b26b
6 changed files with 269 additions and 17 deletions

View File

@ -82,5 +82,10 @@
EXPECT_TRUE(res); \
} while(0)
#define ASSERT_EQ_WAIT(expected, actual, timeout) \
do { \
WAIT(expected == actual, timeout); \
ASSERT_EQ(expected, actual); \
} while(0)
#endif

View File

@ -501,6 +501,20 @@ class TransportTestPeer : public sigslot::has_slots<> {
ASSERT_TRUE(NS_SUCCEEDED(res));
}
void SetAlpn(std::string str, bool withDefault, std::string extra = "") {
std::set<std::string> alpn;
alpn.insert(str); // the one we want to select
if (!extra.empty()) {
alpn.insert(extra);
}
nsresult res = dtls_->SetAlpn(alpn, withDefault ? str : "");
ASSERT_EQ(NS_OK, res);
}
const std::string& GetAlpn() const {
return dtls_->GetNegotiatedAlpn();
}
void SetDtlsPeer(TransportTestPeer *peer, int digests, unsigned int damage) {
unsigned int mask = 1;
@ -854,14 +868,23 @@ class TransportTest : public ::testing::Test {
p2_->SetDtlsAllowAll();
}
void ConnectSocket() {
test_utils->sts_target()->Dispatch(
WrapRunnable(p1_, &TransportTestPeer::ConnectSocket, p2_),
NS_DISPATCH_SYNC);
test_utils->sts_target()->Dispatch(
WrapRunnable(p2_, &TransportTestPeer::ConnectSocket, p1_),
NS_DISPATCH_SYNC);
void SetAlpn(std::string first, std::string second,
bool withDefaults = true) {
if (!first.empty()) {
p1_->SetAlpn(first, withDefaults, "bogus");
}
if (!second.empty()) {
p2_->SetAlpn(second, withDefaults);
}
}
void CheckAlpn(std::string first, std::string second) {
ASSERT_EQ(first, p1_->GetAlpn());
ASSERT_EQ(second, p2_->GetAlpn());
}
void ConnectSocket() {
ConnectSocketInternal();
ASSERT_TRUE_WAIT(p1_->connected(), 10000);
ASSERT_TRUE_WAIT(p2_->connected(), 10000);
@ -870,16 +893,18 @@ class TransportTest : public ::testing::Test {
}
void ConnectSocketExpectFail() {
test_utils->sts_target()->Dispatch(
WrapRunnable(p1_, &TransportTestPeer::ConnectSocket, p2_),
NS_DISPATCH_SYNC);
test_utils->sts_target()->Dispatch(
WrapRunnable(p2_, &TransportTestPeer::ConnectSocket, p1_),
NS_DISPATCH_SYNC);
ConnectSocketInternal();
ASSERT_TRUE_WAIT(p1_->failed(), 10000);
ASSERT_TRUE_WAIT(p2_->failed(), 10000);
}
void ConnectSocketExpectState(TransportLayer::State s1,
TransportLayer::State s2) {
ConnectSocketInternal();
ASSERT_EQ_WAIT(s1, p1_->state(), 10000);
ASSERT_EQ_WAIT(s2, p2_->state(), 10000);
}
void InitIce() {
p1_->InitIce();
p2_->InitIce();
@ -908,6 +933,15 @@ class TransportTest : public ::testing::Test {
}
protected:
void ConnectSocketInternal() {
test_utils->sts_target()->Dispatch(
WrapRunnable(p1_, &TransportTestPeer::ConnectSocket, p2_),
NS_DISPATCH_SYNC);
test_utils->sts_target()->Dispatch(
WrapRunnable(p2_, &TransportTestPeer::ConnectSocket, p1_),
NS_DISPATCH_SYNC);
}
PRFileDesc *fds_[2];
TransportTestPeer *p1_;
TransportTestPeer *p2_;
@ -955,6 +989,56 @@ TEST_F(TransportTest, TestConnectAllowAll) {
ConnectSocket();
}
TEST_F(TransportTest, TestConnectAlpn) {
SetDtlsPeer();
SetAlpn("a", "a");
ConnectSocket();
CheckAlpn("a", "a");
}
TEST_F(TransportTest, TestConnectAlpnMismatch) {
SetDtlsPeer();
SetAlpn("something", "different");
ConnectSocketExpectFail();
}
TEST_F(TransportTest, TestConnectAlpnServerDefault) {
SetDtlsPeer();
SetAlpn("def", "");
// server allows default, client doesn't support
ConnectSocket();
CheckAlpn("def", "");
}
TEST_F(TransportTest, TestConnectAlpnClientDefault) {
SetDtlsPeer();
SetAlpn("", "clientdef");
// client allows default, but server will ignore the extension
ConnectSocket();
CheckAlpn("", "clientdef");
}
TEST_F(TransportTest, TestConnectClientNoAlpn) {
SetDtlsPeer();
// Here the server has ALPN, but no default is allowed.
// Reminder: p1 == server, p2 == client
SetAlpn("server-nodefault", "", false);
// The server doesn't see the extension, so negotiates without it.
// But then the server is forced to close when it discovers that ALPN wasn't
// negotiated; the client sees a close.
ConnectSocketExpectState(TransportLayer::TS_ERROR,
TransportLayer::TS_CLOSED);
}
TEST_F(TransportTest, TestConnectServerNoAlpn) {
SetDtlsPeer();
SetAlpn("", "client-nodefault", false);
// The client aborts; the server doesn't realize this is a problem and just
// sees the close.
ConnectSocketExpectState(TransportLayer::TS_CLOSED,
TransportLayer::TS_ERROR);
}
TEST_F(TransportTest, TestConnectNoDigest) {
SetDtlsPeer(0, 0);

View File

@ -8,6 +8,7 @@
#include <queue>
#include <algorithm>
#include <sstream>
#include "mozilla/UniquePtr.h"
@ -44,6 +45,7 @@ static PRDescIdentity transport_layer_identity = PR_INVALID_IO_LAYER;
MOZ_ASSERT(false); \
PR_SetError(PR_NOT_IMPLEMENTED_ERROR, 0)
#define MAX_ALPN_LENGTH 255
// We need to adapt the NSPR/libssl model to the TransportFlow model.
// The former wants pull semantics and TransportFlow wants push.
@ -388,6 +390,24 @@ void TransportLayerDtls::WasInserted() {
}
// Set the permitted and default ALPN identifiers.
// The default is here to allow for peers that don't want to negotiate ALPN
// in that case, the default string will be reported from GetNegotiatedAlpn().
// Setting the default to the empty string causes the transport layer to fail
// if ALPN is not negotiated.
// Note: we only support Unicode strings here, which are encoded into UTF-8,
// even though ALPN ostensibly allows arbitrary octet sequences.
nsresult TransportLayerDtls::SetAlpn(
const std::set<std::string>& alpn_allowed,
const std::string& alpn_default) {
alpn_allowed_ = alpn_allowed;
alpn_default_ = alpn_default;
return NS_OK;
}
nsresult TransportLayerDtls::SetVerificationAllowAll() {
// Defensive programming
if (verification_mode_ != VERIFY_UNSET)
@ -564,6 +584,10 @@ bool TransportLayerDtls::Setup() {
return false;
}
if (!SetupAlpn(ssl_fd)) {
return false;
}
// Now start the handshake
rv = SSL_ResetHandshake(ssl_fd, role_ == SERVER ? PR_TRUE : PR_FALSE);
if (rv != SECSuccess) {
@ -584,6 +608,43 @@ bool TransportLayerDtls::Setup() {
return true;
}
bool TransportLayerDtls::SetupAlpn(PRFileDesc* ssl_fd) const {
if (alpn_allowed_.empty()) {
return true;
}
SECStatus rv = SSL_OptionSet(ssl_fd, SSL_ENABLE_NPN, PR_FALSE);
if (rv != SECSuccess) {
MOZ_MTLOG(ML_ERROR, "Couldn't disable NPN");
return false;
}
rv = SSL_OptionSet(ssl_fd, SSL_ENABLE_ALPN, PR_TRUE);
if (rv != SECSuccess) {
MOZ_MTLOG(ML_ERROR, "Couldn't enable ALPN");
return false;
}
unsigned char buf[MAX_ALPN_LENGTH];
size_t offset = 0;
for (auto tag = alpn_allowed_.begin();
tag != alpn_allowed_.end(); ++tag) {
if ((offset + 1 + tag->length()) >= sizeof(buf)) {
MOZ_MTLOG(ML_ERROR, "ALPN too long");
return false;
}
buf[offset++] = tag->length();
memcpy(buf + offset, tag->c_str(), tag->length());
offset += tag->length();
}
rv = SSL_SetNextProtoNego(ssl_fd, buf, offset);
if (rv != SECSuccess) {
MOZ_MTLOG(ML_ERROR, "Couldn't set ALPN string");
return false;
}
return true;
}
// Ciphers we need to enable. These are on by default in standard firefox
// builds, but can be disabled with prefs and they aren't on in our unit tests
// since that uses NSS default configuration.
@ -777,6 +838,15 @@ void TransportLayerDtls::Handshake() {
TL_SET_STATE(TS_ERROR);
return;
}
if (!CheckAlpn()) {
// Despite connecting, the connection doesn't have a valid ALPN label.
// Forcibly close the connection so that the peer isn't left hanging
// (assuming the close_notify isn't dropped).
ssl_fd_ = nullptr;
TL_SET_STATE(TS_ERROR);
return;
}
TL_SET_STATE(TS_OPEN);
} else {
int32_t err = PR_GetError();
@ -808,6 +878,61 @@ void TransportLayerDtls::Handshake() {
}
}
// Checks if ALPN was negotiated correctly and returns false if it wasn't.
// After this returns successfully, alpn_ will be set to the negotiated
// protocol.
bool TransportLayerDtls::CheckAlpn() {
if (alpn_allowed_.empty()) {
return true;
}
SSLNextProtoState alpnState;
char chosenAlpn[MAX_ALPN_LENGTH];
unsigned int chosenAlpnLen;
SECStatus rv = SSL_GetNextProto(ssl_fd_, &alpnState,
reinterpret_cast<unsigned char*>(chosenAlpn),
&chosenAlpnLen, sizeof(chosenAlpn));
if (rv != SECSuccess) {
MOZ_MTLOG(ML_ERROR, LAYER_INFO << "ALPN error");
return false;
}
switch (alpnState) {
case SSL_NEXT_PROTO_SELECTED:
case SSL_NEXT_PROTO_NEGOTIATED:
break; // OK
case SSL_NEXT_PROTO_NO_SUPPORT:
MOZ_MTLOG(ML_NOTICE, LAYER_INFO << "ALPN not negotiated, "
<< (alpn_default_.empty() ? "failing" : "selecting default"));
alpn_ = alpn_default_;
return !alpn_.empty();
case SSL_NEXT_PROTO_NO_OVERLAP:
// This only happens if there is a custom NPN/ALPN callback installed and
// that callback doesn't properly handle ALPN.
MOZ_MTLOG(ML_ERROR, LAYER_INFO << "error in ALPN selection callback");
return false;
}
// Warning: NSS won't null terminate the ALPN string for us.
std::string chosen(chosenAlpn, chosenAlpnLen);
MOZ_MTLOG(ML_NOTICE, LAYER_INFO << "Selected ALPN string: " << chosen);
if (alpn_allowed_.find(chosen) == alpn_allowed_.end()) {
// Maybe our peer chose a protocol we didn't offer (when we are client), or
// something is seriously wrong.
std::ostringstream ss;
for (auto i = alpn_allowed_.begin(); i != alpn_allowed_.end(); ++i) {
ss << (i == alpn_allowed_.begin() ? " '" : ", '") << *i << "'";
}
MOZ_MTLOG(ML_ERROR, LAYER_INFO << "Bad ALPN string: '" << chosen
<< "'; permitted:" << ss.str());
return false;
}
alpn_ = chosen;
return true;
}
void TransportLayerDtls::PacketReceived(TransportLayer* layer,
const unsigned char *data,
size_t len) {
@ -928,6 +1053,9 @@ nsresult TransportLayerDtls::SetSrtpCiphers(std::vector<uint16_t> ciphers) {
nsresult TransportLayerDtls::GetSrtpCipher(uint16_t *cipher) const {
CheckThread();
if (state_ != TS_OPEN) {
return NS_ERROR_NOT_AVAILABLE;
}
SECStatus rv = SSL_GetSRTPCipher(ssl_fd_, cipher);
if (rv != SECSuccess) {
MOZ_MTLOG(ML_DEBUG, "No SRTP cipher negotiated");
@ -943,6 +1071,10 @@ nsresult TransportLayerDtls::ExportKeyingMaterial(const std::string& label,
unsigned char *out,
unsigned int outlen) {
CheckThread();
if (state_ != TS_OPEN) {
MOZ_ASSERT(false, "Transport must be open for ExportKeyingMaterial");
return NS_ERROR_NOT_AVAILABLE;
}
SECStatus rv = SSL_ExportKeyingMaterial(ssl_fd_,
label.c_str(),
label.size(),

View File

@ -10,6 +10,7 @@
#define transportlayerdtls_h__
#include <queue>
#include <set>
#include "sigslot.h"
@ -67,6 +68,10 @@ class TransportLayerDtls final : public TransportLayer {
void SetIdentity(const RefPtr<DtlsIdentity>& identity) {
identity_ = identity;
}
nsresult SetAlpn(const std::set<std::string>& allowedAlpn,
const std::string& alpnDefault);
const std::string& GetNegotiatedAlpn() const { return alpn_; }
nsresult SetVerificationAllowAll();
nsresult SetVerificationDigest(const std::string digest_algorithm,
const unsigned char *digest_value,
@ -131,8 +136,11 @@ class TransportLayerDtls final : public TransportLayer {
bool Setup();
bool SetupCipherSuites(PRFileDesc* ssl_fd) const;
bool SetupAlpn(PRFileDesc* ssl_fd) const;
void Handshake();
bool CheckAlpn();
static SECStatus GetClientAuthDataHook(void *arg, PRFileDesc *fd,
CERTDistNames *caNames,
CERTCertificate **pRetCert,
@ -151,6 +159,13 @@ class TransportLayerDtls final : public TransportLayer {
CERTCertificate *cert);
RefPtr<DtlsIdentity> identity_;
// What ALPN identifiers are permitted.
std::set<std::string> alpn_allowed_;
// What ALPN identifier is used if ALPN is not supported.
// The empty string indicates that ALPN is required.
std::string alpn_default_;
// What ALPN string was negotiated.
std::string alpn_;
std::vector<uint16_t> srtp_ciphers_;
Role role_;

View File

@ -199,6 +199,22 @@ MediaPipelineFactory::CreateOrGetTransportFlow(
return rv;
}
// Always permits negotiation of the confidential mode.
// Only allow non-confidential (which is an allowed default),
// if we aren't confidential.
std::set<std::string> alpn;
std::string alpnDefault = "";
alpn.insert("c-webrtc");
if (!mPC->PrivacyRequested()) {
alpnDefault = "webrtc";
alpn.insert(alpnDefault);
}
rv = dtls->SetAlpn(alpn, alpnDefault);
if (NS_FAILED(rv)) {
MOZ_MTLOG(ML_ERROR, "Couldn't set ALPN");
return rv;
}
nsAutoPtr<PtrVector<TransportLayer> > layers(new PtrVector<TransportLayer>);
layers->values.push_back(ice.release());
layers->values.push_back(dtls.release());

View File

@ -956,14 +956,14 @@ PeerConnectionMedia::EndOfLocalCandidates_m(const std::string& aDefaultAddr,
}
void
PeerConnectionMedia::DtlsConnected_s(TransportLayer *dtlsLayer,
PeerConnectionMedia::DtlsConnected_s(TransportLayer *layer,
TransportLayer::State state)
{
MOZ_ASSERT(layer->id() == "dtls");
TransportLayerDtls* dtlsLayer = static_cast<TransportLayerDtls*>(layer);
dtlsLayer->SignalStateChange.disconnect(this);
bool privacyRequested = false;
// TODO (Bug 952678) set privacy mode, ask the DTLS layer about that
// This has to be a dispatch to a static method, we could be going away
bool privacyRequested = (dtlsLayer->GetNegotiatedAlpn() == "c-webrtc");
GetMainThread()->Dispatch(
WrapRunnableNM(&PeerConnectionMedia::DtlsConnected_m,
mParentHandle, privacyRequested),