blob: 71f38aabadb09aafe67ff4b6dfda04c1ea3375c5 [file] [log] [blame]
/*
* libjingle
* Copyright 2004--2008, Google Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// SecureTunnelSessionClient and SecureTunnelSession implementation.
#include "talk/session/tunnel/securetunnelsessionclient.h"
#include "talk/base/basicdefs.h"
#include "talk/base/basictypes.h"
#include "talk/base/common.h"
#include "talk/base/helpers.h"
#include "talk/base/logging.h"
#include "talk/base/stringutils.h"
#include "talk/base/sslidentity.h"
#include "talk/base/sslstreamadapter.h"
#include "talk/p2p/base/transportchannel.h"
#include "talk/xmllite/xmlelement.h"
#include "talk/session/tunnel/pseudotcpchannel.h"
namespace cricket {
// XML elements and namespaces for XMPP stanzas used in content exchanges.
const char NS_SECURE_TUNNEL[] = "http://www.google.com/talk/securetunnel";
const buzz::StaticQName QN_SECURE_TUNNEL_DESCRIPTION =
{ NS_SECURE_TUNNEL, "description" };
const buzz::StaticQName QN_SECURE_TUNNEL_TYPE =
{ NS_SECURE_TUNNEL, "type" };
const buzz::StaticQName QN_SECURE_TUNNEL_CLIENT_CERT =
{ NS_SECURE_TUNNEL, "client-cert" };
const buzz::StaticQName QN_SECURE_TUNNEL_SERVER_CERT =
{ NS_SECURE_TUNNEL, "server-cert" };
const char CN_SECURE_TUNNEL[] = "securetunnel";
// SecureTunnelContentDescription
// TunnelContentDescription is extended to hold string forms of the
// client and server certificate, PEM encoded.
struct SecureTunnelContentDescription : public ContentDescription {
std::string description;
std::string client_pem_certificate;
std::string server_pem_certificate;
SecureTunnelContentDescription(const std::string& desc,
const std::string& client_pem_cert,
const std::string& server_pem_cert)
: description(desc),
client_pem_certificate(client_pem_cert),
server_pem_certificate(server_pem_cert) {
}
};
// SecureTunnelSessionClient
SecureTunnelSessionClient::SecureTunnelSessionClient(
const buzz::Jid& jid, SessionManager* manager)
: TunnelSessionClient(jid, manager, NS_SECURE_TUNNEL) {
}
void SecureTunnelSessionClient::SetIdentity(talk_base::SSLIdentity* identity) {
ASSERT(identity_.get() == NULL);
identity_.reset(identity);
}
bool SecureTunnelSessionClient::GenerateIdentity() {
ASSERT(identity_.get() == NULL);
identity_.reset(talk_base::SSLIdentity::Generate(
// The name on the certificate does not matter: the peer will
// make sure the cert it gets during SSL negotiation matches the
// one it got from XMPP. It would be neat to put something
// recognizable in there such as the JID, except this will show
// in clear during the SSL negotiation and so it could be a
// privacy issue. Specifying an empty string here causes
// it to use a random string.
#ifdef _DEBUG
jid().Str()
#else
""
#endif
));
if (identity_.get() == NULL) {
LOG(LS_ERROR) << "Failed to generate SSL identity";
return false;
}
return true;
}
talk_base::SSLIdentity& SecureTunnelSessionClient::GetIdentity() const {
ASSERT(identity_.get() != NULL);
return *identity_;
}
// Parses a certificate from a PEM encoded string.
// Returns NULL on failure.
// The caller is responsible for freeing the returned object.
static talk_base::SSLCertificate* ParseCertificate(
const std::string& pem_cert) {
if (pem_cert.empty())
return NULL;
return talk_base::SSLCertificate::FromPEMString(pem_cert, NULL);
}
TunnelSession* SecureTunnelSessionClient::MakeTunnelSession(
Session* session, talk_base::Thread* stream_thread,
TunnelSessionRole role) {
return new SecureTunnelSession(this, session, stream_thread, role);
}
bool FindSecureTunnelContent(const cricket::SessionDescription* sdesc,
std::string* name,
const SecureTunnelContentDescription** content) {
const ContentInfo* cinfo = sdesc->FirstContentByType(NS_SECURE_TUNNEL);
if (cinfo == NULL)
return false;
*name = cinfo->name;
*content = static_cast<const SecureTunnelContentDescription*>(
cinfo->description);
return true;
}
void SecureTunnelSessionClient::OnIncomingTunnel(const buzz::Jid &jid,
Session *session) {
std::string content_name;
const SecureTunnelContentDescription* content = NULL;
if (!FindSecureTunnelContent(session->remote_description(),
&content_name, &content)) {
ASSERT(false);
}
// Validate the certificate
talk_base::scoped_ptr<talk_base::SSLCertificate> peer_cert(
ParseCertificate(content->client_pem_certificate));
if (peer_cert.get() == NULL) {
LOG(LS_ERROR)
<< "Rejecting incoming secure tunnel with invalid cetificate";
DeclineTunnel(session);
return;
}
// If there were a convenient place we could have cached the
// peer_cert so as not to have to parse it a second time when
// configuring the tunnel.
SignalIncomingTunnel(this, jid, content->description, session);
}
// The XML representation of a session initiation request (XMPP IQ),
// containing the initiator's SecureTunnelContentDescription,
// looks something like this:
// <iq from="INITIATOR@gmail.com/pcpE101B7F4"
// to="RECIPIENT@gmail.com/pcp8B87F0A3"
// type="set" id="3">
// <session xmlns="http://www.google.com/session"
// type="initiate" id="2508605813"
// initiator="INITIATOR@gmail.com/pcpE101B7F4">
// <description xmlns="http://www.google.com/talk/securetunnel">
// <type>send:filename</type>
// <client-cert>
// -----BEGIN CERTIFICATE-----
// INITIATOR'S CERTIFICATE IN PERM FORMAT (ASCII GIBBERISH)
// -----END CERTIFICATE-----
// </client-cert>
// </description>
// <transport xmlns="http://www.google.com/transport/p2p"/>
// </session>
// </iq>
// The session accept iq, containing the recipient's certificate and
// echoing the initiator's certificate, looks something like this:
// <iq from="RECIPIENT@gmail.com/pcpE101B7F4"
// to="INITIATOR@gmail.com/pcpE101B7F4"
// type="set" id="5">
// <session xmlns="http://www.google.com/session"
// type="accept" id="2508605813"
// initiator="sdoyon911@gmail.com/pcpE101B7F4">
// <description xmlns="http://www.google.com/talk/securetunnel">
// <type>send:FILENAME</type>
// <client-cert>
// -----BEGIN CERTIFICATE-----
// INITIATOR'S CERTIFICATE IN PERM FORMAT (ASCII GIBBERISH)
// -----END CERTIFICATE-----
// </client-cert>
// <server-cert>
// -----BEGIN CERTIFICATE-----
// RECIPIENT'S CERTIFICATE IN PERM FORMAT (ASCII GIBBERISH)
// -----END CERTIFICATE-----
// </server-cert>
// </description>
// </session>
// </iq>
bool SecureTunnelSessionClient::ParseContent(SignalingProtocol protocol,
const buzz::XmlElement* elem,
const ContentDescription** content,
ParseError* error) {
const buzz::XmlElement* type_elem = elem->FirstNamed(QN_SECURE_TUNNEL_TYPE);
if (type_elem == NULL)
// Missing mandatory XML element.
return false;
// Here we consider the certificate components to be optional. In
// practice the client certificate is always present, and the server
// certificate is initially missing from the session description
// sent during session initiation. OnAccept() will enforce that we
// have a certificate for our peer.
const buzz::XmlElement* client_cert_elem =
elem->FirstNamed(QN_SECURE_TUNNEL_CLIENT_CERT);
const buzz::XmlElement* server_cert_elem =
elem->FirstNamed(QN_SECURE_TUNNEL_SERVER_CERT);
*content = new SecureTunnelContentDescription(
type_elem->BodyText(),
client_cert_elem ? client_cert_elem->BodyText() : "",
server_cert_elem ? server_cert_elem->BodyText() : "");
return true;
}
bool SecureTunnelSessionClient::WriteContent(
SignalingProtocol protocol, const ContentDescription* untyped_content,
buzz::XmlElement** elem, WriteError* error) {
const SecureTunnelContentDescription* content =
static_cast<const SecureTunnelContentDescription*>(untyped_content);
buzz::XmlElement* root =
new buzz::XmlElement(QN_SECURE_TUNNEL_DESCRIPTION, true);
buzz::XmlElement* type_elem = new buzz::XmlElement(QN_SECURE_TUNNEL_TYPE);
type_elem->SetBodyText(content->description);
root->AddElement(type_elem);
if (!content->client_pem_certificate.empty()) {
buzz::XmlElement* client_cert_elem =
new buzz::XmlElement(QN_SECURE_TUNNEL_CLIENT_CERT);
client_cert_elem->SetBodyText(content->client_pem_certificate);
root->AddElement(client_cert_elem);
}
if (!content->server_pem_certificate.empty()) {
buzz::XmlElement* server_cert_elem =
new buzz::XmlElement(QN_SECURE_TUNNEL_SERVER_CERT);
server_cert_elem->SetBodyText(content->server_pem_certificate);
root->AddElement(server_cert_elem);
}
*elem = root;
return true;
}
SessionDescription* NewSecureTunnelSessionDescription(
const std::string& content_name, const ContentDescription* content) {
SessionDescription* sdesc = new SessionDescription();
sdesc->AddContent(content_name, NS_SECURE_TUNNEL, content);
return sdesc;
}
SessionDescription* SecureTunnelSessionClient::CreateOffer(
const buzz::Jid &jid, const std::string &description) {
// We are the initiator so we are the client. Put our cert into the
// description.
std::string pem_cert = GetIdentity().certificate().ToPEMString();
return NewSecureTunnelSessionDescription(
CN_SECURE_TUNNEL,
new SecureTunnelContentDescription(description, pem_cert, ""));
}
SessionDescription* SecureTunnelSessionClient::CreateAnswer(
const SessionDescription* offer) {
std::string content_name;
const SecureTunnelContentDescription* offer_tunnel = NULL;
if (!FindSecureTunnelContent(offer, &content_name, &offer_tunnel))
return NULL;
// We are accepting a session request. We need to add our cert, the
// server cert, into the description. The client cert was validated
// in OnIncomingTunnel().
ASSERT(!offer_tunnel->client_pem_certificate.empty());
return NewSecureTunnelSessionDescription(
content_name,
new SecureTunnelContentDescription(
offer_tunnel->description,
offer_tunnel->client_pem_certificate,
GetIdentity().certificate().ToPEMString()));
}
// SecureTunnelSession
SecureTunnelSession::SecureTunnelSession(
SecureTunnelSessionClient* client, Session* session,
talk_base::Thread* stream_thread, TunnelSessionRole role)
: TunnelSession(client, session, stream_thread),
role_(role) {
}
talk_base::StreamInterface* SecureTunnelSession::MakeSecureStream(
talk_base::StreamInterface* stream) {
talk_base::SSLStreamAdapter* ssl_stream =
talk_base::SSLStreamAdapter::Create(stream);
talk_base::SSLIdentity* identity =
static_cast<SecureTunnelSessionClient*>(client_)->
GetIdentity().GetReference();
ssl_stream->SetIdentity(identity);
if (role_ == RESPONDER)
ssl_stream->SetServerRole();
ssl_stream->StartSSLWithPeer();
// SSL negotiation will start on the stream as soon as it
// opens. However our SSLStreamAdapter still hasn't been told what
// certificate to allow for our peer. If we are the initiator, we do
// not have the peer's certificate yet: we will obtain it from the
// session accept message which we will receive later (see
// OnAccept()). We won't Connect() the PseudoTcpChannel until we get
// that, so the stream will stay closed until then. Keep a handle
// on the streem so we can configure the peer certificate later.
ssl_stream_reference_.reset(new talk_base::StreamReference(ssl_stream));
return ssl_stream_reference_->NewReference();
}
talk_base::StreamInterface* SecureTunnelSession::GetStream() {
ASSERT(channel_ != NULL);
ASSERT(ssl_stream_reference_.get() == NULL);
return MakeSecureStream(channel_->GetStream());
}
void SecureTunnelSession::OnAccept() {
// We have either sent or received a session accept: it's time to
// connect the tunnel. First we must set the peer certificate.
ASSERT(channel_ != NULL);
ASSERT(session_ != NULL);
std::string content_name;
const SecureTunnelContentDescription* remote_tunnel = NULL;
if (!FindSecureTunnelContent(session_->remote_description(),
&content_name, &remote_tunnel)) {
session_->Reject(STR_TERMINATE_INCOMPATIBLE_PARAMETERS);
return;
}
const std::string& cert_pem =
role_ == INITIATOR ? remote_tunnel->server_pem_certificate :
remote_tunnel->client_pem_certificate;
talk_base::SSLCertificate* peer_cert =
ParseCertificate(cert_pem);
if (peer_cert == NULL) {
ASSERT(role_ == INITIATOR); // when RESPONDER we validated it earlier
LOG(LS_ERROR)
<< "Rejecting secure tunnel accept with invalid cetificate";
session_->Reject(STR_TERMINATE_INCOMPATIBLE_PARAMETERS);
return;
}
ASSERT(ssl_stream_reference_.get() != NULL);
talk_base::SSLStreamAdapter* ssl_stream =
static_cast<talk_base::SSLStreamAdapter*>(
ssl_stream_reference_->GetStream());
ssl_stream->SetPeerCertificate(peer_cert); // pass ownership of certificate.
// We no longer need our handle to the ssl stream.
ssl_stream_reference_.reset();
LOG(LS_INFO) << "Connecting tunnel";
// This will try to connect the PseudoTcpChannel. If and when that
// succeeds, then ssl negotiation will take place, and when that
// succeeds, the tunnel stream will finally open.
VERIFY(channel_->Connect(content_name, "tcp"));
}
} // namespace cricket