Skip to content

Commit c2fb3b4

Browse files
committed
Provide OpenSSL integration
1 parent 3ae35aa commit c2fb3b4

7 files changed

Lines changed: 252 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ jobs:
5656
- name: Run tests (debug)
5757
run: cargo test --locked
5858
- name: Check FFI header
59-
run: git diff --exit-code -- upki-ffi/upki.h
59+
run: |
60+
git diff --exit-code -- upki-ffi/upki.h
61+
git diff --exit-code -- upki-openssl/upki-openssl.h
6062
6163
- name: Build (release)
6264
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-mirror", "revoke-test", "rustls-upki", "upki-ffi"]
2+
members = ["upki", "upki-mirror", "revoke-test", "rustls-upki", "upki-ffi", "upki-openssl"]
33
resolver = "3"
44

55
[workspace.package]
@@ -21,6 +21,7 @@ hex = { version = "0.4", features = ["serde"] }
2121
http = "1"
2222
insta = { version = "1.44.3", features = ["filters"] }
2323
insta-cmd = "0.6.0"
24+
openssl-sys = "0"
2425
reqwest = { version = "0.13", default-features = false, features = ["charset", "default-tls", "h2", "http2", "json"] }
2526
rand = "0.10"
2627
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: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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_get0_chain, X509_STORE_CTX_set_error, X509_V_ERR_APPLICATION_VERIFICATION,
10+
X509_V_ERR_CERT_REVOKED, i2d_X509, stack_st_X509,
11+
};
12+
use rustls_pki_types::CertificateDer;
13+
use upki::Error;
14+
use upki::revocation::{Manifest, RevocationCheckInput, RevocationStatus};
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+
/// # Safety
22+
/// This function is called by OpenSSL typically, and its correct operation
23+
/// hinges almost entirely on being called properly. For example, that
24+
/// `x509_ctx` is a valid pointer, or NULL.
25+
///
26+
/// On unexpected/unrecoverable errors, this function returns 0.
27+
#[unsafe(no_mangle)]
28+
pub unsafe extern "C" fn upki_openssl_verify_callback(
29+
mut preverify_ok: c_int,
30+
x509_ctx: *mut X509_STORE_CTX,
31+
) -> c_int {
32+
// Revocation checking never improves the situation if the verification has failed.
33+
if preverify_ok == 0 {
34+
return preverify_ok;
35+
}
36+
37+
// SAFETY: via essential and established principles of the C type system, we rely on
38+
// OpenSSL to call this function with a `x509_ctx` that points to a valid value, or
39+
// exceptionally is NULL.
40+
let Some(mut x509_ctx) = (unsafe { BorrowedX509StoreCtx::from_ptr(x509_ctx) }) else {
41+
return 0;
42+
};
43+
44+
let Some(chain) = x509_ctx.chain() else {
45+
return 0;
46+
};
47+
48+
let Some(certs) = chain.copy_certs() else {
49+
return 0;
50+
};
51+
52+
match revocation_check(&certs) {
53+
Ok(RevocationStatus::CertainlyRevoked) => {
54+
x509_ctx.set_error(X509_V_ERR_CERT_REVOKED);
55+
preverify_ok = 0;
56+
}
57+
Ok(RevocationStatus::NotCoveredByRevocationData | RevocationStatus::NotRevoked) => {}
58+
Err(_e) => {
59+
x509_ctx.set_error(X509_V_ERR_APPLICATION_VERIFICATION);
60+
preverify_ok = 0;
61+
}
62+
}
63+
64+
preverify_ok
65+
}
66+
67+
fn revocation_check(certs: &[CertificateDer<'_>]) -> Result<RevocationStatus, Error> {
68+
let path = upki::ConfigPath::new(None)?;
69+
let config = upki::Config::from_file_or_default(&path)?;
70+
let manifest = Manifest::from_config(&config)?;
71+
let input = RevocationCheckInput::from_certificates(certs)?;
72+
match manifest.check(&input, &config) {
73+
Ok(st) => Ok(st),
74+
Err(e) => Err(Error::Revocation(e)),
75+
}
76+
}
77+
78+
struct BorrowedX509StoreCtx<'a>(&'a mut X509_STORE_CTX);
79+
80+
impl<'a> BorrowedX509StoreCtx<'a> {
81+
unsafe fn from_ptr(ptr: *mut X509_STORE_CTX) -> Option<Self> {
82+
// SAFETY: we pass up the requirements of `ptr::as_mut()` to our caller
83+
unsafe { ptr.as_mut() }.map(Self)
84+
}
85+
86+
fn chain(&self) -> Option<BorrowedX509Stack<'a>> {
87+
// SAFETY: X509_STORE_CTX_get0_chain has no published documentation saying when it is
88+
// safe to call. This type guarantees that the pointer is of the correct type, alignment, etc,
89+
// and is non-NULL.
90+
let chain = unsafe { X509_STORE_CTX_get0_chain(ptr::from_ref(self.0)) };
91+
92+
// SAFETY: we require that openssl correctly returns a valid pointer, or NULL.
93+
unsafe { chain.as_ref() }.map(BorrowedX509Stack)
94+
}
95+
96+
fn set_error(&mut self, err: i32) {
97+
// SAFETY: the input pointer is valid, because it comes from our reference.
98+
// OpenSSL does not document any other preconditions.
99+
unsafe { X509_STORE_CTX_set_error(ptr::from_mut(self.0), err) };
100+
}
101+
}
102+
103+
struct BorrowedX509Stack<'a>(&'a stack_st_X509);
104+
105+
impl<'a> BorrowedX509Stack<'a> {
106+
fn copy_certs(&self) -> Option<Vec<CertificateDer<'static>>> {
107+
// SAFETY: the stack pointer is valid, thanks to it being from a reference.
108+
let count = unsafe { OPENSSL_sk_num(ptr::from_ref(self.0).cast()) };
109+
if count < 0 {
110+
return None;
111+
}
112+
113+
let mut certs = vec![];
114+
for i in 0..count {
115+
// SAFETY: the stack pointer is valid, thanks to it being from a reference. `OPENSSL_sk_value` returns
116+
// a valid pointer to the item or NULL.
117+
let x509: *const X509 =
118+
unsafe { OPENSSL_sk_value(ptr::from_ref(self.0).cast(), i).cast() };
119+
120+
// SAFETY: we require OpenSSL only fills the stack with valid pointers to X509 objects (or NULL)
121+
let x509 = unsafe { x509.as_ref() }?;
122+
certs.push(x509_to_certificate_der(x509));
123+
}
124+
125+
Some(certs)
126+
}
127+
}
128+
129+
fn x509_to_certificate_der(x509: &'_ X509) -> CertificateDer<'static> {
130+
// SAFETY: the x509 pointer is valid, thanks to it coming from a reference.
131+
let (ptr, len) = unsafe {
132+
let mut ptr = ptr::null_mut();
133+
let len = i2d_X509(ptr::from_ref(x509), &mut ptr);
134+
(ptr, len)
135+
};
136+
137+
if len <= 0 {
138+
return vec![].into();
139+
}
140+
let len = len as usize;
141+
142+
let mut v = Vec::with_capacity(len);
143+
// SAFETY: we rely on i2d_X509 allocating `ptr` correctly and signalling an error via negative `len` if not.
144+
// `ptr` must be an allocated pointer.
145+
unsafe {
146+
v.extend_from_slice(slice::from_raw_parts(ptr, len));
147+
OPENSSL_free(ptr as *mut _);
148+
}
149+
v.into()
150+
}

upki-openssl/upki-openssl.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#ifndef UPKI_OPENSSL_H
2+
#define UPKI_OPENSSL_H
3+
4+
#include <stdarg.h>
5+
#include <stdbool.h>
6+
#include <stdint.h>
7+
#include <stdlib.h>
8+
#include <openssl/x509_vfy.h>
9+
10+
/**
11+
* This is a function matching OpenSSL's `SSL_verify_cb` type which does
12+
* revocation checking using upki.
13+
*
14+
* The configuration file and data location is found automatically.
15+
*
16+
* # Safety
17+
* This function is called by OpenSSL typically, and its correct operation
18+
* hinges almost entirely on being called properly. For example, that
19+
* `x509_ctx` is a valid pointer, or NULL.
20+
*
21+
* On unexpected/unrecoverable errors, this function returns 0.
22+
*/
23+
int upki_openssl_verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx);
24+
25+
#endif /* UPKI_OPENSSL_H */

0 commit comments

Comments
 (0)