diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b47dfc7..3d9219db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,12 +51,21 @@ jobs: with: toolchain: ${{ matrix.rustc }} + - name: Install OpenSSL (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append + vcpkg install openssl:x64-windows-static-md + - name: Build (debug) run: cargo build --locked - name: Run tests (debug) run: cargo test --locked - name: Check FFI header - run: git diff --exit-code -- upki-ffi/upki.h + run: | + git diff --exit-code -- upki-ffi/upki.h + git diff --exit-code -- upki-openssl/upki-openssl.h - name: Build (release) run: cargo build --locked --release diff --git a/Cargo.lock b/Cargo.lock index 651c4659..4d7b62b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1551,6 +1551,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -1579,6 +1591,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "plotters" version = "0.3.7" @@ -2704,6 +2722,16 @@ dependencies = [ "upki", ] +[[package]] +name = "upki-openssl" +version = "0.1.0" +dependencies = [ + "cbindgen", + "openssl-sys", + "rustls-pki-types", + "upki", +] + [[package]] name = "url" version = "2.5.8" @@ -2734,6 +2762,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 4c03e85f..277f2c47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["upki", "upki-mirror", "revoke-test", "rustls-upki", "upki-ffi"] +members = ["upki", "upki-mirror", "revoke-test", "rustls-upki", "upki-ffi", "upki-openssl"] resolver = "3" [workspace.package] @@ -21,6 +21,7 @@ hex = { version = "0.4", features = ["serde"] } http = "1" insta = { version = "1.44.3", features = ["filters"] } insta-cmd = "0.6.0" +openssl-sys = "0" reqwest = { version = "0.13", default-features = false, features = ["charset", "default-tls", "h2", "http2", "json"] } rand = "0.10" regex = "1.12" diff --git a/upki-ffi/build.rs b/upki-ffi/build.rs index 1af92aea..cc9790a6 100644 --- a/upki-ffi/build.rs +++ b/upki-ffi/build.rs @@ -8,6 +8,7 @@ fn main() { cbindgen::Builder::new() .with_crate(&crate_dir) .with_language(Language::C) + .with_include_guard("UPKI_H") .generate() .expect("unable to generate bindings") .write_to_file(crate_dir.join("upki.h")); diff --git a/upki-ffi/upki.h b/upki-ffi/upki.h index aff223b4..58df0f71 100644 --- a/upki-ffi/upki.h +++ b/upki-ffi/upki.h @@ -1,3 +1,6 @@ +#ifndef UPKI_H +#define UPKI_H + #include #include #include @@ -207,3 +210,5 @@ enum upki_result upki_config_new(struct upki_config **out); * or null (in which case this is a no-op). */ void upki_config_free(struct upki_config *config); + +#endif /* UPKI_H */ diff --git a/upki-openssl/Cargo.toml b/upki-openssl/Cargo.toml new file mode 100644 index 00000000..1dd9de22 --- /dev/null +++ b/upki-openssl/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "upki-openssl" +version = "0.1.0" +license.workspace = true +rust-version.workspace = true +edition.workspace = true +repository.workspace = true + +[lib] +name = "upkiopenssl" +crate-type = ["cdylib"] + +[dependencies] +openssl-sys = { workspace = true } +rustls-pki-types.workspace = true +upki = { path = "../upki", version = "0.2.0" } + +[build-dependencies] +cbindgen.workspace = true + +[lints] +workspace = true diff --git a/upki-openssl/Makefile b/upki-openssl/Makefile new file mode 100644 index 00000000..affa7009 --- /dev/null +++ b/upki-openssl/Makefile @@ -0,0 +1,23 @@ +CC = gcc +CFLAGS = -Wall -Wextra -fPIC +LDFLAGS = -shared +LIBS = -lssl -lcrypto -ldl -lupkiopenssl -L../target/release/ + +TARGET = libupkiopenssl-preload.so +SOURCE = preload.c + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(SOURCE) upki-openssl.h + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(SOURCE) $(LIBS) + +clean: + rm -f $(TARGET) + +install: $(TARGET) + install -m 755 $(TARGET) /usr/local/lib/ + +uninstall: + rm -f /usr/local/lib/$(TARGET) diff --git a/upki-openssl/build.rs b/upki-openssl/build.rs new file mode 100644 index 00000000..8b689d10 --- /dev/null +++ b/upki-openssl/build.rs @@ -0,0 +1,16 @@ +use std::env; +use std::path::PathBuf; + +use cbindgen::Language; + +fn main() { + let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + cbindgen::Builder::new() + .with_crate(&crate_dir) + .with_language(Language::C) + .with_sys_include("openssl/x509_vfy.h") + .with_include_guard("UPKI_OPENSSL_H") + .generate() + .expect("unable to generate bindings") + .write_to_file(crate_dir.join("upki-openssl.h")); +} diff --git a/upki-openssl/preload.c b/upki-openssl/preload.c new file mode 100644 index 00000000..1157f1dd --- /dev/null +++ b/upki-openssl/preload.c @@ -0,0 +1,25 @@ +#include "upki-openssl.h" +#include +#include + +typedef SSL *(*ssl_new_fn)(SSL_CTX *); + +SSL *SSL_new(SSL_CTX *ctx) { + void *parent = dlsym(RTLD_NEXT, "SSL_new"); + if (!parent) { + return NULL; + } + + SSL *new = ((ssl_new_fn)(parent))(ctx); + if (!new) { + return new; + } + + // TODO: save and call current too. + // SSL_verify_cb current = SSL_get_verify_callback(new); + int mode = SSL_get_verify_mode(new); + SSL_set_verify(new, mode, upki_openssl_verify_callback); + return new; +} + +// TODO: also hook later calls of SSL_set_verify, SSL_get_verify_callback diff --git a/upki-openssl/src/lib.rs b/upki-openssl/src/lib.rs new file mode 100644 index 00000000..3501528b --- /dev/null +++ b/upki-openssl/src/lib.rs @@ -0,0 +1,150 @@ +#![warn(clippy::undocumented_unsafe_blocks)] + +use core::ptr; +use std::os::raw::c_int; +use std::slice; + +use openssl_sys::{ + OPENSSL_free, OPENSSL_sk_num, OPENSSL_sk_value, X509, X509_STORE_CTX, + X509_STORE_CTX_get0_chain, X509_STORE_CTX_set_error, X509_V_ERR_APPLICATION_VERIFICATION, + X509_V_ERR_CERT_REVOKED, i2d_X509, stack_st_X509, +}; +use rustls_pki_types::CertificateDer; +use upki::Error; +use upki::revocation::{Manifest, RevocationCheckInput, RevocationStatus}; + +/// This is a function matching OpenSSL's `SSL_verify_cb` type which does +/// revocation checking using upki. +/// +/// The configuration file and data location is found automatically. +/// +/// # Safety +/// This function is called by OpenSSL typically, and its correct operation +/// hinges almost entirely on being called properly. For example, that +/// `x509_ctx` is a valid pointer, or NULL. +/// +/// On unexpected/unrecoverable errors, this function returns 0. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn upki_openssl_verify_callback( + mut preverify_ok: c_int, + x509_ctx: *mut X509_STORE_CTX, +) -> c_int { + // Revocation checking never improves the situation if the verification has failed. + if preverify_ok == 0 { + return preverify_ok; + } + + // SAFETY: via essential and established principles of the C type system, we rely on + // OpenSSL to call this function with a `x509_ctx` that points to a valid value, or + // exceptionally is NULL. + let Some(mut x509_ctx) = (unsafe { BorrowedX509StoreCtx::from_ptr(x509_ctx) }) else { + return 0; + }; + + let Some(chain) = x509_ctx.chain() else { + return 0; + }; + + let Some(certs) = chain.copy_certs() else { + return 0; + }; + + match revocation_check(&certs) { + Ok(RevocationStatus::CertainlyRevoked) => { + x509_ctx.set_error(X509_V_ERR_CERT_REVOKED); + preverify_ok = 0; + } + Ok(RevocationStatus::NotCoveredByRevocationData | RevocationStatus::NotRevoked) => {} + Err(_e) => { + x509_ctx.set_error(X509_V_ERR_APPLICATION_VERIFICATION); + preverify_ok = 0; + } + } + + preverify_ok +} + +fn revocation_check(certs: &[CertificateDer<'_>]) -> Result { + let path = upki::ConfigPath::new(None)?; + let config = upki::Config::from_file_or_default(&path)?; + let manifest = Manifest::from_config(&config)?; + let input = RevocationCheckInput::from_certificates(certs)?; + match manifest.check(&input, &config) { + Ok(st) => Ok(st), + Err(e) => Err(Error::Revocation(e)), + } +} + +struct BorrowedX509StoreCtx<'a>(&'a mut X509_STORE_CTX); + +impl<'a> BorrowedX509StoreCtx<'a> { + unsafe fn from_ptr(ptr: *mut X509_STORE_CTX) -> Option { + // SAFETY: we pass up the requirements of `ptr::as_mut()` to our caller + unsafe { ptr.as_mut() }.map(Self) + } + + fn chain(&self) -> Option> { + // SAFETY: X509_STORE_CTX_get0_chain has no published documentation saying when it is + // safe to call. This type guarantees that the pointer is of the correct type, alignment, etc, + // and is non-NULL. + let chain = unsafe { X509_STORE_CTX_get0_chain(ptr::from_ref(self.0)) }; + + // SAFETY: we require that openssl correctly returns a valid pointer, or NULL. + unsafe { chain.as_ref() }.map(BorrowedX509Stack) + } + + fn set_error(&mut self, err: i32) { + // SAFETY: the input pointer is valid, because it comes from our reference. + // OpenSSL does not document any other preconditions. + unsafe { X509_STORE_CTX_set_error(ptr::from_mut(self.0), err) }; + } +} + +struct BorrowedX509Stack<'a>(&'a stack_st_X509); + +impl<'a> BorrowedX509Stack<'a> { + fn copy_certs(&self) -> Option>> { + // SAFETY: the stack pointer is valid, thanks to it being from a reference. + let count = unsafe { OPENSSL_sk_num(ptr::from_ref(self.0).cast()) }; + if count < 0 { + return None; + } + + let mut certs = vec![]; + for i in 0..count { + // SAFETY: the stack pointer is valid, thanks to it being from a reference. `OPENSSL_sk_value` returns + // a valid pointer to the item or NULL. + let x509: *const X509 = + unsafe { OPENSSL_sk_value(ptr::from_ref(self.0).cast(), i).cast() }; + + // SAFETY: we require OpenSSL only fills the stack with valid pointers to X509 objects (or NULL) + let x509 = unsafe { x509.as_ref() }?; + certs.push(x509_to_certificate_der(x509)); + } + + Some(certs) + } +} + +fn x509_to_certificate_der(x509: &'_ X509) -> CertificateDer<'static> { + // SAFETY: the x509 pointer is valid, thanks to it coming from a reference. + let (ptr, len) = unsafe { + let mut ptr = ptr::null_mut(); + let len = i2d_X509(ptr::from_ref(x509), &mut ptr); + (ptr, len) + }; + + if len <= 0 { + return vec![].into(); + } + let len = len as usize; + + let mut v = Vec::with_capacity(len); + // SAFETY: we rely on i2d_X509 allocating `ptr` correctly and signalling an error via negative `len` if not. + // `ptr` must be an allocated pointer. + unsafe { + v.extend_from_slice(slice::from_raw_parts(ptr, len)); + OPENSSL_free(ptr as *mut _); + } + v.into() +} diff --git a/upki-openssl/upki-openssl.h b/upki-openssl/upki-openssl.h new file mode 100644 index 00000000..46c90d5a --- /dev/null +++ b/upki-openssl/upki-openssl.h @@ -0,0 +1,25 @@ +#ifndef UPKI_OPENSSL_H +#define UPKI_OPENSSL_H + +#include +#include +#include +#include +#include + +/** + * This is a function matching OpenSSL's `SSL_verify_cb` type which does + * revocation checking using upki. + * + * The configuration file and data location is found automatically. + * + * # Safety + * This function is called by OpenSSL typically, and its correct operation + * hinges almost entirely on being called properly. For example, that + * `x509_ctx` is a valid pointer, or NULL. + * + * On unexpected/unrecoverable errors, this function returns 0. + */ +int upki_openssl_verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx); + +#endif /* UPKI_OPENSSL_H */ diff --git a/upki/src/lib.rs b/upki/src/lib.rs index a1d22979..48f3c9e4 100644 --- a/upki/src/lib.rs +++ b/upki/src/lib.rs @@ -194,6 +194,12 @@ impl fmt::Display for Error { } } +impl From for Error { + fn from(value: revocation::Error) -> Self { + Self::Revocation(value) + } +} + const PREFIX: &str = "upki"; const CONFIG_FILE: &str = "config.toml";