1025 lines
50 KiB
C++
1025 lines
50 KiB
C++
/*
|
|
Copyright 2015-2016 Amebis
|
|
Copyright 2016 GÉANT
|
|
|
|
This file is part of GÉANTLink.
|
|
|
|
GÉANTLink is free software: you can redistribute it and/or modify it
|
|
under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
GÉANTLink is distributed in the hope that it will be useful, but
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with GÉANTLink. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "StdAfx.h"
|
|
|
|
#pragma comment(lib, "Secur32.lib")
|
|
|
|
using namespace std;
|
|
using namespace winstd;
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// eap::method_defrag
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
eap::method_defrag::method_defrag(_In_ module &mod, _In_ method *inner) :
|
|
m_send_res(false),
|
|
method_tunnel(mod, inner)
|
|
{
|
|
}
|
|
|
|
|
|
eap::method_defrag::method_defrag(_Inout_ method_defrag &&other) :
|
|
m_data_req (std::move(other.m_data_req)),
|
|
m_data_res (std::move(other.m_data_res)),
|
|
m_send_res (std::move(other.m_send_res)),
|
|
method_tunnel(std::move(other ))
|
|
{
|
|
}
|
|
|
|
|
|
eap::method_defrag& eap::method_defrag::operator=(_Inout_ method_defrag &&other)
|
|
{
|
|
if (this != std::addressof(other)) {
|
|
(method_tunnel&)*this = std::move(other );
|
|
m_data_req = std::move(other.m_data_req);
|
|
m_data_res = std::move(other.m_data_res);
|
|
m_send_res = std::move(other.m_send_res);
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
|
|
void eap::method_defrag::begin_session(
|
|
_In_ DWORD dwFlags,
|
|
_In_ const EapAttributes *pAttributeArray,
|
|
_In_ HANDLE hTokenImpersonateUser,
|
|
_In_opt_ DWORD dwMaxSendPacketSize)
|
|
{
|
|
// Initialize tunnel method session only.
|
|
method::begin_session(dwFlags, pAttributeArray, hTokenImpersonateUser, dwMaxSendPacketSize);
|
|
|
|
// Inner method can generate packets of up to 4GB.
|
|
assert(m_inner);
|
|
m_inner->begin_session(dwFlags, pAttributeArray, hTokenImpersonateUser, MAXDWORD);
|
|
}
|
|
|
|
|
|
EapPeerMethodResponseAction eap::method_defrag::process_request_packet(
|
|
_In_bytecount_(dwReceivedPacketSize) const void *pReceivedPacket,
|
|
_In_ DWORD dwReceivedPacketSize)
|
|
{
|
|
assert(dwReceivedPacketSize >= 1); // Request packet should contain flags at least.
|
|
auto data_packet = reinterpret_cast<const unsigned char*>(pReceivedPacket);
|
|
|
|
// Get packet content pointer and size for more readable code later on.
|
|
const unsigned char *data_content;
|
|
size_t size_content;
|
|
if (data_packet[0] & flags_req_length_incl) {
|
|
// Length field is included.
|
|
data_content = data_packet + 5;
|
|
size_content = dwReceivedPacketSize - 5;
|
|
} else {
|
|
// Length field not included.
|
|
data_content = data_packet + 1;
|
|
size_content = dwReceivedPacketSize - 1;
|
|
}
|
|
|
|
// Do the defragmentation.
|
|
if (data_packet[0] & flags_req_more_frag) {
|
|
if (m_data_req.empty()) {
|
|
// Start a new packet.
|
|
if (data_packet[0] & flags_req_length_incl) {
|
|
// Preallocate data according to the Length field.
|
|
m_data_req.reserve(ntohl(*reinterpret_cast<const unsigned int*>(data_packet + 1)));
|
|
}
|
|
}
|
|
m_data_req.insert(m_data_req.end(), data_content, data_content + size_content);
|
|
|
|
// Respond with ACK packet (empty packet).
|
|
m_data_res.clear();
|
|
m_send_res = true;
|
|
return EapPeerMethodResponseActionSend;
|
|
} else if (!m_data_req.empty()) {
|
|
// Last fragment received. Append data.
|
|
m_data_req.insert(m_data_req.end(), data_content, data_content + size_content);
|
|
} else {
|
|
// This is a complete non-fragmented packet.
|
|
m_data_req.assign(data_content, data_content + size_content);
|
|
}
|
|
|
|
if (m_send_res) {
|
|
// We are sending a fragmented message.
|
|
if (m_data_req.empty() && (data_packet[0] & (flags_req_length_incl | flags_req_more_frag | flags_req_start)) == 0) {
|
|
// Received packet is the ACK of our fragmented message packet. Send the next fragment.
|
|
return EapPeerMethodResponseActionSend;
|
|
} else
|
|
throw win_runtime_error(EAP_E_EAPHOST_METHOD_INVALID_PACKET, __FUNCTION__ " ACK expected.");
|
|
}
|
|
|
|
// Process the data with underlying method.
|
|
auto action = method_tunnel::process_request_packet(m_data_req.data(), (DWORD)m_data_req.size());
|
|
|
|
// Packet was processed. Clear its data since we use the absence of data to detect first of fragmented message packages.
|
|
m_data_req.clear();
|
|
return action;
|
|
}
|
|
|
|
|
|
void eap::method_defrag::get_response_packet(
|
|
_Out_ sanitizing_blob &packet,
|
|
_In_opt_ DWORD size_max)
|
|
{
|
|
assert(size_max > 5); // We can not do the fragmentation if we have less space than flags+length+at least one byte of data.
|
|
|
|
if (!m_send_res) {
|
|
// Get data from underlying method.
|
|
method_tunnel::get_response_packet(m_data_res, MAXDWORD);
|
|
}
|
|
|
|
size_t size_data = m_data_res.size();
|
|
assert(size_data <= MAXDWORD); // Packets spanning over 4GB are not supported.
|
|
|
|
packet.clear();
|
|
if (size_data + 1 > size_max) {
|
|
// Write one fragment.
|
|
packet.push_back(flags_res_length_incl | flags_res_more_frag);
|
|
unsigned int length = htonl((unsigned int)size_data);
|
|
packet.insert(packet.end(), reinterpret_cast<const unsigned char*>(&length), reinterpret_cast<const unsigned char*>(&length + 1));
|
|
auto data_begin = m_data_res.begin() + 0, data_end = data_begin + (size_max - 5);
|
|
packet.insert(packet.end(), data_begin, data_end);
|
|
m_data_res.erase(data_begin, data_end);
|
|
m_send_res = true;
|
|
} else {
|
|
// Write single/last fragment.
|
|
packet.push_back(0);
|
|
packet.insert(packet.end(), m_data_res.begin(), m_data_res.end());
|
|
m_data_res.clear();
|
|
m_send_res = false;
|
|
}
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// eap::method_eapmsg
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
eap::method_eapmsg::method_eapmsg(_In_ module &mod, _In_ const wchar_t *identity, _In_ method *inner) :
|
|
m_identity(identity),
|
|
m_phase(phase_unknown),
|
|
method_tunnel(mod, inner)
|
|
{
|
|
}
|
|
|
|
|
|
eap::method_eapmsg::method_eapmsg(_Inout_ method_eapmsg &&other) :
|
|
m_identity (std::move(other.m_identity )),
|
|
m_phase (std::move(other.m_phase )),
|
|
m_packet_res (std::move(other.m_packet_res)),
|
|
method_tunnel(std::move(other ))
|
|
{
|
|
}
|
|
|
|
|
|
eap::method_eapmsg& eap::method_eapmsg::operator=(_Inout_ method_eapmsg &&other)
|
|
{
|
|
if (this != std::addressof(other)) {
|
|
(method_tunnel&)*this = std::move(other );
|
|
m_identity = std::move(other.m_identity );
|
|
m_phase = std::move(other.m_phase );
|
|
m_packet_res = std::move(other.m_packet_res);
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
|
|
void eap::method_eapmsg::begin_session(
|
|
_In_ DWORD dwFlags,
|
|
_In_ const EapAttributes *pAttributeArray,
|
|
_In_ HANDLE hTokenImpersonateUser,
|
|
_In_opt_ DWORD dwMaxSendPacketSize)
|
|
{
|
|
// Initialize tunnel method session only.
|
|
method::begin_session(dwFlags, pAttributeArray, hTokenImpersonateUser, dwMaxSendPacketSize);
|
|
|
|
// Inner method can generate packets of up to 16MB (less the Diameter AVP header).
|
|
// Initialize inner method with appropriately less packet size maximum.
|
|
if (dwMaxSendPacketSize < sizeof(diameter_avp_header))
|
|
throw invalid_argument(string_printf(__FUNCTION__ " Maximum packet size too small (minimum: %u, available: %u).", sizeof(diameter_avp_header) + 1, dwMaxSendPacketSize));
|
|
assert(m_inner);
|
|
m_inner->begin_session(dwFlags, pAttributeArray, hTokenImpersonateUser, std::min<DWORD>(dwMaxSendPacketSize, 0xffffff) - sizeof(diameter_avp_header));
|
|
|
|
m_phase = phase_identity;
|
|
}
|
|
|
|
|
|
EapPeerMethodResponseAction eap::method_eapmsg::process_request_packet(
|
|
_In_bytecount_(dwReceivedPacketSize) const void *pReceivedPacket,
|
|
_In_ DWORD dwReceivedPacketSize)
|
|
{
|
|
switch (m_phase) {
|
|
case phase_identity: {
|
|
// Convert identity to UTF-8.
|
|
sanitizing_string identity_utf8;
|
|
WideCharToMultiByte(CP_UTF8, 0, m_identity.c_str(), -1, identity_utf8, NULL, NULL);
|
|
|
|
// Build EAP-Response/Identity packet.
|
|
auto size_identity = identity_utf8.length(), size_packet = size_identity + sizeof(EapPacket);
|
|
assert(size_packet <= MAXWORD); // Packets spanning over 64kB are not supported.
|
|
eap_packet pck;
|
|
if (!pck.create(EapCodeResponse, 0, (WORD)size_packet))
|
|
throw win_runtime_error(__FUNCTION__ " EapPacket creation failed.");
|
|
pck->Data[0] = eap_type_identity;
|
|
memcpy(pck->Data + 1, identity_utf8.data(), size_identity);
|
|
|
|
// Diameter AVP (EAP-Message=79)
|
|
m_packet_res.clear();
|
|
diameter_avp_append(79, diameter_avp_flag_mandatory, (const EapPacket*)pck, (unsigned int)size_packet, m_packet_res);
|
|
|
|
m_phase = phase_finished;
|
|
return EapPeerMethodResponseActionSend;
|
|
}
|
|
|
|
case phase_finished: {
|
|
EapPeerMethodResponseAction action = EapPeerMethodResponseActionNone;
|
|
bool eap_message_found = false;
|
|
|
|
// Parse Diameter AVP(s).
|
|
// Process the first EAP-Message, but keep iterating over all to check if there is any additional mandatory AVP we should process.
|
|
for (const unsigned char *pck = reinterpret_cast<const unsigned char*>(pReceivedPacket), *pck_end = pck + dwReceivedPacketSize; pck < pck_end; ) {
|
|
if (pck + sizeof(diameter_avp_header) > pck_end)
|
|
throw win_runtime_error(EAP_E_EAPHOST_METHOD_INVALID_PACKET, __FUNCTION__ " Incomplete message header.");
|
|
const diameter_avp_header *hdr = reinterpret_cast<const diameter_avp_header*>(pck);
|
|
unsigned int size_msg = ntoh24(hdr->length);
|
|
const unsigned char
|
|
*msg = reinterpret_cast<const unsigned char*>(hdr + 1),
|
|
*msg_end = pck + size_msg,
|
|
*msg_next = msg_end + (unsigned int)((4 - size_msg) % 4);
|
|
if (msg_end > pck_end)
|
|
throw win_runtime_error(EAP_E_EAPHOST_METHOD_INVALID_PACKET, __FUNCTION__ " Incomplete message data.");
|
|
unsigned int code = ntohl(*reinterpret_cast<const unsigned int*>(hdr->code));
|
|
|
|
switch (code) {
|
|
case 79: // EAP-Message
|
|
if (!eap_message_found) {
|
|
action = method_tunnel::process_request_packet(msg, (DWORD)(msg_end - msg));
|
|
eap_message_found = true;
|
|
break;
|
|
}
|
|
// Do not break out of this case to allow continuing with the following case, checking there is no second mandatory EAP-Message present.
|
|
|
|
default:
|
|
if (hdr->flags & diameter_avp_flag_mandatory)
|
|
throw win_runtime_error(EAP_E_EAPHOST_METHOD_INVALID_PACKET, string_printf(__FUNCTION__ " Unsupported mandatory Diameter AVP (code %u).", code));
|
|
}
|
|
|
|
pck = msg_next;
|
|
}
|
|
|
|
// Signal get_response_packet() method we did not generate any response data to proxy inner method.
|
|
m_packet_res.clear();
|
|
|
|
return action;
|
|
}
|
|
|
|
default:
|
|
throw invalid_argument(string_printf(__FUNCTION__ " Unknown phase (phase %u).", m_phase).c_str());
|
|
}
|
|
}
|
|
|
|
|
|
void eap::method_eapmsg::get_response_packet(
|
|
_Out_ sanitizing_blob &packet,
|
|
_In_opt_ DWORD size_max)
|
|
{
|
|
if (m_packet_res.empty()) {
|
|
assert(size_max >= sizeof(diameter_avp_header)); // We should be able to respond with at least Diameter AVP header.
|
|
|
|
if (size_max > 0xffffff) size_max = 0xffffff; // Diameter AVP maximum size is 16MB.
|
|
|
|
// Get data from underlying method.
|
|
method_tunnel::get_response_packet(packet, size_max - sizeof(diameter_avp_header));
|
|
|
|
// Prepare EAP-Message Diameter AVP header.
|
|
diameter_avp_header hdr;
|
|
*reinterpret_cast<unsigned int*>(hdr.code) = htonl(79);
|
|
hdr.flags = diameter_avp_flag_mandatory;
|
|
size_t size_packet = packet.size() + sizeof(diameter_avp_header);
|
|
assert(size_packet <= 0xffffff); // Packets spanning over 16MB are not supported.
|
|
hton24((unsigned int)size_packet, hdr.length);
|
|
|
|
// Insert EAP header before data.
|
|
packet.insert(packet.begin(), reinterpret_cast<const unsigned char*>(&hdr), reinterpret_cast<const unsigned char*>(&hdr + 1));
|
|
|
|
// Add padding.
|
|
packet.insert(packet.end(), (unsigned int)((4 - size_packet) % 4), 0);
|
|
} else {
|
|
if (m_packet_res.size() > size_max)
|
|
throw invalid_argument(string_printf(__FUNCTION__ " This method does not support packet fragmentation, but the data size is too big to fit in one packet (packet: %u, maximum: %u).", m_packet_res.size(), size_max).c_str());
|
|
|
|
packet.assign(m_packet_res.begin(), m_packet_res.end());
|
|
}
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// eap::method_ttls
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
eap::method_ttls::method_ttls(_In_ module &mod, _In_ config_method_ttls &cfg, _In_ credentials_ttls &cred, _In_ method *inner) :
|
|
m_cfg(cfg),
|
|
m_cred(cred),
|
|
m_user_ctx(NULL),
|
|
m_phase(phase_unknown),
|
|
m_packet_res_inner(false),
|
|
method_tunnel(mod, inner)
|
|
{
|
|
}
|
|
|
|
|
|
eap::method_ttls::method_ttls(_Inout_ method_ttls &&other) :
|
|
m_cfg ( other.m_cfg ),
|
|
m_cred ( other.m_cred ),
|
|
m_user_ctx (std::move(other.m_user_ctx )),
|
|
m_sc_target_name (std::move(other.m_sc_target_name )),
|
|
m_sc_cred (std::move(other.m_sc_cred )),
|
|
m_sc_queue (std::move(other.m_sc_queue )),
|
|
m_sc_ctx (std::move(other.m_sc_ctx )),
|
|
m_phase (std::move(other.m_phase )),
|
|
m_packet_res (std::move(other.m_packet_res )),
|
|
m_packet_res_inner(std::move(other.m_packet_res_inner)),
|
|
m_eap_attr (std::move(other.m_eap_attr )),
|
|
method_tunnel (std::move(other ))
|
|
{
|
|
}
|
|
|
|
|
|
eap::method_ttls& eap::method_ttls::operator=(_Inout_ method_ttls &&other)
|
|
{
|
|
if (this != std::addressof(other)) {
|
|
assert(std::addressof(m_cfg ) == std::addressof(other.m_cfg )); // Move method within same configuration only!
|
|
assert(std::addressof(m_cred) == std::addressof(other.m_cred)); // Move method within same credentials only!
|
|
(method_tunnel&)*this = std::move(other );
|
|
m_user_ctx = std::move(other.m_user_ctx );
|
|
m_sc_target_name = std::move(other.m_sc_target_name );
|
|
m_sc_cred = std::move(other.m_sc_cred );
|
|
m_sc_queue = std::move(other.m_sc_queue );
|
|
m_sc_ctx = std::move(other.m_sc_ctx );
|
|
m_phase = std::move(other.m_phase );
|
|
m_packet_res = std::move(other.m_packet_res );
|
|
m_packet_res_inner = std::move(other.m_packet_res_inner);
|
|
m_eap_attr = std::move(other.m_eap_attr );
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
|
|
void eap::method_ttls::begin_session(
|
|
_In_ DWORD dwFlags,
|
|
_In_ const EapAttributes *pAttributeArray,
|
|
_In_ HANDLE hTokenImpersonateUser,
|
|
_In_opt_ DWORD dwMaxSendPacketSize)
|
|
{
|
|
// In TLS, maximum packet length can precisely be calculated only after handshake is complete.
|
|
// Therefore, we allow inner method same maximum packet size as this method.
|
|
// Initialize tunnel and inner method session with same parameters.
|
|
method_tunnel::begin_session(dwFlags, pAttributeArray, hTokenImpersonateUser, dwMaxSendPacketSize);
|
|
|
|
// Presume authentication will fail with generic protocol failure. (Pesimist!!!)
|
|
// We will reset once we get get_result(Success) call.
|
|
m_cfg.m_last_status = config_method::status_auth_failed;
|
|
m_cfg.m_last_msg.clear();
|
|
|
|
m_user_ctx = hTokenImpersonateUser;
|
|
user_impersonator impersonating(m_user_ctx);
|
|
|
|
// Build (expected) server name(s) for Schannel.
|
|
m_sc_target_name.clear();
|
|
for (auto name = m_cfg.m_server_names.cbegin(), name_end = m_cfg.m_server_names.cend(); name != name_end; ++name) {
|
|
if (name != m_cfg.m_server_names.cbegin())
|
|
m_sc_target_name += _T(';');
|
|
#ifdef _UNICODE
|
|
m_sc_target_name.insert(m_sc_target_name.end(), name->begin(), name->end());
|
|
#else
|
|
string buf;
|
|
WideCharToMultiByte(CP_ACP, 0, name->c_str(), -1, buf, NULL, NULL);
|
|
m_sc_target_name.insert(m_sc_target_name.end(), buf.begin(), buf.end());
|
|
#endif
|
|
}
|
|
|
|
// Prepare client credentials for Schannel.
|
|
PCCERT_CONTEXT certs[] = { m_cred.m_cert ? (PCCERT_CONTEXT)m_cred.m_cert : NULL };
|
|
SCHANNEL_CRED cred = {
|
|
SCHANNEL_CRED_VERSION, // dwVersion
|
|
m_cred.m_cert ? 1 : 0, // cCreds
|
|
certs, // paCred
|
|
NULL, // hRootStore: Not valid for client credentials
|
|
0, // cMappers
|
|
NULL, // aphMappers
|
|
0, // cSupportedAlgs: Use system configured default
|
|
NULL, // palgSupportedAlgs: Use system configured default
|
|
SP_PROT_TLS1_X_CLIENT | (SP_PROT_TLS1_2_CLIENT<<2), // grbitEnabledProtocols: TLS 1.x
|
|
0, // dwMinimumCipherStrength: Use system configured default
|
|
0, // dwMaximumCipherStrength: Use system configured default
|
|
0, // dwSessionLifespan: Use system configured default = 10hr
|
|
#if EAP_TLS >= EAP_TLS_SCHANNEL_FULL
|
|
SCH_CRED_AUTO_CRED_VALIDATION | // dwFlags: Let Schannel verify server certificate
|
|
#else
|
|
SCH_CRED_MANUAL_CRED_VALIDATION | // dwFlags: Prevent Schannel verify server certificate (we want to use custom root CA store and multiple name checking)
|
|
#endif
|
|
SCH_CRED_CACHE_ONLY_URL_RETRIEVAL_ON_CREATE | // dwFlags: Do not attempt online revocation check - we do not expect to have network connection yet
|
|
SCH_CRED_IGNORE_NO_REVOCATION_CHECK | // dwFlags: Ignore no-revocation-check errors - as we cannot check for revocation, it makes little sense to insist certificate has to have revocation set-up
|
|
SCH_CRED_IGNORE_REVOCATION_OFFLINE | // dwFlags: Ignore offline-revocation errors - we do not expect to have network connection yet
|
|
SCH_CRED_NO_DEFAULT_CREDS | // dwFlags: If client certificate we provided is not acceptable, do not try to select one on your own
|
|
(m_cfg.m_server_names.empty() ? SCH_CRED_NO_SERVERNAME_CHECK : 0) | // dwFlags: When no expected server name is given, do not do the server name check.
|
|
0x00400000 /*SCH_USE_STRONG_CRYPTO*/, // dwFlags: Do not use broken ciphers
|
|
0 // dwCredFormat
|
|
};
|
|
SECURITY_STATUS stat = m_sc_cred.acquire(NULL, UNISP_NAME, SECPKG_CRED_OUTBOUND, NULL, &cred);
|
|
if (FAILED(stat))
|
|
throw sec_runtime_error(stat, __FUNCTION__ " Error acquiring Schannel credentials handle.");
|
|
|
|
m_phase = phase_handshake_init;
|
|
}
|
|
|
|
|
|
EapPeerMethodResponseAction eap::method_ttls::process_request_packet(
|
|
_In_bytecount_(dwReceivedPacketSize) const void *pReceivedPacket,
|
|
_In_ DWORD dwReceivedPacketSize)
|
|
{
|
|
assert(pReceivedPacket || dwReceivedPacketSize == 0);
|
|
|
|
user_impersonator impersonating(m_user_ctx);
|
|
|
|
switch (m_phase) {
|
|
case phase_handshake_init: {
|
|
// Prepare input buffer(s).
|
|
SecBuffer buf_in[] = {
|
|
{ (unsigned long)dwReceivedPacketSize, SECBUFFER_TOKEN, const_cast<void*>(pReceivedPacket) },
|
|
{ 0, SECBUFFER_EMPTY, NULL },
|
|
};
|
|
SecBufferDesc buf_in_desc = { SECBUFFER_VERSION, _countof(buf_in), buf_in };
|
|
|
|
// Prepare output buffer(s).
|
|
SecBuffer buf_out[] = {
|
|
{ 0, SECBUFFER_TOKEN, NULL },
|
|
{ 0, SECBUFFER_ALERT, NULL },
|
|
};
|
|
sec_buffer_desc buf_out_desc(buf_out, _countof(buf_out));
|
|
|
|
// Initialize Schannel security context and process initial data.
|
|
SECURITY_STATUS status = m_sc_ctx.initialize(
|
|
m_sc_cred,
|
|
!m_sc_target_name.empty() ? m_sc_target_name.c_str() : NULL,
|
|
ISC_REQ_REPLAY_DETECT | ISC_REQ_SEQUENCE_DETECT | ISC_REQ_CONFIDENTIALITY | ISC_REQ_INTEGRITY | ISC_REQ_STREAM | /*ISC_REQ_USE_SUPPLIED_CREDS |*/ ISC_REQ_EXTENDED_ERROR | ISC_REQ_ALLOCATE_MEMORY,
|
|
0,
|
|
&buf_in_desc,
|
|
&buf_out_desc);
|
|
|
|
// In a desparate attempt to make Schannel remember and resume the TLS session, we send it the SCHANNEL_SESSION_TOKEN/SSL_SESSION_ENABLE_RECONNECTS
|
|
SCHANNEL_SESSION_TOKEN token_session = { SCHANNEL_SESSION, SSL_SESSION_ENABLE_RECONNECTS };
|
|
SecBuffer token[] = { { sizeof(token_session), SECBUFFER_TOKEN, &token_session } };
|
|
SecBufferDesc token_desc = { SECBUFFER_VERSION, _countof(token), token };
|
|
ApplyControlToken(m_sc_ctx, &token_desc);
|
|
|
|
if (status == SEC_I_CONTINUE_NEEDED) {
|
|
// Send Schannel's token.
|
|
assert(buf_out[0].BufferType == SECBUFFER_TOKEN);
|
|
assert(m_sc_ctx.m_attrib & ISC_RET_ALLOCATED_MEMORY);
|
|
m_packet_res.assign(reinterpret_cast<const unsigned char*>(buf_out[0].pvBuffer), reinterpret_cast<const unsigned char*>(buf_out[0].pvBuffer) + buf_out[0].cbBuffer);
|
|
if (buf_in[1].BufferType == SECBUFFER_EXTRA) {
|
|
// Server appended extra data.
|
|
m_sc_queue.assign(reinterpret_cast<const unsigned char*>(pReceivedPacket) + dwReceivedPacketSize - buf_in[1].cbBuffer, reinterpret_cast<const unsigned char*>(pReceivedPacket) + dwReceivedPacketSize);
|
|
} else
|
|
m_sc_queue.clear();
|
|
|
|
m_phase = phase_handshake_cont;
|
|
m_packet_res_inner = false;
|
|
return EapPeerMethodResponseActionSend;
|
|
} else if (FAILED(status)) {
|
|
if (m_sc_ctx.m_attrib & ISC_RET_EXTENDED_ERROR) {
|
|
// Send alert.
|
|
assert(buf_out[1].BufferType == SECBUFFER_ALERT);
|
|
assert(m_sc_ctx.m_attrib & ISC_RET_ALLOCATED_MEMORY);
|
|
m_packet_res.assign(reinterpret_cast<const unsigned char*>(buf_out[1].pvBuffer), reinterpret_cast<const unsigned char*>(buf_out[1].pvBuffer) + buf_out[1].cbBuffer);
|
|
m_packet_res_inner = false;
|
|
return EapPeerMethodResponseActionSend;
|
|
} else
|
|
throw sec_runtime_error(status, __FUNCTION__ " Schannel error.");
|
|
} else
|
|
throw sec_runtime_error(status, __FUNCTION__ " Unexpected Schannel result.");
|
|
}
|
|
|
|
case phase_handshake_cont: {
|
|
m_sc_queue.insert(m_sc_queue.end(), reinterpret_cast<const unsigned char*>(pReceivedPacket), reinterpret_cast<const unsigned char*>(pReceivedPacket) + dwReceivedPacketSize);
|
|
|
|
// Prepare input buffer(s).
|
|
SecBuffer buf_in[] = {
|
|
{ (unsigned long)m_sc_queue.size(), SECBUFFER_TOKEN, m_sc_queue.data() },
|
|
{ 0, SECBUFFER_EMPTY, NULL },
|
|
};
|
|
SecBufferDesc buf_in_desc = { SECBUFFER_VERSION, _countof(buf_in), buf_in };
|
|
|
|
// Prepare output buffer(s).
|
|
SecBuffer buf_out[] = {
|
|
{ 0, SECBUFFER_TOKEN, NULL },
|
|
{ 0, SECBUFFER_ALERT, NULL },
|
|
};
|
|
sec_buffer_desc buf_out_desc(buf_out, _countof(buf_out));
|
|
|
|
// Process Schannel data.
|
|
SECURITY_STATUS status = m_sc_ctx.process(
|
|
m_sc_cred,
|
|
!m_sc_target_name.empty() ? m_sc_target_name.c_str() : NULL,
|
|
ISC_REQ_REPLAY_DETECT | ISC_REQ_SEQUENCE_DETECT | ISC_REQ_CONFIDENTIALITY | ISC_REQ_INTEGRITY | ISC_REQ_STREAM | /*ISC_REQ_USE_SUPPLIED_CREDS |*/ ISC_REQ_EXTENDED_ERROR | ISC_REQ_ALLOCATE_MEMORY,
|
|
0,
|
|
&buf_in_desc,
|
|
&buf_out_desc);
|
|
|
|
#if EAP_TLS < EAP_TLS_SCHANNEL_FULL
|
|
if (status == SEC_E_OK)
|
|
verify_server_trust();
|
|
#endif
|
|
|
|
if (status == SEC_E_OK || status == SEC_I_CONTINUE_NEEDED) {
|
|
// Send Schannel's token.
|
|
assert(buf_out[0].BufferType == SECBUFFER_TOKEN);
|
|
assert(m_sc_ctx.m_attrib & ISC_RET_ALLOCATED_MEMORY);
|
|
m_packet_res.assign(reinterpret_cast<const unsigned char*>(buf_out[0].pvBuffer), reinterpret_cast<const unsigned char*>(buf_out[0].pvBuffer) + buf_out[0].cbBuffer);
|
|
if (buf_in[1].BufferType == SECBUFFER_EXTRA) {
|
|
// Server appended extra data.
|
|
m_sc_queue.erase(m_sc_queue.begin(), m_sc_queue.end() - buf_in[1].cbBuffer);
|
|
} else
|
|
m_sc_queue.clear();
|
|
|
|
if (status == SEC_I_CONTINUE_NEEDED) {
|
|
// Blame credentials if we fail beyond this point.
|
|
m_cfg.m_last_status = config_method::status_cred_invalid;
|
|
m_packet_res_inner = false;
|
|
} else {
|
|
SecPkgContext_Authority auth;
|
|
if (FAILED(status = QueryContextAttributes(m_sc_ctx, SECPKG_ATTR_AUTHORITY, &auth))) {
|
|
m_module.log_event(&EAPMETHOD_TLS_QUERY_FAILED, event_data((unsigned int)SECPKG_ATTR_AUTHORITY), event_data(status), event_data::blank);
|
|
auth.sAuthorityName = _T("");
|
|
}
|
|
|
|
SecPkgContext_ConnectionInfo info;
|
|
if (SUCCEEDED(status = QueryContextAttributes(m_sc_ctx, SECPKG_ATTR_CONNECTION_INFO, &info)))
|
|
m_module.log_event(&EAPMETHOD_TLS_HANDSHAKE_FINISHED,
|
|
event_data((unsigned int)eap_type_tls),
|
|
event_data(auth.sAuthorityName),
|
|
event_data(info.dwProtocol),
|
|
event_data(info.aiCipher),
|
|
event_data(info.dwCipherStrength),
|
|
event_data(info.aiHash),
|
|
event_data(info.dwHashStrength),
|
|
event_data(info.aiExch),
|
|
event_data(info.dwExchStrength),
|
|
event_data::blank);
|
|
else
|
|
m_module.log_event(&EAPMETHOD_TLS_QUERY_FAILED, event_data((unsigned int)SECPKG_ATTR_CONNECTION_INFO), event_data(status), event_data::blank);
|
|
|
|
m_phase = phase_finished;
|
|
|
|
method_mschapv2 *inner_mschapv2 = dynamic_cast<method_mschapv2*>(m_inner.get());
|
|
if (inner_mschapv2) {
|
|
// Push keying material to inner MSCHAPv2 method.
|
|
static const DWORD s_key_id = 0x02; // EAP-TTLSv0 Challenge Data
|
|
static const SecPkgContext_EapPrfInfo s_prf_info = { 0, sizeof(s_key_id), (PBYTE)&s_key_id };
|
|
SECURITY_STATUS status = SetContextAttributes(m_sc_ctx, SECPKG_ATTR_EAP_PRF_INFO, (void*)&s_prf_info, sizeof(s_prf_info));
|
|
if (FAILED(status))
|
|
throw sec_runtime_error(status, __FUNCTION__ " Error setting TTLS PRF in Schannel.");
|
|
|
|
SecPkgContext_EapKeyBlock key_block;
|
|
status = QueryContextAttributes(m_sc_ctx, SECPKG_ATTR_EAP_KEY_BLOCK, &key_block);
|
|
if (FAILED(status))
|
|
throw sec_runtime_error(status, __FUNCTION__ " Error generating PRF in Schannel.");
|
|
|
|
memcpy(&inner_mschapv2->m_challenge_server, key_block.rgbKeys, sizeof(challenge_mschapv2));
|
|
inner_mschapv2->m_ident = key_block.rgbKeys[sizeof(challenge_mschapv2) + 0];
|
|
|
|
SecureZeroMemory(&key_block, sizeof(key_block));
|
|
}
|
|
|
|
// Piggyback initial inner response.
|
|
if (!(m_sc_ctx.m_attrib & ISC_RET_CONFIDENTIALITY))
|
|
throw runtime_error(__FUNCTION__ " Connection is not encrypted.");
|
|
|
|
EapPeerMethodResponseAction action = EapPeerMethodResponseActionDiscard;
|
|
if (m_sc_queue.empty()) {
|
|
// No extra initial data for inner authentication avaliable.
|
|
action = method_tunnel::process_request_packet(NULL, 0);
|
|
} else {
|
|
// Authenticator sent some data for inner authentication. Unencrypt it.
|
|
|
|
// Decrypt the message.
|
|
SecBuffer buf[] = {
|
|
{ (unsigned long)m_sc_queue.size(), SECBUFFER_DATA, m_sc_queue.data() },
|
|
{ 0, SECBUFFER_EMPTY, NULL },
|
|
{ 0, SECBUFFER_EMPTY, NULL },
|
|
{ 0, SECBUFFER_EMPTY, NULL },
|
|
};
|
|
SecBufferDesc buf_desc = { SECBUFFER_VERSION, _countof(buf), buf };
|
|
SECURITY_STATUS status = DecryptMessage(m_sc_ctx, &buf_desc, 0, NULL);
|
|
if (status == SEC_E_OK) {
|
|
// Process data (only the first SECBUFFER_DATA found).
|
|
for (size_t i = 0; i < _countof(buf); i++)
|
|
if (buf[i].BufferType == SECBUFFER_DATA) {
|
|
action = method_tunnel::process_request_packet(buf[i].pvBuffer, buf[i].cbBuffer);
|
|
break;
|
|
}
|
|
|
|
// Queue remaining data for the next time.
|
|
m_sc_queue.clear();
|
|
for (size_t i = 0; i < _countof(buf); i++)
|
|
if (buf[i].BufferType == SECBUFFER_EXTRA)
|
|
m_sc_queue.insert(m_sc_queue.end(), reinterpret_cast<const unsigned char*>(buf[i].pvBuffer), reinterpret_cast<const unsigned char*>(buf[i].pvBuffer) + buf[i].cbBuffer);
|
|
} else if (FAILED(status))
|
|
throw sec_runtime_error(status, __FUNCTION__ " Schannel error.");
|
|
else
|
|
throw sec_runtime_error(status, __FUNCTION__ " Unexpected Schannel result.");
|
|
}
|
|
switch (action) {
|
|
case EapPeerMethodResponseActionSend: m_packet_res_inner = true ; break;
|
|
case EapPeerMethodResponseActionNone: m_packet_res_inner = false; break;
|
|
default : throw invalid_argument(string_printf(__FUNCTION__ " Inner method returned an unsupported action (action %u).", action).c_str());
|
|
}
|
|
}
|
|
return EapPeerMethodResponseActionSend;
|
|
} else if (FAILED(status)) {
|
|
if (m_sc_ctx.m_attrib & ISC_RET_EXTENDED_ERROR) {
|
|
// Send alert.
|
|
assert(buf_out[1].BufferType == SECBUFFER_ALERT);
|
|
assert(m_sc_ctx.m_attrib & ISC_RET_ALLOCATED_MEMORY);
|
|
m_packet_res.assign(reinterpret_cast<const unsigned char*>(buf_out[1].pvBuffer), reinterpret_cast<const unsigned char*>(buf_out[1].pvBuffer) + buf_out[1].cbBuffer);
|
|
m_packet_res_inner = false;
|
|
return EapPeerMethodResponseActionSend;
|
|
} else
|
|
throw sec_runtime_error(status, __FUNCTION__ " Schannel error.");
|
|
} else
|
|
throw sec_runtime_error(status, __FUNCTION__ " Unexpected Schannel result.");
|
|
}
|
|
|
|
case phase_finished: {
|
|
m_packet_res.clear();
|
|
m_sc_queue.insert(m_sc_queue.end(), reinterpret_cast<const unsigned char*>(pReceivedPacket), reinterpret_cast<const unsigned char*>(pReceivedPacket) + dwReceivedPacketSize);
|
|
|
|
if (!(m_sc_ctx.m_attrib & ISC_RET_CONFIDENTIALITY))
|
|
throw runtime_error(__FUNCTION__ " Connection is not encrypted.");
|
|
|
|
if (m_sc_queue.empty()) {
|
|
// No extra data for inner authentication.
|
|
return method_tunnel::process_request_packet(NULL, 0);
|
|
} else {
|
|
// Authenticator sent data for inner authentication. Unencrypt it.
|
|
|
|
// Decrypt the message.
|
|
SecBuffer buf[] = {
|
|
{ (unsigned long)m_sc_queue.size(), SECBUFFER_DATA, m_sc_queue.data() },
|
|
{ 0, SECBUFFER_EMPTY, NULL },
|
|
{ 0, SECBUFFER_EMPTY, NULL },
|
|
{ 0, SECBUFFER_EMPTY, NULL },
|
|
};
|
|
SecBufferDesc buf_desc = { SECBUFFER_VERSION, _countof(buf), buf };
|
|
SECURITY_STATUS status = DecryptMessage(m_sc_ctx, &buf_desc, 0, NULL);
|
|
if (status == SEC_E_OK) {
|
|
// Process data (only the first SECBUFFER_DATA found).
|
|
EapPeerMethodResponseAction action = EapPeerMethodResponseActionDiscard;
|
|
for (size_t i = 0; i < _countof(buf); i++)
|
|
if (buf[i].BufferType == SECBUFFER_DATA) {
|
|
action = method_tunnel::process_request_packet(buf[i].pvBuffer, buf[i].cbBuffer);
|
|
break;
|
|
}
|
|
|
|
// Queue remaining data for the next time.
|
|
m_sc_queue.clear();
|
|
for (size_t i = 0; i < _countof(buf); i++)
|
|
if (buf[i].BufferType == SECBUFFER_EXTRA)
|
|
m_sc_queue.insert(m_sc_queue.end(), reinterpret_cast<const unsigned char*>(buf[i].pvBuffer), reinterpret_cast<const unsigned char*>(buf[i].pvBuffer) + buf[i].cbBuffer);
|
|
|
|
return action;
|
|
} else if (FAILED(status))
|
|
throw sec_runtime_error(status, __FUNCTION__ " Schannel error.");
|
|
else
|
|
throw sec_runtime_error(status, __FUNCTION__ " Unexpected Schannel result.");
|
|
}
|
|
}
|
|
|
|
default:
|
|
throw invalid_argument(string_printf(__FUNCTION__ " Unknown phase (phase %u).", m_phase).c_str());
|
|
}
|
|
}
|
|
|
|
|
|
void eap::method_ttls::get_response_packet(
|
|
_Out_ sanitizing_blob &packet,
|
|
_In_opt_ DWORD size_max)
|
|
{
|
|
if (m_packet_res_inner) {
|
|
// Get maximum allowable packet size.
|
|
SecPkgContext_StreamSizes sizes;
|
|
SECURITY_STATUS status = QueryContextAttributes(m_sc_ctx, SECPKG_ATTR_STREAM_SIZES, &sizes);
|
|
if (FAILED(status))
|
|
throw sec_runtime_error(status, __FUNCTION__ " Error getting Schannel required encryption sizes.");
|
|
if (m_packet_res.size() + sizes.cbHeader + sizes.cbTrailer > size_max)
|
|
throw invalid_argument(string_printf(__FUNCTION__ " This method does not support packet fragmentation, but the data size is too big to fit in one packet (packet: %u, maximum: %u).", m_packet_res.size(), size_max).c_str());
|
|
sizes.cbMaximumMessage = std::min<unsigned long>(sizes.cbMaximumMessage, size_max - (unsigned long)(m_packet_res.size() + sizes.cbHeader + sizes.cbTrailer));
|
|
|
|
// Get inner response packet.
|
|
packet.reserve(sizes.cbHeader + sizes.cbMaximumMessage + sizes.cbTrailer);
|
|
method_tunnel::get_response_packet(packet, sizes.cbMaximumMessage);
|
|
if (!packet.empty()) {
|
|
DWORD size_data = (DWORD)packet.size();
|
|
|
|
// Insert and append space for header and trailer.
|
|
packet.insert(packet.begin(), sizes.cbHeader , 0);
|
|
packet.insert(packet.end (), sizes.cbTrailer, 0);
|
|
|
|
// Encrypt the message.
|
|
unsigned char *ptr_data = packet.data();
|
|
SecBuffer buf[] = {
|
|
{ sizes.cbHeader, SECBUFFER_STREAM_HEADER , ptr_data },
|
|
{ size_data, SECBUFFER_DATA , ptr_data += sizes.cbHeader },
|
|
{ sizes.cbTrailer, SECBUFFER_STREAM_TRAILER, ptr_data += size_data },
|
|
{ 0, SECBUFFER_EMPTY , NULL },
|
|
};
|
|
SecBufferDesc buf_desc = { SECBUFFER_VERSION, _countof(buf), buf };
|
|
status = EncryptMessage(m_sc_ctx, 0, &buf_desc, 0);
|
|
if (FAILED(status))
|
|
throw sec_runtime_error(status, __FUNCTION__ " Error encrypting message.");
|
|
|
|
m_packet_res.insert(m_packet_res.end(), reinterpret_cast<const unsigned char*>(buf[0].pvBuffer), reinterpret_cast<const unsigned char*>(buf[0].pvBuffer) + buf[0].cbBuffer + buf[1].cbBuffer + buf[2].cbBuffer);
|
|
}
|
|
} else if (m_packet_res.size() > size_max)
|
|
throw invalid_argument(string_printf(__FUNCTION__ " This method does not support packet fragmentation, but the data size is too big to fit in one packet (packet: %u, maximum: %u).", m_packet_res.size(), size_max).c_str());
|
|
|
|
packet.assign(m_packet_res.begin(), m_packet_res.end());
|
|
}
|
|
|
|
|
|
void eap::method_ttls::get_result(
|
|
_In_ EapPeerMethodResultReason reason,
|
|
_Inout_ EapPeerMethodResult *pResult)
|
|
{
|
|
assert(pResult);
|
|
|
|
// Get inner result.
|
|
method_tunnel::get_result(reason, pResult);
|
|
|
|
if (reason == EapPeerMethodResultSuccess) {
|
|
eap_attr a;
|
|
|
|
// Prepare EAP result attributes.
|
|
if (pResult->pAttribArray) {
|
|
m_eap_attr.reserve(pResult->pAttribArray->dwNumberOfAttributes + 3);
|
|
m_eap_attr.clear();
|
|
// Copy all EAP attributes from inner method up to blank terminator. Exclude any MPPE-Recv-Key or MPPE-Send-Key if found.
|
|
for (auto attr = pResult->pAttribArray->pAttribs, attr_end = pResult->pAttribArray->pAttribs + pResult->pAttribArray->dwNumberOfAttributes; attr != attr_end && attr->eaType; ++attr) {
|
|
if (attr->eaType != eatVendorSpecific || attr->dwLength < 5 || ntohl(*reinterpret_cast<const unsigned int*>(attr->pValue)) != 311 || attr->pValue[4] != 16 && attr->pValue[4] != 17)
|
|
m_eap_attr.push_back(*attr);
|
|
}
|
|
} else {
|
|
m_eap_attr.reserve(3);
|
|
m_eap_attr.clear();
|
|
}
|
|
|
|
// Derive MSK keys.
|
|
static const DWORD s_key_id = 0x01; // EAP-TTLSv0 Keying Material
|
|
static const SecPkgContext_EapPrfInfo s_prf_info = { 0, sizeof(s_key_id), (PBYTE)&s_key_id };
|
|
SECURITY_STATUS status = SetContextAttributes(m_sc_ctx, SECPKG_ATTR_EAP_PRF_INFO, (void*)&s_prf_info, sizeof(s_prf_info));
|
|
if (FAILED(status))
|
|
throw sec_runtime_error(status, __FUNCTION__ " Error setting TTLS PRF in Schannel.");
|
|
|
|
SecPkgContext_EapKeyBlock key_block;
|
|
status = QueryContextAttributes(m_sc_ctx, SECPKG_ATTR_EAP_KEY_BLOCK, &key_block);
|
|
if (FAILED(status))
|
|
throw sec_runtime_error(status, __FUNCTION__ " Error generating MSK in Schannel.");
|
|
const unsigned char *_key_block = key_block.rgbKeys;
|
|
|
|
// MSK: MPPE-Recv-Key
|
|
a.create_ms_mppe_key(16, _key_block, 32);
|
|
m_eap_attr.push_back(std::move(a));
|
|
_key_block += 32;
|
|
|
|
// MSK: MPPE-Send-Key
|
|
a.create_ms_mppe_key(17, _key_block, 32);
|
|
m_eap_attr.push_back(std::move(a));
|
|
_key_block += 32;
|
|
|
|
SecureZeroMemory(&key_block, sizeof(key_block));
|
|
|
|
// Append blank EAP attribute.
|
|
m_eap_attr.push_back(eap_attr::blank);
|
|
|
|
m_eap_attr_desc.dwNumberOfAttributes = (DWORD)m_eap_attr.size();
|
|
m_eap_attr_desc.pAttribs = m_eap_attr.data();
|
|
pResult->pAttribArray = &m_eap_attr_desc;
|
|
|
|
m_cfg.m_last_status = config_method::status_success;
|
|
}
|
|
|
|
// Always ask EAP host to save the connection data. And it will save it *only* when we report "success".
|
|
// Don't worry. EapHost is well aware of failed authentication condition.
|
|
pResult->fSaveConnectionData = TRUE;
|
|
pResult->fIsSuccess = TRUE;
|
|
|
|
//if (m_phase == phase_finished) {
|
|
// // Get inner method result.
|
|
// EapPeerMethodResult result = {};
|
|
// m_inner->get_result(reason, &result);
|
|
// if (result.fSaveConnectionData)
|
|
// pResult->fSaveConnectionData = TRUE;
|
|
|
|
// if (m_inner->m_cfg.m_last_status != config_method::status_success) {
|
|
// // Inner method admitted problems, so autentication must have proceeded to inner authentication already.
|
|
// // Therefore, outer authentication must have been OK.
|
|
// m_cfg.m_last_status = config_method::status_success;
|
|
// }
|
|
//}
|
|
}
|
|
|
|
|
|
#if EAP_TLS < EAP_TLS_SCHANNEL_FULL
|
|
|
|
void eap::method_ttls::verify_server_trust() const
|
|
{
|
|
cert_context cert;
|
|
SECURITY_STATUS status = QueryContextAttributes(m_sc_ctx, SECPKG_ATTR_REMOTE_CERT_CONTEXT, (PVOID)&cert);
|
|
if (FAILED(status))
|
|
throw sec_runtime_error(status, __FUNCTION__ " Error retrieving server certificate from Schannel.");
|
|
|
|
for (auto c = m_cfg.m_trusted_root_ca.cbegin(), c_end = m_cfg.m_trusted_root_ca.cend(); c != c_end; ++c) {
|
|
if (cert->cbCertEncoded == (*c)->cbCertEncoded &&
|
|
memcmp(cert->pbCertEncoded, (*c)->pbCertEncoded, cert->cbCertEncoded) == 0)
|
|
{
|
|
// Server certificate found directly on the trusted root CA list.
|
|
m_module.log_event(&EAPMETHOD_TLS_SERVER_CERT_TRUSTED_EX, event_data::blank);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check server name.
|
|
if (!m_cfg.m_server_names.empty()) {
|
|
bool
|
|
has_san = false,
|
|
found = false;
|
|
|
|
// Search subjectAltName2 and subjectAltName.
|
|
for (DWORD idx_ext = 0; !found && idx_ext < cert->pCertInfo->cExtension; idx_ext++) {
|
|
unique_ptr<CERT_ALT_NAME_INFO, LocalFree_delete<CERT_ALT_NAME_INFO> > san_info;
|
|
if (strcmp(cert->pCertInfo->rgExtension[idx_ext].pszObjId, szOID_SUBJECT_ALT_NAME2) == 0) {
|
|
unsigned char *output = NULL;
|
|
DWORD size_output;
|
|
if (!CryptDecodeObjectEx(
|
|
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
|
|
szOID_SUBJECT_ALT_NAME2,
|
|
cert->pCertInfo->rgExtension[idx_ext].Value.pbData, cert->pCertInfo->rgExtension[idx_ext].Value.cbData,
|
|
CRYPT_DECODE_ALLOC_FLAG | CRYPT_DECODE_ENABLE_PUNYCODE_FLAG,
|
|
NULL,
|
|
&output, &size_output))
|
|
throw win_runtime_error(__FUNCTION__ " Error decoding subjectAltName2 certificate extension.");
|
|
san_info.reset((CERT_ALT_NAME_INFO*)output);
|
|
} else if (strcmp(cert->pCertInfo->rgExtension[idx_ext].pszObjId, szOID_SUBJECT_ALT_NAME) == 0) {
|
|
unsigned char *output = NULL;
|
|
DWORD size_output;
|
|
if (!CryptDecodeObjectEx(
|
|
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
|
|
szOID_SUBJECT_ALT_NAME,
|
|
cert->pCertInfo->rgExtension[idx_ext].Value.pbData, cert->pCertInfo->rgExtension[idx_ext].Value.cbData,
|
|
CRYPT_DECODE_ALLOC_FLAG | CRYPT_DECODE_ENABLE_PUNYCODE_FLAG,
|
|
NULL,
|
|
&output, &size_output))
|
|
throw win_runtime_error(__FUNCTION__ " Error decoding subjectAltName certificate extension.");
|
|
san_info.reset((CERT_ALT_NAME_INFO*)output);
|
|
} else {
|
|
// Skip this extension.
|
|
continue;
|
|
}
|
|
has_san = true;
|
|
|
|
for (auto s = m_cfg.m_server_names.cbegin(), s_end = m_cfg.m_server_names.cend(); !found && s != s_end; ++s) {
|
|
for (DWORD idx_entry = 0; !found && idx_entry < san_info->cAltEntry; idx_entry++) {
|
|
if (san_info->rgAltEntry[idx_entry].dwAltNameChoice == CERT_ALT_NAME_DNS_NAME &&
|
|
_wcsicmp(s->c_str(), san_info->rgAltEntry[idx_entry].pwszDNSName) == 0)
|
|
{
|
|
m_module.log_event(&EAPMETHOD_TLS_SERVER_NAME_TRUSTED1, event_data(san_info->rgAltEntry[idx_entry].pwszDNSName), event_data::blank);
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!has_san) {
|
|
// Certificate has no subjectAltName. Compare against Common Name.
|
|
wstring subj;
|
|
if (!CertGetNameStringW(cert, CERT_NAME_DNS_TYPE, CERT_NAME_STR_ENABLE_PUNYCODE_FLAG, NULL, subj))
|
|
throw win_runtime_error(__FUNCTION__ " Error retrieving server's certificate subject name.");
|
|
|
|
for (auto s = m_cfg.m_server_names.cbegin(), s_end = m_cfg.m_server_names.cend(); !found && s != s_end; ++s) {
|
|
if (_wcsicmp(s->c_str(), subj.c_str()) == 0) {
|
|
m_module.log_event(&EAPMETHOD_TLS_SERVER_NAME_TRUSTED1, event_data(subj), event_data::blank);
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
throw sec_runtime_error(SEC_E_WRONG_PRINCIPAL, __FUNCTION__ " Name provided in server certificate is not on the list of trusted server names.");
|
|
}
|
|
|
|
if (cert->pCertInfo->Issuer.cbData == cert->pCertInfo->Subject.cbData &&
|
|
memcmp(cert->pCertInfo->Issuer.pbData, cert->pCertInfo->Subject.pbData, cert->pCertInfo->Issuer.cbData) == 0)
|
|
throw sec_runtime_error(SEC_E_CERT_UNKNOWN, __FUNCTION__ " Server is using a self-signed certificate. Cannot trust it.");
|
|
|
|
// Create temporary certificate store of our trusted root CAs.
|
|
cert_store store;
|
|
if (!store.create(CERT_STORE_PROV_MEMORY, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, NULL, 0, NULL))
|
|
throw win_runtime_error(__FUNCTION__ " Error creating temporary certificate store.");
|
|
for (auto c = m_cfg.m_trusted_root_ca.cbegin(), c_end = m_cfg.m_trusted_root_ca.cend(); c != c_end; ++c)
|
|
CertAddCertificateContextToStore(store, *c, CERT_STORE_ADD_REPLACE_EXISTING, NULL);
|
|
|
|
// Add all intermediate certificates from the server's certificate chain.
|
|
for (cert_context c(cert); c;) {
|
|
DWORD flags = 0;
|
|
c.attach(CertGetIssuerCertificateFromStore(cert->hCertStore, c, NULL, &flags));
|
|
if (!c) break;
|
|
|
|
if (c->pCertInfo->Issuer.cbData == c->pCertInfo->Subject.cbData &&
|
|
memcmp(c->pCertInfo->Issuer.pbData, c->pCertInfo->Subject.pbData, c->pCertInfo->Issuer.cbData) == 0)
|
|
{
|
|
// Skip the root CA certificates (self-signed). We define in whom we trust!
|
|
continue;
|
|
}
|
|
|
|
CertAddCertificateContextToStore(store, c, CERT_STORE_ADD_REPLACE_EXISTING, NULL);
|
|
}
|
|
|
|
// Prepare the certificate chain validation, and check.
|
|
CERT_CHAIN_PARA chain_params = {
|
|
sizeof(chain_params), // cbSize
|
|
{
|
|
USAGE_MATCH_TYPE_AND, // RequestedUsage.dwType
|
|
{}, // RequestedUsage.Usage
|
|
},
|
|
#ifdef CERT_CHAIN_PARA_HAS_EXTRA_FIELDS
|
|
{}, // RequestedIssuancePolicy
|
|
1, // dwUrlRetrievalTimeout (1ms to speed up checking)
|
|
#else
|
|
#define _S2(x) #x
|
|
#define _S(x) _S2(x)
|
|
#pragma message(__FILE__ "(" _S(__LINE__) "): warning X0000: Please define CERT_CHAIN_PARA_HAS_EXTRA_FIELDS constant when compiling this project.")
|
|
#endif
|
|
};
|
|
cert_chain_context context;
|
|
if (!context.create(NULL, cert, NULL, store, &chain_params, 0))
|
|
throw win_runtime_error(__FUNCTION__ " Error creating certificate chain context.");
|
|
|
|
// Check chain validation error flags. Ignore CERT_TRUST_IS_UNTRUSTED_ROOT flag since we check root CA explicitly.
|
|
if (context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR &&
|
|
(context->TrustStatus.dwErrorStatus & ~CERT_TRUST_IS_UNTRUSTED_ROOT) != CERT_TRUST_NO_ERROR)
|
|
{
|
|
if (context->TrustStatus.dwErrorStatus & (CERT_TRUST_IS_NOT_TIME_VALID | CERT_TRUST_IS_NOT_TIME_NESTED))
|
|
throw sec_runtime_error(SEC_E_CERT_EXPIRED, __FUNCTION__ " Server certificate has expired (or is not valid yet).");
|
|
else if (context->TrustStatus.dwErrorStatus & (CERT_TRUST_IS_UNTRUSTED_ROOT | CERT_TRUST_IS_PARTIAL_CHAIN))
|
|
throw sec_runtime_error(SEC_E_UNTRUSTED_ROOT, __FUNCTION__ " Server's certificate not issued by one of configured trusted root CAs.");
|
|
else
|
|
throw sec_runtime_error(SEC_E_CERT_UNKNOWN, __FUNCTION__ " Error validating server certificate.");
|
|
}
|
|
|
|
// Verify Root CA against our trusted root CA list
|
|
if (context->cChain != 1)
|
|
throw sec_runtime_error(SEC_E_CERT_UNKNOWN, __FUNCTION__ " Multiple chain verification not supported.");
|
|
if (context->rgpChain[0]->cElement == 0)
|
|
throw sec_runtime_error(SEC_E_CERT_UNKNOWN, __FUNCTION__ " Can not verify empty certificate chain.");
|
|
|
|
PCCERT_CONTEXT cert_root = context->rgpChain[0]->rgpElement[context->rgpChain[0]->cElement-1]->pCertContext;
|
|
for (auto c = m_cfg.m_trusted_root_ca.cbegin(), c_end = m_cfg.m_trusted_root_ca.cend();; ++c) {
|
|
if (c != c_end) {
|
|
if (cert_root->cbCertEncoded == (*c)->cbCertEncoded &&
|
|
memcmp(cert_root->pbCertEncoded, (*c)->pbCertEncoded, cert_root->cbCertEncoded) == 0)
|
|
{
|
|
// Trusted root CA found.
|
|
break;
|
|
}
|
|
} else {
|
|
// Not found.
|
|
throw sec_runtime_error(SEC_E_UNTRUSTED_ROOT, __FUNCTION__ " Server's certificate not issued by one of configured trusted root CAs.");
|
|
}
|
|
}
|
|
|
|
m_module.log_event(&EAPMETHOD_TLS_SERVER_CERT_TRUSTED, event_data::blank);
|
|
}
|
|
|
|
#endif
|