Some banks and payment providers require the merchant to identify itself with its own certificate on top of an API key: the server asks for the certificate during the handshake and cuts the connection if none it accepts arrives. HttpOpts presents it PER REQUEST with http_opts_client_cert(opts, cert_pem, key_pem).
// Nyx by Example: HTTPS con certificado de cliente (TLS mutuo). // // Algunos bancos y pasarelas de pago exigen que el comercio se identifique con // un certificado propio ademas de su clave de API: el servidor pide el // certificado durante el handshake y corta la conexion si no llega uno que // acepte. HttpOpts lo presenta POR PETICION: // - http_opts_client_cert(opts, cert_pem, key_pem): certificado y clave // privada como CONTENIDO PEM (no rutas: se traen con read_file o // include_bytes). El certificado va primero; si hay intermedios, a // continuacion en el mismo texto. La clave no puede tener contrasena. // - Se validan ANTES de conectar: uno sin el otro, PEM ilegible o una clave que // no corresponde al certificado dan Err invalid 22 sin abrir el socket. // - Solo vale para esa peticion: el resto del proceso no presenta nada. // - Se combina con cas_extra cuando el banco usa una CA privada: // o.cas_extra = read_file("certs/ca-banco.pem") // // La receta corre sin red: muestra la validacion previa (los errores salen sin // conectar, aun contra un puerto cerrado). import "std/http" import "std/error" fn consultar_saldo(url: String, cert_pem: String, key_pem: String) -> Result<Array, Error> { let o: HttpOpts = http_opts_client_cert(http_opts(), cert_pem, key_pem) return try_http_request_opts("GET", url, [], "", o) } fn main() { // En un programa real: // let cert: String = read_file("certs/cliente.crt") // let key: String = read_file("certs/cliente.key") match consultar_saldo("https://127.0.0.1:1/saldo", "esto no es un certificado", "ni una clave") { Result.Ok(r) => { print("inesperado: " + int_to_string(http_status(r))) } Result.Err(e) => { print("rechazado antes de conectar: " + e.kind + " " + int_to_string(e.code)) assert(e.kind == "invalid" and e.code == 22, "un par ilegible se dice, no se ignora") } } // Un certificado sin su clave tampoco se manda a medias. match consultar_saldo("https://127.0.0.1:1/saldo", "-----BEGIN CERTIFICATE-----", "") { Result.Ok(r) => { print("inesperado: " + int_to_string(http_status(r))) } Result.Err(e) => { assert(e.kind == "invalid", "certificado sin clave") } } }
rechazado antes de conectar: invalid 22
How it works
Certificate and private key are PEM CONTENT, not paths — bring them with read_file or include_bytes. The certificate goes first, with any intermediates after it in the same text, and the key cannot have a passphrase. Everything is validated BEFORE connecting: one without the other, an unreadable PEM, or a key that does not match the certificate give Err invalid 22 without opening the socket. That is what the recipe shows, even against a closed port.
It only applies to that request; the rest of the process presents nothing. When the bank uses a private CA it combines with cas_extra (recipe 122): o.cas_extra = read_file("certs/ca-banco.pem"). The recipe needs no network, so the output is the early rejection and nothing more.