blob: 3af54b5825d3119615b3fad220feb1680ce64846 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
module thrift.internal.ssl;
import core.memory : GC;
import core.stdc.config;
import core.stdc.errno : errno;
import core.stdc.string : strerror;
import deimos.openssl.err;
import deimos.openssl.ssl;
import deimos.openssl.x509v3;
import std.array : empty, appender;
import std.conv : to;
import std.socket : Address;
import thrift.transport.ssl;
/**
* Checks if the peer is authorized after the SSL handshake has been
* completed on the given conncetion and throws an TSSLException if not.
*
* Params:
* ssl = The SSL connection to check.
* accessManager = The access manager to check the peer againts.
* peerAddress = The (IP) address of the peer.
* hostName = The host name of the peer.
*/
void authorize(SSL* ssl, TAccessManager accessManager,
Address peerAddress, lazy string hostName
) {
alias TAccessManager.Decision Decision;
auto rc = SSL_get_verify_result(ssl);
if (rc != X509_V_OK) {
throw new TSSLException("SSL_get_verify_result(): " ~
to!string(X509_verify_cert_error_string(rc)));
}
auto cert = SSL_get_peer_certificate(ssl);
if (cert is null) {
// Certificate is not present.
if (SSL_get_verify_mode(ssl) & SSL_VERIFY_FAIL_IF_NO_PEER_CERT) {
throw new TSSLException(
"Authorize: Required certificate not present.");
}
// If we don't have an access manager set, we don't intend to authorize
// the client, so everything's fine.
if (accessManager) {
throw new TSSLException(
"Authorize: Certificate required for authorization.");
}
return;
}
if (accessManager is null) {
// No access manager set, can return immediately as the cert is valid
// and all peers are authorized.
X509_free(cert);
return;
}
// both certificate and access manager are present
auto decision = accessManager.verify(peerAddress);
if (decision != Decision.SKIP) {
X509_free(cert);
if (decision != Decision.ALLOW) {
throw new TSSLException("Authorize: Access denied based on remote IP.");
}
return;
}
// Check subjectAltName(s), if present.
auto alternatives = cast(STACK_OF!(GENERAL_NAME)*)
X509_get_ext_d2i(cert, NID_subject_alt_name, null, null);
if (alternatives != null) {
auto count = sk_GENERAL_NAME_num(alternatives);
for (int i = 0; decision == Decision.SKIP && i < count; i++) {
auto name = sk_GENERAL_NAME_value(alternatives, i);
if (name is null) {
continue;
}
auto data = ASN1_STRING_data(name.d.ia5);
auto length = ASN1_STRING_length(name.d.ia5);
switch (name.type) {
case GENERAL_NAME.GEN_DNS:
decision = accessManager.verify(hostName, cast(char[])data[0 .. length]);
break;
case GENERAL_NAME.GEN_IPADD:
decision = accessManager.verify(peerAddress, data[0 .. length]);
break;
default:
// Do nothing.
}
}
// DMD @@BUG@@: Empty template arguments parens should not be needed.
sk_GENERAL_NAME_pop_free!()(alternatives, &GENERAL_NAME_free);
}
// If we are alredy done, return.
if (decision != Decision.SKIP) {
X509_free(cert);
if (decision != Decision.ALLOW) {
throw new TSSLException("Authorize: Access denied.");
}
return;
}
// Check commonName.
auto name = X509_get_subject_name(cert);
if (name !is null) {
X509_NAME_ENTRY* entry;
char* utf8;
int last = -1;
while (decision == Decision.SKIP) {
last = X509_NAME_get_index_by_NID(name, NID_commonName, last);
if (last == -1)
break;
entry = X509_NAME_get_entry(name, last);
if (entry is null)
continue;
auto common = X509_NAME_ENTRY_get_data(entry);
auto size = ASN1_STRING_to_UTF8(&utf8, common);
decision = accessManager.verify(hostName, utf8[0 .. size]);
CRYPTO_free(utf8);
}
}
X509_free(cert);
if (decision != Decision.ALLOW) {
throw new TSSLException("Authorize: Could not authorize peer.");
}
}
/*
* OpenSSL error information used for storing D exceptions on the OpenSSL
* error stack.
*/
enum ERR_LIB_D_EXCEPTION = ERR_LIB_USER;
enum ERR_F_D_EXCEPTION = 0; // function id - what to use here?
enum ERR_R_D_EXCEPTION = 1234; // 99 and above are reserved for applications
enum ERR_FILE_D_EXCEPTION = "d_exception";
enum ERR_LINE_D_EXCEPTION = 0;
enum ERR_FLAGS_D_EXCEPTION = 0;
/**
* Returns an exception for the last.
*
* Params:
* location = An optional "location" to add to the error message (typically
* the last SSL API call).
*/
Exception getSSLException(string location = null, string clientFile = __FILE__,
size_t clientLine = __LINE__
) {
// We can return either an exception saved from D BIO code, or a "true"
// OpenSSL error. Because there can possibly be more than one error on the
// error stack, we have to fetch all of them, and pick the last, i.e. newest
// one. We concatenate multiple successive OpenSSL error messages into a
// single one, but always just return the last D expcetion.
string message; // Probably better use an Appender here.
bool hadMessage;
Exception exception;
void initMessage() {
message.destroy();
hadMessage = false;
if (!location.empty) {
message ~= location;
message ~= ": ";
}
}
initMessage();
auto errn = errno;
const(char)* file = void;
int line = void;
const(char)* data = void;
int flags = void;
c_ulong code = void;
while ((code = ERR_get_error_line_data(&file, &line, &data, &flags)) != 0) {
if (ERR_GET_REASON(code) == ERR_R_D_EXCEPTION) {
initMessage();
GC.removeRoot(cast(void*)data);
exception = cast(Exception)data;
} else {
exception = null;
if (hadMessage) {
message ~= ", ";
}
auto reason = ERR_reason_error_string(code);
if (reason) {
message ~= "SSL error: " ~ to!string(reason);
} else {
message ~= "SSL error #" ~ to!string(code);
}
hadMessage = true;
}
}
// If the last item from the stack was a D exception, throw it.
if (exception) return exception;
// We are dealing with an OpenSSL error that doesn't root in a D exception.
if (!hadMessage) {
// If we didn't get an actual error from the stack yet, try errno.
string errnString;
if (errn != 0) {
errnString = to!string(strerror(errn));
}
if (errnString.empty) {
message ~= "Unknown error";
} else {
message ~= errnString;
}
}
message ~= ".";
return new TSSLException(message, clientFile, clientLine);
}