SecureSocket: add some certificate support

* Instead of creating an OpenSSL context ofor each socket, use a global
one and initialize it lazily when the first SecureSocket is created
* Load the certificates from our certificate list so SSL certificates
sent by servers can be validated.
* Add a callback for signalling that certificate validation failed, the
default implementation proceeds with the connection anyway (to keep the
old behavior).
* Introduce BCertificate class, that provides some information about a
certificate. Currently it's only used by the callback mentionned above,
but it will be possible to get the leaf certificate for the connection
after it's established.

Review of the API and implementation is welcome, before I start making
use of this in HttpRequest and WebKit to allow the user to accept new
certificates.
This commit is contained in:
Adrien Destugues 2014-01-15 17:39:06 +01:00
parent b11772acca
commit 5ebdc79955
6 changed files with 287 additions and 14 deletions

View File

@ -0,0 +1,32 @@
/*
* Copyright 2014 Haiku, Inc.
* Distributed under the terms of the MIT License.
*/
#ifndef _CERTIFICATE_H
#define _CERTIFICATE_H
#include <SecureSocket.h>
#include <String.h>
class BCertificate {
public:
~BCertificate();
BString String();
bigtime_t StartDate();
bigtime_t ExpirationDate();
BString Issuer();
BString Subject();
private:
friend class BSecureSocket::Private;
class Private;
BCertificate(Private* data);
Private* fPrivate;
};
#endif

View File

@ -9,6 +9,9 @@
#include <Socket.h>
class BCertificate;
class BSecureSocket : public BSocket {
public:
BSecureSocket();
@ -17,6 +20,10 @@ public:
BSecureSocket(const BSecureSocket& other);
virtual ~BSecureSocket();
virtual bool CertificateVerificationFailed(BCertificate);
// BSocket implementation
virtual status_t Connect(const BNetworkAddress& peer,
bigtime_t timeout = B_INFINITE_TIMEOUT);
virtual void Disconnect();
@ -30,6 +37,7 @@ public:
virtual ssize_t Write(const void* buffer, size_t size);
private:
friend class BCertificate;
class Private;
Private* fPrivate;
};

View File

