// Copyright 2025 International Digital Economy Academy
// Copyright 2026 Strangelight-Merser
//
// Licensed 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.
///|
/// Extract a `TlsError` message for tests and callers that caught `Error`.
pub fn describe_tls_error(err : Error) -> String {
try {
raise err
} catch {
TlsError(msg) => msg
_ => "\{err}"
}
}
///|
/// PEM files presented by a TLS client during handshake.
///
/// `certificate_chain_file` is the leaf certificate followed by any
/// intermediate certificates required to reach a CA the peer trusts.
/// `private_key_file` must be an unencrypted PEM private key. Encrypted keys
/// are rejected without prompting for a passphrase.
pub(all) struct ClientIdentity {
certificate_chain_file : String
private_key_file : String
}
///|
#cfg(not(platform="windows"))
fn private_key_is_encrypted(text : String) -> Bool {
text.contains("BEGIN ENCRYPTED PRIVATE KEY") ||
text.contains("Proc-Type: 4,ENCRYPTED")
}
///|
#cfg(not(platform="windows"))
extern "C" fn SSL_CTX::reject_encrypted_keys(self : SSL_CTX) = "strangelight_async_tls_ssl_ctx_reject_encrypted_keys"
///|
#cfg(not(platform="windows"))
#borrow(file)
extern "C" fn SSL_CTX::use_certificate_chain_file(
self : SSL_CTX,
file : Bytes,
) -> Int = "strangelight_async_tls_ssl_ctx_use_certificate_chain_file"
///|
#cfg(not(platform="windows"))
#borrow(file)
extern "C" fn SSL_CTX::use_private_key_file(
self : SSL_CTX,
file : Bytes,
file_type : X509FileType,
) -> Int = "strangelight_async_tls_ssl_ctx_use_private_key_file"
///|
#cfg(not(platform="windows"))
extern "C" fn SSL_CTX::check_private_key(self : SSL_CTX) -> Int = "strangelight_async_tls_ssl_ctx_check_private_key"
///|
#cfg(not(platform="windows"))
extern "C" fn SSL_CTX::require_client_certificate(self : SSL_CTX) = "strangelight_async_tls_ssl_ctx_require_client_certificate"
///|
/// Set the minimum protocol version through `SSL_CTX_ctrl`. OpenSSL 3 exposes
/// `SSL_CTX_set_min_proto_version` only as a macro, so it cannot be `dlsym`'d.
#cfg(not(platform="windows"))
extern "C" fn SSL_CTX::set_min_proto_version(
self : SSL_CTX,
version : Int,
) -> Int = "strangelight_async_tls_ssl_ctx_set_min_proto_version"
///|
#cfg(not(platform="windows"))
extern "C" fn SSL::require_client_certificate(self : SSL) = "strangelight_async_tls_ssl_require_client_certificate"
///|
#cfg(not(platform="windows"))
async fn read_text_file(path : String, what~ : String) -> String raise {
try {
@fs.read_file(path).text()
} catch {
err =>
raise TlsError("client identity: failed to read \{what} \{path}: \{err}")
}
}
///|
#cfg(not(platform="windows"))
async fn SSL_CTX::load_pem_trust(self : SSL_CTX, path : String) -> Unit raise {
let pem = try {
@fs.read_file(path).text()
} catch {
err => raise TlsError("failed to read trust file \{path}: \{err}")
}
for cert in decode_pem_certificates(pem) {
if self.add_root_certificate(cert) != 1 {
raise TlsError(err_get_error())
}
}
}
///|
#cfg(not(platform="windows"))
async fn SSL_CTX::apply_client_identity(self : SSL_CTX, identity : ClientIdentity) -> Unit raise {
self.reject_encrypted_keys()
let key_text = read_text_file(
identity.private_key_file,
what="private key file",
)
guard !private_key_is_encrypted(key_text) else {
raise TlsError("client identity: encrypted private keys are not supported")
}
let certificate_chain_file = @utf8.encode(identity.certificate_chain_file)
if self.use_certificate_chain_file(certificate_chain_file) != 1 {
raise TlsError(
"client identity: failed to load certificate chain: " + err_get_error(),
)
}
let private_key_file = @utf8.encode(identity.private_key_file)
if self.use_private_key_file(private_key_file, PEM) != 1 {
raise TlsError(
"client identity: failed to load private key: " + err_get_error(),
)
}
if self.check_private_key() != 1 {
raise TlsError("client identity: certificate and private key do not match")
}
}