Skip to content

Commit 35d1c1a

Browse files
committed
Provide OpenSSL integration
1 parent 38949d4 commit 35d1c1a

8 files changed

Lines changed: 467 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,21 @@ jobs:
5151
with:
5252
toolchain: ${{ matrix.rustc }}
5353

54+
- name: Install OpenSSL (Windows)
55+
if: runner.os == 'Windows'
56+
shell: powershell
57+
run: |
58+
echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
59+
vcpkg install openssl:x64-windows-static-md
60+
5461
- name: Build (debug)
5562
run: cargo build --locked
5663
- name: Run tests (debug)
5764
run: cargo test --locked
5865
- name: Check FFI header
59-
run: git diff --exit-code -- upki-ffi/upki.h
66+
run: |
67+
git diff --exit-code -- upki-ffi/upki.h
68+
git diff --exit-code -- upki-openssl/upki-openssl.h
6069
6170
- name: Build (release)
6271
run: cargo build --locked --release

Cargo.lock

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["upki", "upki-cli", "upki-mirror", "revoke-test", "rustls-upki", "upki-ffi"]
2+
members = ["upki", "upki-cli", "upki-mirror", "revoke-test", "rustls-upki", "upki-ffi", "upki-openssl"]
33
resolver = "3"
44

55
[workspace.package]
@@ -22,6 +22,7 @@ hex = { version = "0.4", features = ["serde"] }
2222
http = "1"
2323
insta = { version = "1.44.3", features = ["filters"] }
2424
insta-cmd = "0.6.0"
25+
openssl-sys = "0"
2526
reqwest = { version = "0.13", default-features = false, features = ["charset", "default-tls", "h2", "http2", "json"] }
2627
rand = "0.10"
2728
regex = "1.12"

upki-openssl/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "upki-openssl"
3+
version = "0.1.0"
4+
license.workspace = true
5+
rust-version.workspace = true
6+
edition.workspace = true
7+
repository.workspace = true
8+
9+
[lib]
10+
name = "upkiopenssl"
11+
crate-type = ["cdylib"]
12+
13+
[dependencies]
14+
openssl-sys = { workspace = true }
15+
rustls-pki-types.workspace = true
16+
upki = { path = "../upki", version = "0.2.0" }
17+
18+
[build-dependencies]
19+
cbindgen.workspace = true
20+
21+
[lints]
22+
workspace = true

upki-openssl/build.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use std::env;
2+
use std::path::PathBuf;
3+
4+
use cbindgen::Language;
5+
6+
fn main() {
7+
let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
8+
cbindgen::Builder::new()
9+
.with_crate(&crate_dir)
10+
.with_language(Language::C)
11+
.with_sys_include("openssl/x509_vfy.h")
12+
.with_include_guard("UPKI_OPENSSL_H")
13+
.generate()
14+
.expect("unable to generate bindings")
15+
.write_to_file(crate_dir.join("upki-openssl.h"));
16+
}