@ -0,0 +1,105 @@
/*
* Copyright 2014 Haiku, Inc.
* Distributed under the terms of the MIT License.
*/
#include <Certificate.h>
#include <String.h>
#include "CertificatePrivate.h"
static time_t parse_ASN1(ASN1_GENERALIZEDTIME *asn1)
{
// Get the raw string data out of the ASN1 container. It looks like this:
// "YYMMDDHHMMSSZ"
struct tm time;
if (sscanf((char*)asn1->data, "%2d%2d%2d%2d%2d%2d", &time.tm_year,
&time.tm_mon, &time.tm_mday, &time.tm_hour, &time.tm_min,
&time.tm_sec) == 6)
return mktime(&time);
return B_BAD_DATA;
}
static BString decode_X509_NAME(X509_NAME* name)
{
int len = X509_NAME_get_text_by_NID(name, 0, NULL, 0);
char buffer[len];
X509_NAME_get_text_by_NID(name, 0, buffer, len);
return BString(buffer);
}
// #pragma mark - BCertificate
BCertificate::BCertificate(Private* data)
{
fPrivate = data;
}
BCertificate::~BCertificate()
{
delete fPrivate;
}
BString
BCertificate::String()
{
BIO *buffer = BIO_new(BIO_s_mem());
X509_print_ex(buffer, fPrivate->fX509, XN_FLAG_COMPAT, X509_FLAG_COMPAT);
char* pointer;
long length = BIO_get_mem_data(buffer, &pointer);
BString result(pointer, length);
BIO_free(buffer);
return result;
}
bigtime_t
BCertificate::StartDate()
{
return parse_ASN1(X509_get_notBefore(fPrivate->fX509));
}
bigtime_t
BCertificate::ExpirationDate()
{
return parse_ASN1(X509_get_notAfter(fPrivate->fX509));
}
BString
BCertificate::Issuer()
{
X509_NAME* name = X509_get_issuer_name(fPrivate->fX509);
return decode_X509_NAME(name);
}
BString
BCertificate::Subject()
{
X509_NAME* name = X509_get_subject_name(fPrivate->fX509);
return decode_X509_NAME(name);
}
// #pragma mark - BCertificate::Private
BCertificate::Private::Private(X509* data)
: fX509(data)
{
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2014 Haiku, Inc.
* Distributed under the terms of the MIT License.
*/
#ifndef _CERTIFICATE_PRIVATE_H
#define _CERTIFICATE_PRIVATE_H
#ifdef OPENSSL_ENABLED
# include <openssl/ssl.h>
#endif
class BCertificate::Private {
public:
Private(X509* data);
public:
X509* fX509;
};
#endif

View File

@ -14,7 +14,7 @@ for architectureObject in [ MultiArchSubDirSetup ] {
if [ FIsBuildFeatureEnabled openssl ] {
SubDirC++Flags -DOPENSSL_ENABLED ;
UseBuildFeatureHeaders openssl ;
sslSources = SSL.cpp ;
sslSources = SSL.cpp Certificate.cpp ;
md5Sources = ;
Includes [ FGristFiles $(sslSources) SecureSocket.cpp
HttpAuthentication.cpp ]

View File

@ -1,4 +1,5 @@
/*
* Copyright 2014 Haiku, Inc.
* Copyright 2011, Axel Dörfler, axeld@pinc-software.de.
* Copyright 2010, Clemens Zeidler <haiku@clemens-zeidler.de>
* Distributed under the terms of the MIT License.
@ -11,6 +12,12 @@
# include <openssl/ssl.h>
#endif
#include <Certificate.h>
#include <FindDirectory.h>
#include <Path.h>
#include "CertificatePrivate.h"
//#define TRACE_SOCKET
#ifdef TRACE_SOCKET
@ -25,12 +32,86 @@
class BSecureSocket::Private {
public:
SSL_CTX* fCTX;
static SSL_CTX* Context();
static int VerifyCallback(int ok, X509_STORE_CTX* ctx);
public:
SSL* fSSL;
BIO* fBIO;
static int sDataIndex;
private:
static SSL_CTX* sContext;
// FIXME When do we SSL_CTX_free it?
static vint32 sInitOnce;
};
/* static */ SSL_CTX* BSecureSocket::Private::sContext = NULL;
/* static */ int BSecureSocket::Private::sDataIndex;
/* static */ vint32 sInitOnce = false;
/* static */ SSL_CTX*
BSecureSocket::Private::Context()
{
// We use lazy initialisation here, because reading certificates from disk
// and parsing them is a relatively long operation and uses some memory.
// We don't want programs that don't use SSL to waste resources with that.
if (!sInitOnce) {
sInitOnce = true;
sContext = SSL_CTX_new(SSLv23_method());
// Setup certificate verification
BPath certificateStore;
find_directory(B_SYSTEM_DATA_DIRECTORY, &certificateStore);
certificateStore.Append("ssl/CARootCertificates.pem");
// TODO we may want to add a non-packaged certificate directory?
// (would make it possible to store user-added certificate exceptions
// there)
SSL_CTX_load_verify_locations(sContext, certificateStore.Path(), NULL);
SSL_CTX_set_verify(sContext, SSL_VERIFY_PEER, VerifyCallback);
// Get an unique index number for storing application data in SSL
// structs. We will store a pointer to the BSecureSocket class there.
sDataIndex = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL);
}
return sContext;
}
// This is called each time a certificate verification occurs. It allows us to
// catch failures and report them.
/* static */ int
BSecureSocket::Private::VerifyCallback(int ok, X509_STORE_CTX* ctx)
{
// OpenSSL already checked the certificate again the certificate store for
// us, and tells the result of that in the ok parameter.
// If the verification succeeded, no need for any further checks. Let's
// proceed with the connection.
if (ok)
return ok;
// The certificate verification failed. Signal this to the caller, and fail.
// First of all, get the affected BSecureSocket
SSL* ssl = (SSL*)X509_STORE_CTX_get_ex_data(ctx,
SSL_get_ex_data_X509_STORE_CTX_idx());
BSecureSocket* socket = (BSecureSocket*)SSL_get_ex_data(ssl, sDataIndex);
// Get the certificate that we could not validate (this may not be the one
// we got from the server, but something higher up in the certificate
// chain)
X509* certificate = X509_STORE_CTX_get_current_cert(ctx);
return socket->CertificateVerificationFailed(BCertificate(
new BCertificate::Private(certificate)));
}
// # pragma mark - BSecureSocket
BSecureSocket::BSecureSocket()
:
fPrivate(NULL)
@ -50,12 +131,14 @@ BSecureSocket::BSecureSocket(const BSecureSocket& other)
:
BSocket(other)
{
// TODO: this won't work this way!
fPrivate = (BSecureSocket::Private*)malloc(sizeof(BSecureSocket::Private));
if (fPrivate != NULL)
if (fPrivate != NULL) {
memcpy(fPrivate, other.fPrivate, sizeof(BSecureSocket::Private));
else
// TODO: this won't work this way!
SSL_set_ex_data(fPrivate->fSSL, Private::sDataIndex, this);
} else
fInitStatus = B_NO_MEMORY;
}
@ -79,16 +162,32 @@ BSecureSocket::Connect(const BNetworkAddress& peer, bigtime_t timeout)
if (status != B_OK)
return status;
fPrivate->fCTX = SSL_CTX_new(SSLv23_method());
fPrivate->fSSL = SSL_new(fPrivate->fCTX);
fPrivate->fSSL = SSL_new(BSecureSocket::Private::Context());
fPrivate->fBIO = BIO_new_socket(fSocket, BIO_NOCLOSE);
SSL_set_bio(fPrivate->fSSL, fPrivate->fBIO, fPrivate->fBIO);
SSL_set_ex_data(fPrivate->fSSL, Private::sDataIndex, this);
if (SSL_connect(fPrivate->fSSL) <= 0) {
printf("Connecting %p\n", fPrivate->fSSL);
int sslStatus = SSL_connect(fPrivate->fSSL);
if (sslStatus <= 0) {
TRACE("SSLConnection can't connect\n");
BSocket::Disconnect();
// TODO: translate ssl to Haiku error
return B_ERROR;
switch(SSL_get_error(fPrivate->fSSL, sslStatus))
{
case SSL_ERROR_NONE:
// Shouldn't happen...
return B_NO_ERROR;
case SSL_ERROR_ZERO_RETURN:
// Socket is closed
return B_CANCELED;
case SSL_ERROR_SSL:
// Probably no certificate
return B_NOT_ALLOWED;
default:
return B_ERROR;
}
}
return B_OK;
@ -103,10 +202,6 @@ BSecureSocket::Disconnect()
SSL_shutdown(fPrivate->fSSL);
fPrivate->fSSL = NULL;
}
if (fPrivate->fCTX != NULL) {
SSL_CTX_free(fPrivate->fCTX);
fPrivate->fCTX = NULL;
}
BSocket::Disconnect();
// Must do this before freeing the BIO, to make sure any pending
@ -134,6 +229,16 @@ BSecureSocket::WaitForReadable(bigtime_t timeout) const
}
bool
BSecureSocket::CertificateVerificationFailed(BCertificate)
{
// Until apps actually make use of the certificate API, let's keep the old
// behavior and accept all connections, even if the certificate validation
// didn't work.
return true;
}
// #pragma mark - BDataIO implementation