tls: extract a shared listener; move FIFA 17's profile into its adapter
The redirector was the only host that spoke TLS, so its TLS lived inside
it. The roster host needs the same listener, and that made the choice
explicit: share this code or copy it.
Copying it is what already went wrong. On 2026-08-11 the Rust redirector
served one certificate while the container served another. ProtoSSL
caches the server certificate per backend, so the redirector -- the first
TLS connection of a session -- decided what the client expected, and
every later service failed its handshake. Silently: Python's socketserver
swallows ssl.SSLError as OSError. Three gates went to it. One place to
configure TLS is the structural fix, so it exists before the second host
does rather than after.
Split along the line the architecture already draws:
openfut-tls how to build an acceptor. Game-independent.
Knows nothing about which suites any client
offers.
adapter-fifa17::tls what FIFA 17 was OBSERVED to offer: the six
enabled suites, the two refused, the TLS 1.2
window, the EA SNI. Plain strings, so the
adapter keeps its lean dependencies -- reading
a card table should not build OpenSSL.
redirector-host joins the two. Chooses no cipher of its own.
Behaviour is unchanged, and shown to be:
* tests/fifa17_tls_profile.rs carries over every case from the deleted
module -- FIFA's eight suites negotiate AES256-GCM-SHA384, each enabled
suite works alone, RC4-only is refused, ECDHE-only is refused. Deleting
a module must not quietly delete its evidence.
* one test pins the composed values literally against the host as it was
when gates 1-14 passed. A "pure refactor" that cannot fail is not a
claim, it is an assumption.
* the rebuilt binary self-tests to the same TLSv1.2 / AES256-GCM-SHA384
the retail client negotiated at 17:09 today.
Two improvements fall out of having one place to look:
* the startup banner now prints cert_sha256. The mismatch above raised no
error at startup and broke the client much later with nothing logged;
it is now the first line of the log.
* tls_min/tls_max print as TLSv1.2 rather than SslVersion(771). This line
is gate evidence and gets read by people.
Nothing deployed and nothing restarted: FIFA is mid-session on the
running redirector, which is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,484 @@
|
||||
//! Legacy-TLS listeners for OpenFUT transport hosts.
|
||||
//!
|
||||
//! # Why this crate exists
|
||||
//!
|
||||
//! Every OpenFUT host that faces a game client needs the same thing: an
|
||||
//! OpenSSL acceptor configured for a client far older than the defaults any
|
||||
//! modern TLS stack ships. Before this crate there was exactly one such host, so
|
||||
//! its TLS lived inside it. The moment a second host needed the same setup, the
|
||||
//! choice was to share this code or copy it.
|
||||
//!
|
||||
//! Copying it is not a style question. On 2026-08-11 the Rust redirector served
|
||||
//! one certificate while the rest of the stack served another. FIFA's ProtoSSL
|
||||
//! caches the server certificate per backend, so the first connection of a
|
||||
//! session decided what the client expected and every later service failed its
|
||||
//! handshake — silently, because Python's `socketserver` swallows `ssl.SSLError`
|
||||
//! as `OSError` and logs nothing. Three gates were lost to it. One place to
|
||||
//! configure TLS is the structural fix.
|
||||
//!
|
||||
//! # What belongs here, and what does not
|
||||
//!
|
||||
//! This crate is **game-independent**. It knows how to build an acceptor, not
|
||||
//! which suites any particular client offers. Cipher lists, protocol windows,
|
||||
//! the SNI a client sends, and which suites are deliberately refused are facts
|
||||
//! about a *game*, and live in that game's adapter — see
|
||||
//! `openfut-adapter-fifa17`'s `tls` module. Values arrive here as plain data so
|
||||
//! adapters never take an OpenSSL dependency.
|
||||
//!
|
||||
//! # Scope of every setting
|
||||
//!
|
||||
//! Everything configured here applies to the `SslContext` being built. Nothing
|
||||
//! global is weakened, and no other OpenFUT crate links OpenSSL.
|
||||
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslOptions, SslVersion};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TlsError(pub String);
|
||||
|
||||
impl fmt::Display for TlsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TlsError {}
|
||||
|
||||
/// A TLS protocol version, expressed without an OpenSSL type.
|
||||
///
|
||||
/// Adapters name the version window their client was observed to use; keeping
|
||||
/// that expressible as plain data is what lets them stay dependency-free.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProtocolVersion {
|
||||
Tls10,
|
||||
Tls11,
|
||||
Tls12,
|
||||
Tls13,
|
||||
}
|
||||
|
||||
impl ProtocolVersion {
|
||||
/// Parse OpenSSL's own spelling, as reported by a handshake.
|
||||
///
|
||||
/// Deliberately strict: an unrecognised version is an error, never a
|
||||
/// silent fallback to a default. Guessing here would mean serving a
|
||||
/// protocol window nobody chose.
|
||||
pub fn parse(s: &str) -> Result<ProtocolVersion, TlsError> {
|
||||
match s.trim() {
|
||||
"TLSv1" | "TLSv1.0" => Ok(ProtocolVersion::Tls10),
|
||||
"TLSv1.1" => Ok(ProtocolVersion::Tls11),
|
||||
"TLSv1.2" => Ok(ProtocolVersion::Tls12),
|
||||
"TLSv1.3" => Ok(ProtocolVersion::Tls13),
|
||||
other => Err(TlsError(format!("unknown TLS version {other:?}"))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ProtocolVersion::Tls10 => "TLSv1.0",
|
||||
ProtocolVersion::Tls11 => "TLSv1.1",
|
||||
ProtocolVersion::Tls12 => "TLSv1.2",
|
||||
ProtocolVersion::Tls13 => "TLSv1.3",
|
||||
}
|
||||
}
|
||||
|
||||
fn to_ssl(self) -> SslVersion {
|
||||
match self {
|
||||
ProtocolVersion::Tls10 => SslVersion::TLS1,
|
||||
ProtocolVersion::Tls11 => SslVersion::TLS1_1,
|
||||
ProtocolVersion::Tls12 => SslVersion::TLS1_2,
|
||||
ProtocolVersion::Tls13 => SslVersion::TLS1_3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TLS knobs, all overridable so a failed retail handshake is a configuration
|
||||
/// change rather than a code change.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TlsConfig {
|
||||
pub cert_path: String,
|
||||
pub key_path: String,
|
||||
pub cipher_list: String,
|
||||
pub min_version: ProtocolVersion,
|
||||
pub max_version: ProtocolVersion,
|
||||
/// `None` = leave OpenSSL's default policy alone. Only set this if a real
|
||||
/// handshake proves it necessary, and record why. Lowering the security
|
||||
/// level pre-emptively weakens the service to fix a problem nobody has
|
||||
/// demonstrated.
|
||||
pub security_level: Option<u32>,
|
||||
}
|
||||
|
||||
impl TlsConfig {
|
||||
/// Build a config from an adapter's observed profile.
|
||||
///
|
||||
/// There is intentionally no `Default`: the cipher list and version window
|
||||
/// are evidence about a specific client, and a generic crate has no
|
||||
/// business inventing them.
|
||||
pub fn new(
|
||||
cert_path: impl Into<String>,
|
||||
key_path: impl Into<String>,
|
||||
cipher_list: impl Into<String>,
|
||||
min_version: ProtocolVersion,
|
||||
max_version: ProtocolVersion,
|
||||
) -> TlsConfig {
|
||||
TlsConfig {
|
||||
cert_path: cert_path.into(),
|
||||
key_path: key_path.into(),
|
||||
cipher_list: cipher_list.into(),
|
||||
min_version,
|
||||
max_version,
|
||||
security_level: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The OpenSSL version this binary is linked against.
|
||||
///
|
||||
/// Vendored, so it is fixed at build time rather than inherited from the host.
|
||||
/// Printed at startup and recorded in gate evidence.
|
||||
pub fn openssl_version() -> String {
|
||||
openssl::version::version().to_string()
|
||||
}
|
||||
|
||||
/// Build a listener acceptor from `cfg`.
|
||||
pub fn build_acceptor(cfg: &TlsConfig) -> Result<SslAcceptor, TlsError> {
|
||||
// `mozilla_intermediate` preloads a modern, forward-secret-only policy —
|
||||
// precisely wrong for a legacy client. It is used only as a starting
|
||||
// builder; every setting that matters is then stated explicitly below, so
|
||||
// nothing is inherited silently.
|
||||
let mut b = SslAcceptor::mozilla_intermediate(SslMethod::tls())
|
||||
.map_err(|e| TlsError(format!("acceptor: {e}")))?;
|
||||
|
||||
b.set_min_proto_version(Some(cfg.min_version.to_ssl()))
|
||||
.map_err(|e| TlsError(format!("min proto: {e}")))?;
|
||||
b.set_max_proto_version(Some(cfg.max_version.to_ssl()))
|
||||
.map_err(|e| TlsError(format!("max proto: {e}")))?;
|
||||
|
||||
// Explicit list, replacing whatever policy the profile brought with it.
|
||||
b.set_cipher_list(&cfg.cipher_list)
|
||||
.map_err(|e| TlsError(format!("cipher list {:?}: {e}", cfg.cipher_list)))?;
|
||||
|
||||
if let Some(level) = cfg.security_level {
|
||||
b.set_security_level(level);
|
||||
}
|
||||
|
||||
// SSLv3 is never acceptable, whatever the version floor says.
|
||||
b.set_options(SslOptions::NO_SSLV3);
|
||||
|
||||
let cert = Path::new(&cfg.cert_path);
|
||||
let key = Path::new(&cfg.key_path);
|
||||
if !cert.exists() {
|
||||
return Err(TlsError(format!(
|
||||
"certificate not found: {}",
|
||||
cfg.cert_path
|
||||
)));
|
||||
}
|
||||
if !key.exists() {
|
||||
return Err(TlsError(format!("private key not found: {}", cfg.key_path)));
|
||||
}
|
||||
b.set_certificate_chain_file(cert)
|
||||
.map_err(|e| TlsError(format!("certificate {}: {e}", cfg.cert_path)))?;
|
||||
b.set_private_key_file(key, SslFiletype::PEM)
|
||||
.map_err(|e| TlsError(format!("private key {}: {e}", cfg.key_path)))?;
|
||||
// Catches a mismatched pair at startup rather than mid-handshake, where it
|
||||
// would look like a client problem.
|
||||
b.check_private_key()
|
||||
.map_err(|e| TlsError(format!("certificate and key do not match: {e}")))?;
|
||||
|
||||
Ok(b.build())
|
||||
}
|
||||
|
||||
/// The SHA-256 fingerprint of the configured certificate, colon-separated.
|
||||
///
|
||||
/// Exists so a host can log which certificate it is serving, and so a caller
|
||||
/// can compare hosts. Serving a *different* certificate from the rest of the
|
||||
/// stack is not a visible error at startup — it fails much later, inside a
|
||||
/// client that logs nothing — so the value is worth printing unprompted.
|
||||
pub fn certificate_fingerprint(cert_path: &str) -> Result<String, TlsError> {
|
||||
let pem = std::fs::read(cert_path)
|
||||
.map_err(|e| TlsError(format!("reading certificate {cert_path}: {e}")))?;
|
||||
let cert = openssl::x509::X509::from_pem(&pem)
|
||||
.map_err(|e| TlsError(format!("parsing certificate {cert_path}: {e}")))?;
|
||||
let digest = cert
|
||||
.digest(openssl::hash::MessageDigest::sha256())
|
||||
.map_err(|e| TlsError(format!("fingerprint: {e}")))?;
|
||||
Ok(digest
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":"))
|
||||
}
|
||||
|
||||
/// What a handshake actually negotiated.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Negotiated {
|
||||
pub version: String,
|
||||
pub cipher: String,
|
||||
}
|
||||
|
||||
/// Handshake against ourselves with a client restricted to `client_ciphers`.
|
||||
///
|
||||
/// Enumerating the server's configured list would be the obvious check, but the
|
||||
/// openssl crate exposes no such accessor — and a real handshake is better
|
||||
/// evidence anyway: it proves MUTUAL support and reports what was actually
|
||||
/// chosen, which is the thing a gate is judged on. Passing an adapter's
|
||||
/// observed client suites makes this a direct rehearsal of the retail
|
||||
/// handshake.
|
||||
///
|
||||
/// `sni` is the name the real client sends, so the rehearsal matches it.
|
||||
pub fn self_test(cfg: &TlsConfig, client_ciphers: &str, sni: &str) -> Result<Negotiated, TlsError> {
|
||||
use openssl::ssl::{SslConnector, SslVerifyMode};
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
let acceptor = build_acceptor(cfg)?;
|
||||
let listener = TcpListener::bind("127.0.0.1:0").map_err(|e| TlsError(format!("bind: {e}")))?;
|
||||
let addr = listener
|
||||
.local_addr()
|
||||
.map_err(|e| TlsError(format!("addr: {e}")))?;
|
||||
|
||||
let server = std::thread::spawn(move || -> Result<Negotiated, String> {
|
||||
let (sock, _) = listener.accept().map_err(|e| e.to_string())?;
|
||||
let mut tls = acceptor.accept(sock).map_err(|e| e.to_string())?;
|
||||
let neg = {
|
||||
let s = tls.ssl();
|
||||
Negotiated {
|
||||
version: s.version_str().to_string(),
|
||||
cipher: s
|
||||
.current_cipher()
|
||||
.map(|c| c.name().to_string())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
};
|
||||
let mut buf = [0u8; 64];
|
||||
let _ = tls.read(&mut buf);
|
||||
let _ = tls.write_all(b"ok");
|
||||
let _ = tls.flush();
|
||||
Ok(neg)
|
||||
});
|
||||
|
||||
let mut cb =
|
||||
SslConnector::builder(SslMethod::tls()).map_err(|e| TlsError(format!("connector: {e}")))?;
|
||||
cb.set_cipher_list(client_ciphers)
|
||||
.map_err(|e| TlsError(format!("client cipher list: {e}")))?;
|
||||
// The point is cipher/version negotiation, not PKI: the certificate is
|
||||
// self-signed and the real client's checks are patched out.
|
||||
cb.set_verify(SslVerifyMode::NONE);
|
||||
let connector = cb.build();
|
||||
|
||||
let sock = TcpStream::connect(addr).map_err(|e| TlsError(format!("connect: {e}")))?;
|
||||
let mut client = connector
|
||||
.configure()
|
||||
.and_then(|c| c.verify_hostname(false).into_ssl(sni))
|
||||
.and_then(|ssl| openssl::ssl::SslStream::new(ssl, sock))
|
||||
.map_err(|e| TlsError(format!("client setup: {e}")))?;
|
||||
client
|
||||
.connect()
|
||||
.map_err(|e| TlsError(format!("client handshake failed: {e}")))?;
|
||||
let _ = client.write_all(b"ping");
|
||||
let _ = client.flush();
|
||||
let mut buf = [0u8; 8];
|
||||
let _ = client.read(&mut buf);
|
||||
|
||||
server
|
||||
.join()
|
||||
.map_err(|_| TlsError("server thread panicked".into()))?
|
||||
.map_err(|e| TlsError(format!("server handshake failed: {e}")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A self-signed pair, generated here rather than borrowed from the game
|
||||
/// stack: this crate is game-independent and its tests must not depend on
|
||||
/// FIFA material. Adapter crates test their own profiles.
|
||||
fn keypair() -> (String, String) {
|
||||
use openssl::asn1::Asn1Time;
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::rsa::Rsa;
|
||||
use openssl::x509::{X509Name, X509};
|
||||
|
||||
let dir = std::env::temp_dir().join(format!("openfut-tls-test-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let cert_path = dir.join("cert.pem");
|
||||
let key_path = dir.join("key.pem");
|
||||
if cert_path.exists() && key_path.exists() {
|
||||
return (
|
||||
cert_path.to_string_lossy().into(),
|
||||
key_path.to_string_lossy().into(),
|
||||
);
|
||||
}
|
||||
|
||||
let rsa = Rsa::generate(2048).unwrap();
|
||||
let pkey = PKey::from_rsa(rsa).unwrap();
|
||||
let mut name = X509Name::builder().unwrap();
|
||||
name.append_entry_by_text("CN", "openfut-tls-test").unwrap();
|
||||
let name = name.build();
|
||||
|
||||
let mut b = X509::builder().unwrap();
|
||||
b.set_version(2).unwrap();
|
||||
b.set_subject_name(&name).unwrap();
|
||||
b.set_issuer_name(&name).unwrap();
|
||||
b.set_pubkey(&pkey).unwrap();
|
||||
b.set_not_before(&Asn1Time::days_from_now(0).unwrap())
|
||||
.unwrap();
|
||||
b.set_not_after(&Asn1Time::days_from_now(3650).unwrap())
|
||||
.unwrap();
|
||||
b.sign(&pkey, MessageDigest::sha256()).unwrap();
|
||||
let cert = b.build();
|
||||
|
||||
std::fs::write(&cert_path, cert.to_pem().unwrap()).unwrap();
|
||||
std::fs::write(&key_path, pkey.private_key_to_pem_pkcs8().unwrap()).unwrap();
|
||||
(
|
||||
cert_path.to_string_lossy().into(),
|
||||
key_path.to_string_lossy().into(),
|
||||
)
|
||||
}
|
||||
|
||||
/// A legacy static-RSA profile, standing in for what an adapter supplies.
|
||||
fn cfg() -> TlsConfig {
|
||||
let (c, k) = keypair();
|
||||
TlsConfig::new(
|
||||
c,
|
||||
k,
|
||||
"AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA",
|
||||
ProtocolVersion::Tls12,
|
||||
ProtocolVersion::Tls12,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_acceptor_builds_from_a_valid_pair() {
|
||||
assert!(build_acceptor(&cfg()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_static_rsa_client_completes_a_handshake() {
|
||||
let neg = self_test(&cfg(), "AES256-GCM-SHA384", "example.invalid").expect("handshake");
|
||||
assert_eq!(neg.cipher, "AES256-GCM-SHA384");
|
||||
assert_eq!(neg.version, "TLSv1.2");
|
||||
}
|
||||
|
||||
/// The configured list is authoritative: a client offering only something
|
||||
/// outside it must fail. Without this, an acceptor that silently kept the
|
||||
/// modern default profile would look identical to a correct one.
|
||||
#[test]
|
||||
fn a_suite_outside_the_configured_list_is_refused() {
|
||||
let r = self_test(&cfg(), "ECDHE-RSA-AES256-GCM-SHA384", "example.invalid");
|
||||
assert!(r.is_err(), "expected refusal, got {r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_protocol_window_is_enforced() {
|
||||
let (c, k) = keypair();
|
||||
let mut cfg = TlsConfig::new(
|
||||
c,
|
||||
k,
|
||||
"AES256-GCM-SHA384",
|
||||
ProtocolVersion::Tls12,
|
||||
ProtocolVersion::Tls12,
|
||||
);
|
||||
assert_eq!(
|
||||
self_test(&cfg, "AES256-GCM-SHA384", "x").unwrap().version,
|
||||
"TLSv1.2"
|
||||
);
|
||||
// Ceiling raised: TLS 1.3 has its own suites, so a 1.2-only client list
|
||||
// still lands on 1.2 — the window is a window, not a forced version.
|
||||
cfg.max_version = ProtocolVersion::Tls13;
|
||||
assert_eq!(
|
||||
self_test(&cfg, "AES256-GCM-SHA384", "x").unwrap().version,
|
||||
"TLSv1.2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_certificate_fails_clearly() {
|
||||
let mut c = cfg();
|
||||
c.cert_path = "/nonexistent/cert.pem".into();
|
||||
let e = match build_acceptor(&c) {
|
||||
Err(e) => e.to_string(),
|
||||
Ok(_) => panic!("a missing certificate must not build"),
|
||||
};
|
||||
assert!(e.contains("certificate not found"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mismatched_key_is_caught_at_startup() {
|
||||
let mut c = cfg();
|
||||
// A valid PEM that is not the key.
|
||||
c.key_path = c.cert_path.clone();
|
||||
assert!(build_acceptor(&c).is_err(), "mismatch must not build");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_level_is_not_lowered_by_default() {
|
||||
assert_eq!(cfg().security_level, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn versions_round_trip_and_unknown_ones_are_refused() {
|
||||
for v in [
|
||||
ProtocolVersion::Tls10,
|
||||
ProtocolVersion::Tls11,
|
||||
ProtocolVersion::Tls12,
|
||||
ProtocolVersion::Tls13,
|
||||
] {
|
||||
assert_eq!(ProtocolVersion::parse(v.as_str()).unwrap(), v);
|
||||
}
|
||||
// OpenSSL reports 1.0 as bare "TLSv1"; accept what a handshake reports.
|
||||
assert_eq!(
|
||||
ProtocolVersion::parse("TLSv1").unwrap(),
|
||||
ProtocolVersion::Tls10
|
||||
);
|
||||
assert!(ProtocolVersion::parse("SSLv3").is_err());
|
||||
assert!(ProtocolVersion::parse("").is_err());
|
||||
}
|
||||
|
||||
/// The fingerprint must be the certificate's, so two hosts can be compared.
|
||||
#[test]
|
||||
fn the_fingerprint_matches_openssl_and_differs_between_certificates() {
|
||||
let (c, _) = keypair();
|
||||
let fp = certificate_fingerprint(&c).expect("fingerprint");
|
||||
assert_eq!(fp.len(), 32 * 3 - 1, "32 hex pairs, colon separated: {fp}");
|
||||
assert!(fp.chars().all(|ch| ch.is_ascii_hexdigit() || ch == ':'));
|
||||
|
||||
// A different certificate must produce a different fingerprint — this
|
||||
// is the whole point of the check that guards cross-host parity.
|
||||
let dir = std::env::temp_dir().join(format!("openfut-tls-alt-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let other = dir.join("other.pem");
|
||||
if !other.exists() {
|
||||
// Reuse the generator by pointing at a fresh temp dir.
|
||||
let rsa = openssl::rsa::Rsa::generate(2048).unwrap();
|
||||
let pkey = openssl::pkey::PKey::from_rsa(rsa).unwrap();
|
||||
let mut nb = openssl::x509::X509Name::builder().unwrap();
|
||||
nb.append_entry_by_text("CN", "other").unwrap();
|
||||
let name = nb.build();
|
||||
let mut b = openssl::x509::X509::builder().unwrap();
|
||||
b.set_version(2).unwrap();
|
||||
b.set_subject_name(&name).unwrap();
|
||||
b.set_issuer_name(&name).unwrap();
|
||||
b.set_pubkey(&pkey).unwrap();
|
||||
b.set_not_before(&openssl::asn1::Asn1Time::days_from_now(0).unwrap())
|
||||
.unwrap();
|
||||
b.set_not_after(&openssl::asn1::Asn1Time::days_from_now(1).unwrap())
|
||||
.unwrap();
|
||||
b.sign(&pkey, openssl::hash::MessageDigest::sha256())
|
||||
.unwrap();
|
||||
std::fs::write(&other, b.build().to_pem().unwrap()).unwrap();
|
||||
}
|
||||
let fp2 = certificate_fingerprint(other.to_str().unwrap()).unwrap();
|
||||
assert_ne!(fp, fp2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unreadable_certificate_reports_the_path() {
|
||||
let e = certificate_fingerprint("/nonexistent/x.pem")
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(e.contains("/nonexistent/x.pem"), "{e}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user