upki-openssl/src/lib.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#![warn(clippy::undocumented_unsafe_blocks)]
2+
3+
use core::ptr;
4+
use std::os::raw::c_int;
5+
use std::slice;
6+
7+
use openssl_sys::{
8+
OPENSSL_free, OPENSSL_sk_num, OPENSSL_sk_value, X509, X509_STORE_CTX,
9+
X509_STORE_CTX_get_error_depth, X509_STORE_CTX_get0_chain, X509_STORE_CTX_set_error,
10+
X509_V_ERR_APPLICATION_VERIFICATION, X509_V_ERR_CERT_REVOKED, i2d_X509, stack_st_X509,
11+
};
12+
use rustls_pki_types::CertificateDer;
13+
use upki::revocation::{Index, RevocationCheckInput, RevocationStatus};
14+
use upki::{Config, ConfigPath, Error};
15+
16+
/// This is a function matching OpenSSL's `SSL_verify_cb` type which does
17+
/// revocation checking using upki.
18+
///
19+
/// The configuration file and data location is found automatically.
20+
///
21+
/// # Errors
22+
/// This function returns 0 if called with 0 for the `preverify_ok` parameter.
23+
/// As a result, it never allows a verification to pass if the previous verification
24+
/// step has failed.
25+
///
26+
/// If the certificate chain obtained from `x509_ctx` is revoked, this function returns 0
27+
/// and sets the `X509_V_ERR_CERT_REVOKED` error on `x509_ctx` (using
28+
/// `X509_STORE_CTX_set_error(3SSL)`).
29+
///
30+
/// If the certificate chain obtained from `x509_ctx` is not included in the revocation data,
31+
/// this function returns `preverify_ok`.
32+
///
33+
/// If the revocation status cannot be determined, this function returns 0 and sets
34+
/// the `X509_V_ERR_APPLICATION_VERIFICATION` error on `x509_ctx` (using
35+
/// `X509_STORE_CTX_set_error(3SSL)`).
36+
///
37+
/// On unexpected/unrecoverable errors, this function returns 0.
38+
///
39+
/// # Safety
40+
/// This function is called by OpenSSL typically, and its correct operation
41+
/// hinges almost entirely on being called properly. For example, that
42+
/// `x509_ctx` is a valid pointer, or NULL.
43+
#[unsafe(no_mangle)]
44+
pub unsafe extern "C" fn upki_openssl_verify_callback(
45+
mut preverify_ok: c_int,
46+
x509_ctx: *mut X509_STORE_CTX,
47+
) -> c_int {
48+
// Revocation checking never improves the situation if the verification has failed.
49+
if preverify_ok == 0 {
50+
return preverify_ok;
51+
}
52+
53+
// SAFETY: via essential and established principles of the C type system, we rely on
54+
// OpenSSL to call this function with a `x509_ctx` that points to a valid value, or
55+
// exceptionally is NULL.
56+
let Some(mut x509_ctx) = (unsafe { BorrowedX509StoreCtx::from_ptr(x509_ctx) }) else {
57+
return 0;
58+
};
59+
60+
// This callback is called once per certificate, with the final call being for the
61+
// leaf certificate denoted by error_depth = 0. We only process the chain as a whole;
62+
// do this at the leaf certificate level.
63+
if x509_ctx.error_depth() != 0 {
64+
return preverify_ok;
65+
}
66+
67+
let Some(chain) = x509_ctx.chain() else {
68+
return 0;
69+
};
70+
71+
let Some(certs) = chain.copy_certs() else {
72+
return 0;
73+
};
74+
75+
match revocation_check(&certs) {
76+
Ok(RevocationStatus::CertainlyRevoked) => {
77+
x509_ctx.set_error(X509_V_ERR_CERT_REVOKED);
78+
preverify_ok = 0;
79+
}
80+
Ok(RevocationStatus::NotCoveredByRevocationData | RevocationStatus::NotRevoked) => {}
81+
Err(_e) => {
82+
x509_ctx.set_error(X509_V_ERR_APPLICATION_VERIFICATION);
83+
preverify_ok = 0;
84+
}
85+
}
86+
87+
preverify_ok
88+
}
89+
90+
fn revocation_check(certs: &[CertificateDer<'_>]) -> Result<RevocationStatus, Error> {
91+
let path = ConfigPath::new(None)?;
92+
let config = Config::from_file_or_user_default(&path)?;
93+
let mut index = Index::from_cache(&config)?;
94+
let input = RevocationCheckInput::from_certificates(certs)?;
95+
match index.check(&input) {
96+
Ok(st) => Ok(st),
97+
Err(e) => Err(Error::Revocation(e)),
98+
}
99+
}
100+
101+
struct BorrowedX509StoreCtx<'a>(&'a mut X509_STORE_CTX);
102+
103+
impl<'a> BorrowedX509StoreCtx<'a> {
104+
unsafe fn from_ptr(ptr: *mut X509_STORE_CTX) -> Option<Self> {
105+
// SAFETY: we pass up the requirements of `ptr::as_mut()` to our caller
106+
unsafe { ptr.as_mut() }.map(Self)
107+
}
108+
109+
fn error_depth(&self) -> c_int {
110+
// SAFETY: the input pointer is valid, because it comes from our reference.
111+
unsafe { X509_STORE_CTX_get_error_depth(ptr::from_ref(self.0)) }
112+
}
113+
114+
fn chain(&self) -> Option<BorrowedX509Stack<'a>> {
115+
// SAFETY: X509_STORE_CTX_get0_chain has no published documentation saying when it is
116+
// safe to call. This type guarantees that the pointer is of the correct type, alignment, etc,
117+
// and is non-NULL.
118+
let chain = unsafe { X509_STORE_CTX_get0_chain(ptr::from_ref(self.0)) };
119+
120+
// SAFETY: we require that openssl correctly returns a valid pointer, or NULL.
121+
unsafe { chain.as_ref() }.map(BorrowedX509Stack)
122+
}
123+
124+
fn set_error(&mut self, err: i32) {
125+
// SAFETY: the input pointer is valid, because it comes from our reference.
126+
// OpenSSL does not document any other preconditions.
127+
unsafe { X509_STORE_CTX_set_error(ptr::from_mut(self.0), err) };
128+
}
129+
}
130+
131+
struct BorrowedX509Stack<'a>(&'a stack_st_X509);
132+
133+
impl<'a> BorrowedX509Stack<'a> {
134+
fn copy_certs(&self) -> Option<Vec<CertificateDer<'static>>> {
135+
// SAFETY: the stack pointer is valid, thanks to it being from a reference.
136+
let count = unsafe { OPENSSL_sk_num(ptr::from_ref(self.0).cast()) };
137+
if count < 0 {
138+
return None;
139+
}
140+
141+
let mut certs = vec![];
142+
for i in 0..count {
143+
// SAFETY: the stack pointer is valid, thanks to it being from a reference. `OPENSSL_sk_value` returns
144+
// a valid pointer to the item or NULL.
145+
let x509: *const X509 =
146+
unsafe { OPENSSL_sk_value(ptr::from_ref(self.0).cast(), i).cast() };
147+
148+
// SAFETY: we require OpenSSL only fills the stack with valid pointers to X509 objects (or NULL)
149+
let x509 = unsafe { x509.as_ref() }?;
150+
certs.push(x509_to_certificate_der(x509)?);
151+
}
152+
153+
Some(certs)
154+
}
155+
}
156+
157+
fn x509_to_certificate_der(x509: &'_ X509) -> Option<CertificateDer<'static>> {
158+
// SAFETY: the x509 pointer is valid, thanks to it coming from a reference.
159+
let (ptr, len) = unsafe {
160+
let mut ptr = ptr::null_mut();
161+
let len = i2d_X509(ptr::from_ref(x509), &mut ptr);
162+
(ptr, len)
163+
};
164+
165+
if len <= 0 || ptr.is_null() {
166+
return None;
167+
}
168+
let len = len as usize;
169+
170+
let mut v = Vec::with_capacity(len);
171+
// SAFETY: we rely on i2d_X509 allocating `ptr` correctly and signalling an error via negative `len` if not.
172+
// `ptr` must be an allocated pointer.
173+
unsafe {
174+
v.extend_from_slice(slice::from_raw_parts(ptr, len));
175+
OPENSSL_free(ptr as *mut _);
176+
}
177+
Some(v.into())
178+
}
179+
180+
#[cfg(test)]
181+
mod test;

0 commit comments

Comments
 (0)