"syn 2.0.117",
]
+[[package]]
+name = "aws-lc-rs"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
+dependencies = [
+ "aws-lc-sys",
+ "untrusted 0.7.1",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+]
+
[[package]]
name = "backtrace"
version = "0.3.76"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
dependencies = [
"find-msvc-tools",
+ "jobserver",
+ "libc",
"shlex",
]
"error-code",
]
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "colorchoice"
version = "1.0.5"
"percent-encoding",
]
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
[[package]]
name = "js-sys"
version = "0.3.99"
"libc",
"log",
"openssl",
- "openssl-probe",
+ "openssl-probe 0.2.1",
"openssl-sys",
"schannel",
- "security-framework",
+ "security-framework 3.7.0",
"security-framework-sys",
"tempfile",
]
"syn 2.0.117",
]
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
[[package]]
name = "openssl-probe"
version = "0.2.1"
"cfg-if",
"getrandom 0.2.17",
"libc",
- "untrusted",
+ "untrusted 0.9.0",
"windows-sys 0.52.0",
]
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
dependencies = [
+ "aws-lc-rs",
"log",
"ring",
"rustls-pki-types",
"zeroize",
]
+[[package]]
+name = "rustls-native-certs"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
+dependencies = [
+ "openssl-probe 0.1.6",
+ "rustls-pemfile 2.2.0",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework 2.11.1",
+]
+
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
+ "aws-lc-rs",
"ring",
"rustls-pki-types",
- "untrusted",
+ "untrusted 0.9.0",
]
[[package]]
version = "0.10.0"
dependencies = [
"arcu",
+ "aws-lc-rs",
"base64 0.22.1",
"bit-set",
"bitvec",
"fxhash",
"getrandom 0.2.17",
"git-version",
+ "hex",
"hostname",
"iai-callgrind",
"indexmap",
"libloading",
"maplit",
"modular-bitfield",
- "native-tls",
"num-order",
"ordered-float",
"ouroboros",
"ring",
"ripemd",
"roxmltree",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pemfile 2.2.0",
"rustyline",
"ryu",
"scraper",
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.11.1",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
[[package]]
name = "untrusted"
version = "0.9.0"
ffi = ["dep:libffi"]
repl = ["dep:crossterm", "dep:ctrlc", "dep:rustyline"]
hostname = ["dep:hostname"]
-tls = ["dep:native-tls"]
+tls = ["dep:rustls", "dep:rustls-native-certs", "dep:rustls-pemfile", "dep:aws-lc-rs", "dep:hex"]
http = ["dep:warp", "dep:reqwest"]
# crypto function that require non pure-rust dependencies
crypto-full = ["dep:ring"]
ctrlc = { version = "3.4.4", optional = true }
hostname = { version = "0.4.0", optional = true }
libffi = { version = "5.1.0", optional = true }
-native-tls = { version = "0.2.12", optional = true }
+rustls-pemfile = { version = "2", optional = true }
# the version requirement of reqwest is kept low for compatibility with old deno versions
# that pin reqwest to 0.11.20
reqwest = { version = "0.11.0", optional = true }
+rustls = { version = "0.22", optional = true, features = ["aws_lc_rs"] }
+aws-lc-rs = { version = "1.15", optional = true }
+hex = { version = "0.4", optional = true }
+rustls-native-certs = { version = "0.7", optional = true }
rustyline = { version = "18.0.0", optional = true }
tokio = { version = "1.39.2", features = ["full"] }
warp = { version = "0.3.7", features = ["tls"], optional = true }
SocketServerClose,
#[strum_discriminants(strum(props(Arity = "2", Name = "$copy_stream")))]
CopyStream,
- #[strum_discriminants(strum(props(Arity = "4", Name = "$tls_accept_client")))]
+ #[strum_discriminants(strum(props(Arity = "5", Name = "$tls_accept_client")))]
TLSAcceptClient,
#[strum_discriminants(strum(props(Arity = "3", Name = "$tls_client_connect")))]
TLSClientConnect,
:- module(tls, [tls_client_context/2, % -Context, +Options
tls_client_negotiate/3, % +Context, +Stream0, -Stream
tls_server_context/2, % -Context, +Options
- tls_server_negotiate/3 % +Context, +Stream0, -Stream
+ tls_server_negotiate/4 % +Context, +Stream0, -Stream, -ClientCert
]).
:- use_module(library(lists)).
Use tls_server_context/2 to create a TLS context, for example with:
- tls_server_context(Context, [pkcs12(Chars)])
+ tls_server_context(Context, [certificate(Cert),key(Key)])
- where Chars is a list of characters with the contents of a
- DER-formatted PKCS #12 archive. The option password(Ps) can be used
- to specify the password Ps (also a string) for decrypting the key.
- On some versions of OSX, and potentially also on other platforms,
- empty passwords are not supported.
+ where Cert is a list of characters with the contents of a
+ PEM-encoded certificate chain, and Key is a list of characters with
+ the contents of the corresponding PEM-encoded private key. The
+ private key may be in PKCS #8, PKCS #1 (RSA) or SEC1 (EC) format.
- The archive should contain a leaf certificate and its private key,
- as well any intermediate certificates that should be sent to
- clients to allow them to build a chain to a trusted root. The chain
+ The certificate chain should contain a leaf certificate followed by
+ any intermediate certificates that should be sent to clients to
+ allow them to build a chain to a trusted root. The chain
certificates should be in order from the leaf certificate towards
the root.
- PKCS #12 archives typically have the file extension .p12 or .pfx,
- and can be created with the OpenSSL pkcs12 tool:
-
- $ openssl pkcs12 -export -out identity.pfx \
- -inkey key.pem -in cert.pem -certfile chain_certs.pem
-
-
You can use phrase_from_file/3 from library(pio) and seq//1 from
- library(dcgs) to read the contents of "identity.pfx" into a string:
+ library(dcgs) to read the contents of "cert.pem" and "key.pem" into
+ strings:
- phrase_from_file(seq(Chars), "identity.pfx", [type(binary)])
+ phrase_from_file(seq(Cert), "cert.pem"), phrase_from_file(seq(Key), "key.pem")
The obtained context should be treated as an opaque Prolog term.
is created for every connection, using the specified parameters.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-tls_server_context(tls_context(Cert,Password), Options) :-
- ( member(pcks12(Cert), Options) ->
+tls_server_context(tls_context(Cert,Key), Options) :-
+ ( member(certificate(Cert), Options) ->
must_be(chars, Cert)
- ; domain_error(contains_pcks12, Options, tls_server_context/2)
+ ; domain_error(contains_certificate, Options, tls_server_context/2)
),
- ( member(password(Password), Options) ->
- must_be(chars, Password)
- ; Password = ""
+ ( member(key(Key), Options) ->
+ must_be(chars, Key)
+ ; domain_error(contains_key, Options, tls_server_context/2)
).
-tls_server_negotiate(tls_context(Cert,Password), S0, S) :-
- '$tls_accept_client'(Cert, Password, S0, S).
-
+tls_server_negotiate(tls_context(Cert,Key), S0, S, ClientCert) :-
+ '$tls_accept_client'(Cert, Key, S0, S, ClientCert).
use std::sync::mpsc::TryRecvError;
#[cfg(feature = "tls")]
-use native_tls::TlsStream;
+use rustls::{ClientConnection, ServerConnection, StreamOwned as RustlsStreamOwned};
#[cfg(feature = "http")]
use warp::hyper;
}
#[cfg(feature = "tls")]
-#[derive(Debug)]
+pub enum TlsStream {
+ Client(RustlsStreamOwned<ClientConnection, Stream>),
+ Server(RustlsStreamOwned<ServerConnection, Stream>),
+}
+
+#[cfg(feature = "tls")]
+impl TlsStream {
+ #[inline]
+ pub fn send_close_notify(&mut self) {
+ match self {
+ TlsStream::Client(s) => s.conn.send_close_notify(),
+ TlsStream::Server(s) => s.conn.send_close_notify(),
+ }
+ }
+}
+
+#[cfg(feature = "tls")]
+impl Read for TlsStream {
+ #[inline]
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ match self {
+ TlsStream::Client(s) => s.read(buf),
+ TlsStream::Server(s) => s.read(buf),
+ }
+ }
+}
+
+#[cfg(feature = "tls")]
+impl Write for TlsStream {
+ #[inline]
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ match self {
+ TlsStream::Client(s) => s.write(buf),
+ TlsStream::Server(s) => s.write(buf),
+ }
+ }
+
+ #[inline]
+ fn flush(&mut self) -> std::io::Result<()> {
+ match self {
+ TlsStream::Client(s) => s.flush(),
+ TlsStream::Server(s) => s.flush(),
+ }
+ }
+}
+
+#[cfg(feature = "tls")]
pub struct NamedTlsStream {
address: Atom,
- tls_stream: TlsStream<Stream>,
+ tls_stream: TlsStream,
+}
+
+#[cfg(feature = "tls")]
+impl fmt::Debug for NamedTlsStream {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("NamedTlsStream")
+ .field("address", &self.address)
+ .finish_non_exhaustive()
+ }
}
#[cfg(feature = "tls")]
#[inline]
pub(crate) fn from_tls_stream(
address: Atom,
- tls_stream: TlsStream<Stream>,
+ tls_stream: TlsStream,
arena: &mut Arena,
) -> Self {
Stream::NamedTls(arena_alloc!(
tcp_stream.inner_mut().tcp_stream.shutdown(Shutdown::Both)
}
#[cfg(feature = "tls")]
- Stream::NamedTls(ref mut tls_stream) => tls_stream.inner_mut().tls_stream.shutdown(),
+ Stream::NamedTls(ref mut tls_stream) => {
+ let inner = tls_stream.inner_mut();
+ inner.tls_stream.send_close_notify();
+ inner.tls_stream.flush()
+ }
#[cfg(feature = "http")]
Stream::HttpRead(ref mut http_stream) => {
http_stream.drop_payload();
#[cfg(feature = "http")]
use std::str::FromStr;
use std::sync::LazyLock;
+#[cfg(any(feature = "http", feature = "tls"))]
+use std::sync::Arc;
#[cfg(feature = "http")]
-use std::sync::{Arc, Condvar, Mutex};
+use std::sync::{Condvar, Mutex};
use chrono::{offset::Local, DateTime};
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod special_math;
#[cfg(feature = "tls")]
-use native_tls::{Identity, TlsAcceptor, TlsConnector};
+use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
+#[cfg(feature = "tls")]
+use rustls::{DistinguishedName, ClientConfig, ClientConnection, RootCertStore, ServerConfig, ServerConnection, SignatureScheme};
+#[cfg(feature = "tls")]
+use aws_lc_rs;
+#[cfg(feature = "tls")]
+use rustls::server::danger::*;
use base64;
use roxmltree;
3,
)?;
- let connector = TlsConnector::new().unwrap();
- let stream = match connector.connect(&hostname.as_str(), stream0) {
- Ok(tls_stream) => tls_stream,
+ let mut roots = RootCertStore::empty();
+ if let Ok(certs) = rustls_native_certs::load_native_certs() {
+ for cert in certs {
+ let _ = roots.add(cert);
+ }
+ }
+
+ let config = ClientConfig::builder()
+ .with_root_certificates(roots)
+ .with_no_client_auth();
+
+ let server_name = match ServerName::try_from(hostname.as_str().to_string()) {
+ Ok(name) => name,
+ Err(_) => {
+ return Err(self.machine_st.open_permission_error(
+ self.machine_st.registers[1],
+ atom!("tls_client_negotiate"),
+ 3,
+ ));
+ }
+ };
+
+ let mut conn = match ClientConnection::new(Arc::new(config), server_name) {
+ Ok(conn) => conn,
Err(_) => {
return Err(self.machine_st.open_permission_error(
self.machine_st.registers[1],
}
};
+ let mut sock = stream0;
+ if conn.complete_io(&mut sock).is_err() {
+ return Err(self.machine_st.open_permission_error(
+ self.machine_st.registers[1],
+ atom!("tls_client_negotiate"),
+ 3,
+ ));
+ }
+
+ let tls_stream = crate::machine::streams::TlsStream::Client(
+ rustls::StreamOwned::new(conn, sock),
+ );
+
let addr = atom!("TLS");
- let stream = Stream::from_tls_stream(addr, stream, &mut self.machine_st.arena);
+ let stream = Stream::from_tls_stream(addr, tls_stream, &mut self.machine_st.arena);
self.indices
.add_stream(stream, atom!("tls_client_negotiate"), 3)
#[cfg(feature = "tls")]
#[inline(always)]
pub(crate) fn tls_accept_client(&mut self) -> CallResult {
- let pkcs12 = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet"));
+ let cert_pem = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet"));
+ let key_pem = self.string_encoding_bytes(self.machine_st.registers[2], atom!("octet"));
- if let Some(password) = self
- .machine_st
- .value_to_str_like(self.machine_st.registers[2])
- {
- let identity = match Identity::from_pkcs12(&pkcs12, &password.as_str()) {
- Ok(identity) => identity,
- Err(_) => {
+ let cert_chain: Vec<CertificateDer<'static>> =
+ match rustls_pemfile::certs(&mut &cert_pem[..]).collect::<Result<Vec<_>, _>>() {
+ Ok(certs) if !certs.is_empty() => certs,
+ _ => {
return Err(self.machine_st.open_permission_error(
self.machine_st.registers[1],
atom!("tls_server_negotiate"),
}
};
- let stream0 = self.machine_st.get_stream_or_alias(
- self.machine_st.registers[3],
- &self.indices,
- atom!("tls_server_negotiate"),
- 3,
- )?;
-
- let acceptor = TlsAcceptor::new(identity).unwrap();
-
- let stream = match acceptor.accept(stream0) {
- Ok(tls_stream) => tls_stream,
- Err(_) => {
+ let private_key: PrivateKeyDer<'static> =
+ match rustls_pemfile::private_key(&mut &key_pem[..]) {
+ Ok(Some(key)) => key,
+ _ => {
return Err(self.machine_st.open_permission_error(
- self.machine_st.registers[3],
+ self.machine_st.registers[2],
atom!("tls_server_negotiate"),
3,
));
}
};
- let stream = Stream::from_tls_stream(atom!("TLS"), stream, &mut self.machine_st.arena);
+ let stream0 = self.machine_st.get_stream_or_alias(
+ self.machine_st.registers[3],
+ &self.indices,
+ atom!("tls_server_negotiate"),
+ 3,
+ )?;
- self.indices
- .add_stream(stream, atom!("tls_server_negotiate"), 3)
- .map_err(|stub_gen| stub_gen(&mut self.machine_st))?;
+ let config = match ServerConfig::builder()
+ .with_client_cert_verifier(Arc::new(GeminiClientVerifier))
+ .with_single_cert(cert_chain, private_key)
+ {
+ Ok(config) => config,
+ Err(_) => {
+ return Err(self.machine_st.open_permission_error(
+ self.machine_st.registers[1],
+ atom!("tls_server_negotiate"),
+ 3,
+ ));
+ }
+ };
- let stream_addr = self.deref_register(4);
- self.machine_st
- .bind(stream_addr.as_var().unwrap(), stream.into());
- } else {
- unreachable!();
+ let mut conn = match ServerConnection::new(Arc::new(config)) {
+ Ok(conn) => conn,
+ Err(_) => {
+ return Err(self.machine_st.open_permission_error(
+ self.machine_st.registers[3],
+ atom!("tls_server_negotiate"),
+ 3,
+ ));
+ }
+ };
+
+
+
+
+ let mut sock = stream0;
+ if conn.complete_io(&mut sock).is_err() {
+ return Err(self.machine_st.open_permission_error(
+ self.machine_st.registers[3],
+ atom!("tls_server_negotiate"),
+ 3,
+ ));
}
+
+ let client_digest = conn.peer_certificates().and_then(|peer_certs| {
+ peer_certs.first().map(|client_cert| {
+ let cert_bytes = client_cert.as_ref();
+ aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, cert_bytes)
+ })
+ });
+
+ let tls_stream = crate::machine::streams::TlsStream::Server(
+ rustls::StreamOwned::new(conn, sock),
+ );
+ let stream = Stream::from_tls_stream(atom!("TLS"), tls_stream, &mut self.machine_st.arena);
+
+ self.indices
+ .add_stream(stream, atom!("tls_server_negotiate"), 3)
+ .map_err(|stub_gen| stub_gen(&mut self.machine_st))?;
+
+ let stream_addr = self.deref_register(4);
+ self.machine_st
+ .bind(stream_addr.as_var().unwrap(), stream.into());
+
+ let digest_cell = match client_digest {
+ Some(digest) => {
+ let bytes = digest.as_ref();
+ resource_error_call_result!(
+ self.machine_st,
+ sized_iter_to_heap_list(
+ &mut self.machine_st.heap,
+ bytes.len(),
+ bytes
+ .iter()
+ .map(|b| fixnum_as_cell!(Fixnum::build_with(*b)))
+ )
+ )
+ }
+ None => atom_as_cell!(atom!("none")),
+ };
+
+ self.machine_st
+ .bind(self.deref_register(5).as_var().unwrap(), digest_cell);
+
Ok(())
}
self.0
}
}
+
+#[derive(Debug)]
+struct GeminiClientVerifier;
+
+impl ClientCertVerifier for GeminiClientVerifier {
+ // 1. Indica al servidor que solicite el certificado de cliente
+ fn offer_client_auth(&self) -> bool {
+ true
+ }
+
+ // 2. Crucial para Gemini: No hace obligatorio el certificado (permite anónimos)
+ fn client_auth_mandatory(&self) -> bool {
+ false
+ }
+
+ // 3. No enviamos ninguna pista de CA al cliente (le sirve cualquier certificado)
+ fn root_hint_subjects(&self) -> &[DistinguishedName] {
+ &[]
+ }
+
+ // 4. Aceptamos el certificado tal cual venga, sin validar firmas ni caducidad
+ fn verify_client_cert(
+ &self,
+ _end_entity: &CertificateDer<'_>,
+ _intermediates: &[CertificateDer<'_>],
+ _now: rustls::pki_types::UnixTime,
+ ) -> Result<ClientCertVerified, rustls::Error> {
+ // En Gemini, confiamos en el certificado "as-is" (TOFU)
+ Ok(ClientCertVerified::assertion())
+ }
+
+ // Requerido por el trait para saber qué algoritmos soporta el verificador
+ fn verify_tls12_signature(
+ &self,
+ _message: &[u8],
+ _cert: &CertificateDer<'_>,
+ _dss: &rustls::DigitallySignedStruct,
+ ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
+ Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
+ }
+
+ fn verify_tls13_signature(
+ &self,
+ _message: &[u8],
+ _cert: &CertificateDer<'_>,
+ _dss: &rustls::DigitallySignedStruct,
+ ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
+ Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
+ }
+
+ fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
+ // Obtenemos el proveedor criptográfico por defecto (AWS-LC)
+ let provider = rustls::crypto::aws_lc_rs::default_provider();
+
+ // Devolvemos todos los esquemas de firma que el proveedor sabe manejar
+ provider
+ .signature_verification_algorithms
+ .supported_schemes()
+ }
+}