Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions upki-ffi/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
5 changes: 5 additions & 0 deletions upki-ffi/upki.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#ifndef UPKI_H
#define UPKI_H

#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
Expand Down Expand Up @@ -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 */
22 changes: 22 additions & 0 deletions upki-openssl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions upki-openssl/Makefile
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 16 additions & 0 deletions upki-openssl/build.rs
Original file line number Diff line number Diff line change
@@ -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"));
}
25 changes: 25 additions & 0 deletions upki-openssl/preload.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include "upki-openssl.h"
#include <dlfcn.h>
#include <openssl/ssl.h>

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
150 changes: 150 additions & 0 deletions upki-openssl/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<RevocationStatus, Error> {
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<Self> {
// SAFETY: we pass up the requirements of `ptr::as_mut()` to our caller
unsafe { ptr.as_mut() }.map(Self)
}

fn chain(&self) -> Option<BorrowedX509Stack<'a>> {
// 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<Vec<CertificateDer<'static>>> {
// 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()
}
25 changes: 25 additions & 0 deletions upki-openssl/upki-openssl.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#ifndef UPKI_OPENSSL_H
#define UPKI_OPENSSL_H

#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <openssl/x509_vfy.h>

/**
* 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 */
Loading