mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-11-03 21:46:32 +00:00
A credential made from the older SCHANNEL_CRED structure cannot reach TLS 1.3 on Windows, which offers it only through SCH_CREDENTIALS, and its grbitEnabledProtocols bound the versions it uses. Raven negotiated TLS 1.3 regardless; aria2, which knows TLS 1.1 and 1.2 only, aborted after the handshake. The enabled protocols now set the lowest and highest version, and SCHANNEL_CRED stops at TLS 1.2. Assisted-by: Claude Code:claude-opus-5-5 AI-Scope: the version limits (cherry picked from commit b14156cf5618db6dd4a93db39f6e007d412bd41a)
664 lines
26 KiB
C++
664 lines
26 KiB
C++
#include "kernel_context.h"
|
|
#include "text.h"
|
|
|
|
#include <openssl/err.h>
|
|
#include <openssl/ssl.h>
|
|
#include <openssl/x509v3.h>
|
|
|
|
#include <arpa/inet.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
|
|
namespace raven {
|
|
|
|
namespace {
|
|
|
|
// SECURITY_STATUS values of the SSPI documentation.
|
|
constexpr uint32_t sec_ok = 0, sec_continue_needed = 0x00090312, sec_context_expired = 0x00090317, sec_invalid_handle = 0x80090301,
|
|
sec_unsupported_function = 0x80090302, sec_internal_error = 0x80090304, sec_invalid_token = 0x80090308,
|
|
sec_secpkg_not_found = 0x80090305, sec_buffer_too_small = 0x80090321, sec_incomplete_message = 0x80090318,
|
|
sec_wrong_principal = 0x80090322, sec_untrusted_root = 0x80090325, sec_illegal_message = 0x80090326,
|
|
sec_insufficient_memory = 0x80090300, sec_invalid_parameter = 0x8009035d;
|
|
constexpr uint32_t buffer_empty = 0, buffer_data = 1, buffer_token = 2, buffer_missing = 4, buffer_extra = 5, buffer_trailer = 6,
|
|
buffer_header = 7, buffer_application_protocols = 18;
|
|
constexpr uint32_t request_allocate_memory = 0x100, request_manual_validation = 0x80000;
|
|
constexpr uint32_t credentials_no_name_check = 0x4, credentials_manual_validation = 0x8;
|
|
constexpr uint32_t attribute_stream_sizes = 4, attribute_application_protocol = 35, attribute_connection_info = 0x5a;
|
|
// The record header is the whole fixed part a sender has to leave room for; the trailer covers the largest padding and tag a TLS 1.2
|
|
// block cipher adds.
|
|
constexpr uint32_t record_header = 5, record_trailer = 1024, record_payload = 16384;
|
|
constexpr uint64_t handle_tag = 0x52564e53;
|
|
|
|
// A credential of the Windows logon packages holds no TLS context: local remote procedure calls only acquire one, and never build a
|
|
// security context on it.
|
|
struct Credential {
|
|
std::unique_ptr<SSL_CTX, decltype(&SSL_CTX_free)> tls{nullptr, SSL_CTX_free};
|
|
bool verify = true, check_name = true;
|
|
};
|
|
|
|
struct Context {
|
|
std::unique_ptr<SSL, decltype(&SSL_free)> tls{nullptr, SSL_free};
|
|
BIO *input = nullptr, *output = nullptr;
|
|
bool established = false, closing = false;
|
|
};
|
|
|
|
struct Provider {
|
|
std::mutex lock;
|
|
uint64_t table = 0, next = 1;
|
|
std::map<uint64_t, std::shared_ptr<Credential>> credentials;
|
|
std::map<uint64_t, std::shared_ptr<Context>> contexts;
|
|
};
|
|
|
|
bool tracing()
|
|
{
|
|
static const bool value = std::getenv("RAVEN_TRACE_SCHANNEL");
|
|
return value;
|
|
}
|
|
|
|
// One SecBuffer of a SecBufferDesc: the size and type words followed by the pointer, eight bytes in.
|
|
struct Buffer {
|
|
uint64_t at;
|
|
uint32_t size, type;
|
|
uint64_t data;
|
|
};
|
|
|
|
std::vector<Buffer> buffers(ApiCall &call, uint64_t description)
|
|
{
|
|
std::vector<Buffer> result;
|
|
if (!description)
|
|
return result;
|
|
const uint32_t count = call.load<uint32_t>(description + 4);
|
|
const uint64_t first = call.load_pointer(description + 8);
|
|
const size_t stride = 8 + call.pointer_width();
|
|
for (uint32_t index = 0; index < count && index < 16; ++index) {
|
|
const uint64_t at = first + index * stride;
|
|
result.push_back({at, call.load<uint32_t>(at), call.load<uint32_t>(at + 4), call.load_pointer(at + 8)});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void set_buffer(ApiCall &call, const Buffer &buffer, uint32_t type, uint32_t size, uint64_t data)
|
|
{
|
|
call.store<uint32_t>(buffer.at, size);
|
|
call.store<uint32_t>(buffer.at + 4, type);
|
|
call.store_pointer(buffer.at + 8, data);
|
|
}
|
|
|
|
uint64_t handle_of(ApiCall &call, uint64_t address)
|
|
{
|
|
return address && call.load_pointer(address + call.pointer_width()) == handle_tag ? call.load_pointer(address) : 0;
|
|
}
|
|
|
|
// The length of the records complete at the start of the bytes, every one a five-byte header and the length it announces.
|
|
size_t complete_records(const std::vector<uint8_t> &bytes, size_t limit = SIZE_MAX)
|
|
{
|
|
size_t at = 0;
|
|
for (size_t count = 0; count < limit && at + record_header <= bytes.size(); ++count) {
|
|
const size_t length = record_header + (size_t(bytes[at + 3]) << 8 | bytes[at + 4]);
|
|
if (at + length > bytes.size())
|
|
break;
|
|
at += length;
|
|
}
|
|
return at;
|
|
}
|
|
|
|
std::vector<uint8_t> pending_output(Context &context)
|
|
{
|
|
std::vector<uint8_t> bytes(BIO_ctrl_pending(context.output));
|
|
if (!bytes.empty())
|
|
BIO_read(context.output, bytes.data(), int(bytes.size()));
|
|
return bytes;
|
|
}
|
|
|
|
std::shared_ptr<Credential> make_credential(uint32_t flags, bool manual)
|
|
{
|
|
auto credential = std::make_shared<Credential>();
|
|
credential->tls.reset(SSL_CTX_new(TLS_client_method()));
|
|
if (!credential->tls)
|
|
return nullptr;
|
|
SSL_CTX_set_default_verify_paths(credential->tls.get());
|
|
credential->verify = !manual && !(flags & credentials_manual_validation);
|
|
credential->check_name = !(flags & credentials_no_name_check);
|
|
return credential;
|
|
}
|
|
|
|
uint32_t failure_status(Context &context)
|
|
{
|
|
const long verified = SSL_get_verify_result(context.tls.get());
|
|
if (verified == X509_V_ERR_HOSTNAME_MISMATCH)
|
|
return sec_wrong_principal;
|
|
if (verified != X509_V_OK)
|
|
return sec_untrusted_root;
|
|
return sec_illegal_message;
|
|
}
|
|
|
|
}
|
|
|
|
// The Schannel package of SSPI as a TLS client over the host's OpenSSL: the handshake, record protection and close are the
|
|
// buffer exchanges of the SSPI documentation, with the records framed by OpenSSL through memory BIOs.
|
|
void register_schannel_api(ApiRegistry ®istry, KernelContext &context)
|
|
{
|
|
auto provider = std::make_shared<Provider>();
|
|
registry.add("InitSecurityInterfaceA", 0, [](ApiCall &call) { call.fail(50); });
|
|
// The table is built outside the lock: the guest allocator can give the processor to another guest thread, which may ask for it too on
|
|
// the same host thread. The first table published is the one every caller gets.
|
|
registry.add("InitSecurityInterfaceW", 0, [provider, &context](ApiCall &call) {
|
|
{
|
|
std::lock_guard guard(provider->lock);
|
|
if (provider->table) {
|
|
call.ret(provider->table);
|
|
return;
|
|
}
|
|
}
|
|
{
|
|
// SecurityFunctionTableW: the version, then the entry points in the order of the SSPI header, reserved slots left null.
|
|
static constexpr const char *entries[] = {"EnumerateSecurityPackagesW", "QueryCredentialsAttributesW", "AcquireCredentialsHandleW",
|
|
"FreeCredentialsHandle", nullptr, "InitializeSecurityContextW", "AcceptSecurityContext", "CompleteAuthToken",
|
|
"DeleteSecurityContext", "ApplyControlToken", "QueryContextAttributesW", "ImpersonateSecurityContext",
|
|
"RevertSecurityContext", "MakeSignature", "VerifySignature", "FreeContextBuffer", "QuerySecurityPackageInfoW", nullptr,
|
|
nullptr, "ExportSecurityContext", "ImportSecurityContextW", "AddCredentialsW", nullptr, "QuerySecurityContextToken",
|
|
"EncryptMessage", "DecryptMessage", "SetContextAttributesW", "SetCredentialsAttributesW", "ChangeAccountPasswordW", nullptr,
|
|
nullptr};
|
|
const size_t width = call.pointer_width(), count = std::size(entries);
|
|
const uint64_t table = context.allocator.allocate(width * (count + 1));
|
|
if (!table) {
|
|
call.fail(8);
|
|
return;
|
|
}
|
|
call.store<uint32_t>(table, 4);
|
|
for (size_t index = 0; index < count; ++index)
|
|
if (entries[index])
|
|
call.store_pointer(table + width * (index + 1), context.modules.resolve({"secur32.dll", entries[index], 0, false, 0}));
|
|
std::unique_lock guard(provider->lock);
|
|
const bool first = !provider->table;
|
|
if (first)
|
|
provider->table = table;
|
|
const uint64_t published = provider->table;
|
|
guard.unlock();
|
|
if (!first)
|
|
context.allocator.release(table);
|
|
call.ret(published);
|
|
}
|
|
});
|
|
registry.add("AcquireCredentialsHandleW", 9, [provider](ApiCall &call) {
|
|
const auto package = call[1] ? call.wide(call[1]) : std::u16string();
|
|
const bool logon = equal_fold(package, u"Negotiate") || equal_fold(package, u"NTLM") || equal_fold(package, u"Kerberos");
|
|
if (!logon && !equal_fold(package, u"Microsoft Unified Security Protocol Provider") && !equal_fold(package, u"Schannel")
|
|
&& !equal_fold(package, u"Default TLS SSP")) {
|
|
call.ret(sec_secpkg_not_found);
|
|
return;
|
|
}
|
|
if (logon) {
|
|
std::lock_guard guard(provider->lock);
|
|
const uint64_t id = provider->next++;
|
|
provider->credentials[id] = std::make_shared<Credential>();
|
|
call.store_pointer(call[7], id);
|
|
call.store_pointer(call[7] + call.pointer_width(), handle_tag);
|
|
if (call[8])
|
|
call.store<int64_t>(call[8], INT64_MAX);
|
|
call.ret(sec_ok);
|
|
return;
|
|
}
|
|
// SCHANNEL_CRED (version 4) and SCH_CREDENTIALS (version 5) keep their flags at different places. The older structure offers
|
|
// no TLS 1.3, which only SCH_CREDENTIALS reaches, and its grbitEnabledProtocols, when it names any, bound the versions.
|
|
uint32_t flags = 0, enabled = 0;
|
|
int highest = TLS1_3_VERSION;
|
|
if (const uint64_t data = call[4]) {
|
|
const uint32_t version = call.load<uint32_t>(data);
|
|
const bool wide = call.pointer_width() == 8;
|
|
if (version == 4) {
|
|
flags = call.load<uint32_t>(data + (wide ? 72 : 48));
|
|
enabled = call.load<uint32_t>(data + (wide ? 56 : 32));
|
|
highest = TLS1_2_VERSION;
|
|
} else if (version == 5) {
|
|
flags = call.load<uint32_t>(data + (wide ? 52 : 32));
|
|
}
|
|
}
|
|
auto credential = make_credential(flags, false);
|
|
if (!credential) {
|
|
call.ret(sec_internal_error);
|
|
return;
|
|
}
|
|
// SP_PROT_TLS1_0, 1_1, 1_2 and 1_3, the client and server bits of each.
|
|
static constexpr std::pair<uint32_t, int> versions[] = {{0xc0, TLS1_VERSION}, {0x300, TLS1_1_VERSION}, {0xc00, TLS1_2_VERSION},
|
|
{0x3000, TLS1_3_VERSION}};
|
|
int lowest = 0;
|
|
if (enabled) {
|
|
int top = 0;
|
|
for (const auto &[bits, protocol] : versions)
|
|
if (enabled & bits) {
|
|
if (!lowest)
|
|
lowest = protocol;
|
|
top = protocol;
|
|
}
|
|
if (top)
|
|
highest = std::min(highest, top);
|
|
}
|
|
if (lowest)
|
|
SSL_CTX_set_min_proto_version(credential->tls.get(), lowest);
|
|
SSL_CTX_set_max_proto_version(credential->tls.get(), highest);
|
|
std::lock_guard guard(provider->lock);
|
|
const uint64_t id = provider->next++;
|
|
provider->credentials[id] = credential;
|
|
call.store_pointer(call[7], id);
|
|
call.store_pointer(call[7] + call.pointer_width(), handle_tag);
|
|
if (call[8])
|
|
call.store<int64_t>(call[8], INT64_MAX);
|
|
call.ret(sec_ok);
|
|
});
|
|
registry.add("FreeCredentialsHandle", 1, [provider](ApiCall &call) {
|
|
std::lock_guard guard(provider->lock);
|
|
call.ret(provider->credentials.erase(handle_of(call, call[0])) ? sec_ok : sec_invalid_handle);
|
|
});
|
|
registry.add("DeleteSecurityContext", 1, [provider](ApiCall &call) {
|
|
std::lock_guard guard(provider->lock);
|
|
call.ret(provider->contexts.erase(handle_of(call, call[0])) ? sec_ok : sec_invalid_handle);
|
|
});
|
|
// The packages: the TLS provider under its two names, and the Windows logon packages, whose RPC identifiers let local remote procedure
|
|
// calls register and bind with them. Each SecPkgInfo (capabilities, version, RPC identifier, largest token, name, comment) sits in one
|
|
// block with its strings, freed by FreeContextBuffer.
|
|
const auto package_list = [&context](ApiCall &call, bool narrow, std::optional<std::u16string> only) -> uint32_t {
|
|
struct Package {
|
|
std::u16string_view name, comment;
|
|
uint32_t capabilities;
|
|
uint16_t rpc;
|
|
uint32_t largest_token;
|
|
};
|
|
static constexpr Package packages[] = {
|
|
{u"Microsoft Unified Security Protocol Provider", u"Schannel Security Package", 0x107b3, 14, 0x6000},
|
|
{u"Schannel", u"Schannel Security Package", 0x107b3, 14, 0x6000},
|
|
{u"Negotiate", u"Microsoft Package Negotiator", 0x83bb3, 9, 48256},
|
|
{u"NTLM", u"NTLM Security Package", 0x82b37, 10, 2888},
|
|
{u"Kerberos", u"Microsoft Kerberos V1.0", 0x20bbbf, 16, 48000}};
|
|
std::vector<const Package *> chosen;
|
|
for (const auto &package : packages)
|
|
if (!only || equal_fold(*only, package.name))
|
|
chosen.push_back(&package);
|
|
if (chosen.empty())
|
|
return sec_secpkg_not_found;
|
|
const size_t width = call.pointer_width(), entry = width == 8 ? 32 : 20, unit = narrow ? 1 : 2;
|
|
size_t size = entry * chosen.size();
|
|
for (const auto package : chosen)
|
|
size += (package->name.size() + 1 + package->comment.size() + 1) * unit;
|
|
const uint64_t block = context.allocator.allocate(size, true);
|
|
if (!block)
|
|
return sec_insufficient_memory;
|
|
uint64_t text = block + entry * chosen.size();
|
|
const auto put = [&](std::u16string_view value) {
|
|
const uint64_t at = text;
|
|
if (narrow)
|
|
call.put_narrow(at, to_utf8(value), value.size() + 1);
|
|
else
|
|
call.put_wide(at, value, value.size() + 1);
|
|
text += (value.size() + 1) * unit;
|
|
return at;
|
|
};
|
|
for (size_t index = 0; index < chosen.size(); ++index) {
|
|
const uint64_t at = block + entry * index;
|
|
call.store<uint32_t>(at, chosen[index]->capabilities);
|
|
call.store<uint16_t>(at + 4, 1);
|
|
call.store<uint16_t>(at + 6, chosen[index]->rpc);
|
|
call.store<uint32_t>(at + 8, chosen[index]->largest_token);
|
|
call.store_pointer(at + (width == 8 ? 16 : 12), put(chosen[index]->name));
|
|
call.store_pointer(at + (width == 8 ? 24 : 16), put(chosen[index]->comment));
|
|
}
|
|
if (only) {
|
|
call.store_pointer(call[1], block);
|
|
} else {
|
|
call.store<uint32_t>(call[0], uint32_t(chosen.size()));
|
|
call.store_pointer(call[1], block);
|
|
}
|
|
return sec_ok;
|
|
};
|
|
for (const bool narrow : {false, true}) {
|
|
registry.add(narrow ? "EnumerateSecurityPackagesA" : "EnumerateSecurityPackagesW", 2, [package_list, narrow](ApiCall &call) {
|
|
call.ret(!call[0] || !call[1] ? sec_invalid_parameter : package_list(call, narrow, std::nullopt));
|
|
});
|
|
registry.add(narrow ? "QuerySecurityPackageInfoA" : "QuerySecurityPackageInfoW", 2, [package_list, narrow](ApiCall &call) {
|
|
if (!call[0] || !call[1]) {
|
|
call.ret(sec_invalid_parameter);
|
|
return;
|
|
}
|
|
const auto name = narrow ? widen_ascii(call.narrow(call[0], 256)) : call.wide(call[0], 256);
|
|
call.ret(package_list(call, narrow, name));
|
|
});
|
|
}
|
|
registry.add("FreeContextBuffer", 1, [&context](ApiCall &call) {
|
|
if (call[0])
|
|
context.allocator.release(call[0]);
|
|
call.ret(sec_ok);
|
|
});
|
|
registry.add("ApplyControlToken", 2, [provider](ApiCall &call) {
|
|
std::shared_ptr<Context> session;
|
|
{
|
|
std::lock_guard guard(provider->lock);
|
|
if (const auto found = provider->contexts.find(handle_of(call, call[0])); found != provider->contexts.end())
|
|
session = found->second;
|
|
}
|
|
if (!session) {
|
|
call.ret(sec_invalid_handle);
|
|
return;
|
|
}
|
|
// SCHANNEL_SHUTDOWN asks the next InitializeSecurityContext for the close notification.
|
|
for (const auto &buffer : buffers(call, call[1]))
|
|
if (buffer.type == buffer_token && buffer.size >= 4 && call.load<uint32_t>(buffer.data) == 1)
|
|
session->closing = true;
|
|
call.ret(sec_ok);
|
|
});
|
|
registry.add("InitializeSecurityContextW", 12, [provider, &context](ApiCall &call) {
|
|
const uint32_t request = call.u32(3);
|
|
std::shared_ptr<Context> session;
|
|
{
|
|
std::lock_guard guard(provider->lock);
|
|
if (call[1]) {
|
|
if (const auto found = provider->contexts.find(handle_of(call, call[1])); found != provider->contexts.end())
|
|
session = found->second;
|
|
if (!session) {
|
|
call.ret(sec_invalid_handle);
|
|
return;
|
|
}
|
|
} else {
|
|
const auto found = provider->credentials.find(handle_of(call, call[0]));
|
|
if (found == provider->credentials.end()) {
|
|
call.ret(sec_invalid_handle);
|
|
return;
|
|
}
|
|
const auto &credential = *found->second;
|
|
if (!credential.tls) {
|
|
call.ret(sec_unsupported_function);
|
|
return;
|
|
}
|
|
session = std::make_shared<Context>();
|
|
session->tls.reset(SSL_new(credential.tls.get()));
|
|
session->input = BIO_new(BIO_s_mem());
|
|
session->output = BIO_new(BIO_s_mem());
|
|
SSL_set_bio(session->tls.get(), session->input, session->output);
|
|
SSL_set_connect_state(session->tls.get());
|
|
const auto target = call[2] ? to_utf8(call.wide(call[2])) : std::string();
|
|
in6_addr address;
|
|
const bool numeric = inet_pton(AF_INET, target.c_str(), &address) == 1 || inet_pton(AF_INET6, target.c_str(), &address) == 1;
|
|
if (!target.empty() && !numeric)
|
|
SSL_set_tlsext_host_name(session->tls.get(), target.c_str());
|
|
const bool verify = credential.verify && !(request & request_manual_validation);
|
|
SSL_set_verify(session->tls.get(), verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, nullptr);
|
|
if (verify && credential.check_name && !target.empty()) {
|
|
if (numeric)
|
|
X509_VERIFY_PARAM_set1_ip_asc(SSL_get0_param(session->tls.get()), target.c_str());
|
|
else
|
|
SSL_set1_host(session->tls.get(), target.c_str());
|
|
}
|
|
// SEC_APPLICATION_PROTOCOLS: the size of the lists, then lists of an extension kind, a length and the protocols in
|
|
// the wire form of ALPN.
|
|
for (const auto &buffer : buffers(call, call[6])) {
|
|
if (buffer.type != buffer_application_protocols || buffer.size < 10)
|
|
continue;
|
|
const auto lists = call.read(buffer.data, buffer.size);
|
|
for (size_t at = 4; at + 6 <= lists.size();) {
|
|
const uint32_t kind = lists[at] | lists[at + 1] << 8 | lists[at + 2] << 16 | uint32_t(lists[at + 3]) << 24;
|
|
const size_t length = lists[at + 4] | lists[at + 5] << 8;
|
|
if (at + 6 + length > lists.size())
|
|
break;
|
|
if (kind == 2)
|
|
SSL_set_alpn_protos(session->tls.get(), lists.data() + at + 6, unsigned(length));
|
|
at += 6 + length;
|
|
}
|
|
}
|
|
const uint64_t id = provider->next++;
|
|
provider->contexts[id] = session;
|
|
call.store_pointer(call[8], id);
|
|
call.store_pointer(call[8] + call.pointer_width(), handle_tag);
|
|
}
|
|
}
|
|
uint32_t status = sec_continue_needed;
|
|
if (session->closing) {
|
|
SSL_shutdown(session->tls.get());
|
|
status = sec_ok;
|
|
} else {
|
|
// Records go in one at a time so whatever follows the end of the handshake stays with the caller as extra data.
|
|
const auto inputs = buffers(call, call[6]);
|
|
const Buffer *token = nullptr, *spare = nullptr;
|
|
for (const auto &buffer : inputs)
|
|
if (buffer.type == buffer_token && !token)
|
|
token = &buffer;
|
|
else if (buffer.type == buffer_empty && token && !spare)
|
|
spare = &buffer;
|
|
const auto bytes = token && token->size ? call.read(token->data, token->size) : std::vector<uint8_t>();
|
|
size_t used = 0;
|
|
bool failed = false;
|
|
if (!bytes.empty() && !complete_records(bytes)) {
|
|
if (spare)
|
|
set_buffer(call, *spare, buffer_missing, uint32_t(record_header + 1), 0);
|
|
call.ret(sec_incomplete_message);
|
|
return;
|
|
}
|
|
for (;;) {
|
|
const int result = SSL_do_handshake(session->tls.get());
|
|
if (result == 1) {
|
|
session->established = true;
|
|
status = sec_ok;
|
|
break;
|
|
}
|
|
const int error = SSL_get_error(session->tls.get(), result);
|
|
if (error != SSL_ERROR_WANT_READ) {
|
|
failed = true;
|
|
break;
|
|
}
|
|
const size_t record = complete_records(std::vector<uint8_t>(bytes.begin() + used, bytes.end()), 1);
|
|
if (!record)
|
|
break;
|
|
BIO_write(session->input, bytes.data() + used, int(record));
|
|
used += record;
|
|
}
|
|
if (failed) {
|
|
status = failure_status(*session);
|
|
if (tracing()) {
|
|
char text[256];
|
|
ERR_error_string_n(ERR_peek_last_error(), text, sizeof(text));
|
|
std::fprintf(stderr, "schannel handshake failed: %s verify %ld\n", text, SSL_get_verify_result(session->tls.get()));
|
|
}
|
|
ERR_clear_error();
|
|
}
|
|
if (spare && used < bytes.size() && !failed)
|
|
set_buffer(call, *spare, buffer_extra, uint32_t(bytes.size() - used), 0);
|
|
}
|
|
const auto produced = pending_output(*session);
|
|
// The token goes to the first token buffer, or the first empty one when the caller marked none.
|
|
const auto outputs = buffers(call, call[9]);
|
|
auto chosen = std::find_if(outputs.begin(), outputs.end(), [](const Buffer &buffer) { return buffer.type == buffer_token; });
|
|
if (chosen == outputs.end())
|
|
chosen = std::find_if(outputs.begin(), outputs.end(), [](const Buffer &buffer) { return buffer.type == buffer_empty; });
|
|
if (chosen != outputs.end()) {
|
|
const Buffer &buffer = *chosen;
|
|
uint64_t data = buffer.data;
|
|
if (request & request_allocate_memory) {
|
|
data = produced.empty() ? 0 : context.allocator.allocate(produced.size(), false);
|
|
if (!produced.empty() && !data) {
|
|
call.ret(sec_insufficient_memory);
|
|
return;
|
|
}
|
|
} else if (produced.size() > buffer.size) {
|
|
call.ret(sec_buffer_too_small);
|
|
return;
|
|
}
|
|
if (!produced.empty())
|
|
call.write(data, produced);
|
|
set_buffer(call, buffer, buffer_token, uint32_t(produced.size()), data);
|
|
}
|
|
if (call[10])
|
|
call.store<uint32_t>(call[10], request);
|
|
if (call[11])
|
|
call.store<int64_t>(call[11], INT64_MAX);
|
|
if (tracing())
|
|
std::fprintf(stderr, "schannel handshake status %x sent %zu\n", status, produced.size());
|
|
call.ret(status);
|
|
});
|
|
const auto session_of = [provider](ApiCall &call) {
|
|
std::lock_guard guard(provider->lock);
|
|
const auto found = provider->contexts.find(handle_of(call, call[0]));
|
|
return found == provider->contexts.end() ? nullptr : found->second;
|
|
};
|
|
registry.add("QueryContextAttributesW", 3, [session_of](ApiCall &call) {
|
|
const auto session = session_of(call);
|
|
if (!session) {
|
|
call.ret(sec_invalid_handle);
|
|
return;
|
|
}
|
|
switch (call.u32(1)) {
|
|
case attribute_stream_sizes: {
|
|
// SecPkgContext_StreamSizes: header, trailer, largest message, buffer count and block size.
|
|
const uint32_t sizes[] = {record_header, record_trailer, record_payload, 4, 16};
|
|
call.write(call[2], {reinterpret_cast<const uint8_t *>(sizes), sizeof(sizes)});
|
|
break;
|
|
}
|
|
case attribute_application_protocol: {
|
|
// SecPkgContext_ApplicationProtocol: the outcome, the ALPN extension kind, then the chosen protocol with its length.
|
|
const unsigned char *chosen = nullptr;
|
|
unsigned int length = 0;
|
|
SSL_get0_alpn_selected(session->tls.get(), &chosen, &length);
|
|
std::vector<uint8_t> result(8 + 1 + 255);
|
|
result[0] = length ? 1 : 0;
|
|
result[4] = 2;
|
|
result[8] = uint8_t(length);
|
|
if (length)
|
|
std::memcpy(result.data() + 9, chosen, length);
|
|
call.write(call[2], result);
|
|
break;
|
|
}
|
|
case attribute_connection_info: {
|
|
// SecPkgContext_ConnectionInfo: protocol, cipher, hash and exchange with their strengths.
|
|
const int version = SSL_version(session->tls.get());
|
|
const uint32_t protocol = version == TLS1_3_VERSION ? 0x2000 : version == TLS1_2_VERSION ? 0x800 : 0x200;
|
|
const uint32_t info[] = {protocol, 0x6610, 256, 0x800c, 256, 0xae06, 256};
|
|
call.write(call[2], {reinterpret_cast<const uint8_t *>(info), sizeof(info)});
|
|
break;
|
|
}
|
|
default:
|
|
call.ret(sec_unsupported_function);
|
|
return;
|
|
}
|
|
call.ret(sec_ok);
|
|
});
|
|
registry.add("EncryptMessage", 4, [session_of](ApiCall &call) {
|
|
const auto session = session_of(call);
|
|
if (!session || !session->established) {
|
|
call.ret(sec_invalid_handle);
|
|
return;
|
|
}
|
|
const auto list = buffers(call, call[2]);
|
|
const Buffer *header = nullptr, *data = nullptr, *trailer = nullptr;
|
|
for (const auto &buffer : list)
|
|
if (buffer.type == buffer_header)
|
|
header = &buffer;
|
|
else if (buffer.type == buffer_data)
|
|
data = &buffer;
|
|
else if (buffer.type == buffer_trailer)
|
|
trailer = &buffer;
|
|
if (!header || !data || !trailer || data->size > record_payload) {
|
|
call.ret(sec_invalid_token);
|
|
return;
|
|
}
|
|
const auto plain = call.read(data->data, data->size);
|
|
if (!plain.empty() && SSL_write(session->tls.get(), plain.data(), int(plain.size())) <= 0) {
|
|
ERR_clear_error();
|
|
call.ret(sec_internal_error);
|
|
return;
|
|
}
|
|
// The record is spread over the three buffers in order, so a caller that sends them back to back sends it whole.
|
|
const auto record = pending_output(*session);
|
|
const size_t first = std::min<size_t>(header->size, record.size()), second = std::min<size_t>(data->size, record.size() - first),
|
|
rest = record.size() - first - second;
|
|
if (rest > trailer->size) {
|
|
call.ret(sec_buffer_too_small);
|
|
return;
|
|
}
|
|
call.write(header->data, {record.data(), first});
|
|
call.write(data->data, {record.data() + first, second});
|
|
call.write(trailer->data, {record.data() + first + second, rest});
|
|
set_buffer(call, *header, buffer_header, uint32_t(first), header->data);
|
|
set_buffer(call, *data, buffer_data, uint32_t(second), data->data);
|
|
set_buffer(call, *trailer, buffer_trailer, uint32_t(rest), trailer->data);
|
|
if (tracing())
|
|
std::fprintf(stderr, "schannel sealed %zu bytes into %zu\n", plain.size(), record.size());
|
|
call.ret(sec_ok);
|
|
});
|
|
registry.add("DecryptMessage", 4, [session_of](ApiCall &call) {
|
|
const auto session = session_of(call);
|
|
if (!session || !session->established) {
|
|
call.ret(sec_invalid_handle);
|
|
return;
|
|
}
|
|
const auto list = buffers(call, call[1]);
|
|
const Buffer *data = nullptr;
|
|
for (const auto &buffer : list)
|
|
if (buffer.type == buffer_data) {
|
|
data = &buffer;
|
|
break;
|
|
}
|
|
if (!data || list.size() < 4) {
|
|
call.ret(sec_invalid_token);
|
|
return;
|
|
}
|
|
const auto bytes = call.read(data->data, data->size);
|
|
const size_t record = complete_records(bytes, 1);
|
|
std::vector<const Buffer *> empty;
|
|
for (const auto &buffer : list)
|
|
if (&buffer != data && buffer.type == buffer_empty)
|
|
empty.push_back(&buffer);
|
|
if (!record) {
|
|
if (tracing())
|
|
std::fprintf(stderr, "schannel record incomplete with %zu bytes\n", bytes.size());
|
|
if (!empty.empty()) {
|
|
const size_t wanted = bytes.size() < record_header ? record_header : record_header + (size_t(bytes[3]) << 8 | bytes[4]);
|
|
set_buffer(call, *empty.front(), buffer_missing, uint32_t(wanted - bytes.size()), 0);
|
|
}
|
|
call.ret(sec_incomplete_message);
|
|
return;
|
|
}
|
|
// One record per call, decrypted in place: the header, the plaintext, the rest of the record and whatever followed it.
|
|
BIO_write(session->input, bytes.data(), int(record));
|
|
std::vector<uint8_t> plain;
|
|
uint32_t status = sec_ok;
|
|
for (;;) {
|
|
uint8_t chunk[record_payload];
|
|
const int read = SSL_read(session->tls.get(), chunk, sizeof(chunk));
|
|
if (read > 0) {
|
|
plain.insert(plain.end(), chunk, chunk + read);
|
|
continue;
|
|
}
|
|
const int error = SSL_get_error(session->tls.get(), read);
|
|
if (error == SSL_ERROR_ZERO_RETURN)
|
|
status = sec_context_expired;
|
|
else if (error != SSL_ERROR_WANT_READ)
|
|
status = sec_illegal_message;
|
|
ERR_clear_error();
|
|
break;
|
|
}
|
|
if (status == sec_illegal_message || plain.size() + record_header > record) {
|
|
if (tracing())
|
|
std::fprintf(stderr, "schannel record of %zu bytes did not open into %zu\n", record, plain.size());
|
|
call.ret(sec_illegal_message);
|
|
return;
|
|
}
|
|
call.write(data->data + record_header, plain);
|
|
set_buffer(call, list[0], buffer_header, record_header, data->data);
|
|
set_buffer(call, list[1], buffer_data, uint32_t(plain.size()), data->data + record_header);
|
|
set_buffer(call, list[2], buffer_trailer, uint32_t(record - record_header - plain.size()), data->data + record_header + plain.size());
|
|
if (record < bytes.size())
|
|
set_buffer(call, list[3], buffer_extra, uint32_t(bytes.size() - record), data->data + record);
|
|
else
|
|
set_buffer(call, list[3], buffer_empty, 0, 0);
|
|
if (tracing())
|
|
std::fprintf(stderr, "schannel opened %zu of %zu bytes into %zu, status %x\n", record, bytes.size(), plain.size(), status);
|
|
call.ret(status);
|
|
});
|
|
}
|
|
|
|
}
|