Skip to main content

upki/
lib.rs

1#![crate_type = "staticlib"]
2#![allow(non_camel_case_types)]
3
4use core::ffi::c_char;
5use std::ffi::CStr;
6use std::panic::catch_unwind;
7use std::path::Path;
8use std::slice;
9
10use rustls_pki_types::CertificateDer;
11use upki::revocation::{self, Manifest, RevocationCheckInput, RevocationStatus};
12use upki::{Config, Error};
13
14/// Check the revocation status of a certificate.
15///
16/// The `certificates` array should contain the end-entity certificate first,
17/// followed by any intermediate certificates needed to find the issuer.
18///
19/// Returns a `upki_result` indicating success (with revocation status) or an error.
20///
21/// # Safety
22///
23/// - `config` must be a valid pointer returned by `upki_config_new`.
24/// - `certificates` must point to `certificates_len` `upki_certificate` values.
25/// - Each `upki_certificate` must have a valid `data` pointer to `len` bytes.
26#[unsafe(no_mangle)]
27pub unsafe extern "C" fn upki_check_revocation(
28    config: *const upki_config,
29    certificates: *const upki_certificate_der,
30    certificates_len: usize,
31) -> upki_result {
32    catch_unwind(|| {
33        if config.is_null() || certificates.is_null() {
34            return upki_result::UPKI_ERR_NULL_POINTER;
35        }
36
37        let config = unsafe { &(*config).0 };
38        let certificates = unsafe { slice::from_raw_parts(certificates, certificates_len) };
39
40        let certs = certificates
41            .iter()
42            .map(|c| CertificateDer::from(unsafe { slice::from_raw_parts(c.data, c.len) }))
43            .collect::<Vec<_>>();
44
45        let input = match RevocationCheckInput::from_certificates(&certs) {
46            Ok(input) => input,
47            Err(err) => return Error::Revocation(err).into(),
48        };
49
50        let manifest = match Manifest::from_config(config) {
51            Ok(manifest) => manifest,
52            Err(err) => return Error::Revocation(err).into(),
53        };
54
55        match manifest.check(&input, config) {
56            Ok(status) => match status {
57                RevocationStatus::NotCoveredByRevocationData => {
58                    upki_result::UPKI_REVOCATION_NOT_COVERED
59                }
60                RevocationStatus::CertainlyRevoked => upki_result::UPKI_REVOCATION_REVOKED,
61                RevocationStatus::NotRevoked => upki_result::UPKI_REVOCATION_NOT_REVOKED,
62            },
63            Err(err) => Error::Revocation(err).into(),
64        }
65    })
66    .unwrap_or(upki_result::UPKI_ERR_PANICKED)
67}
68
69/// Opaque type representing a `upki::Config`.
70pub struct upki_config(Config);
71
72/// Create a new `upki_config` by loading it from the file at `path`.
73///
74/// On success, writes the config pointer to `out` and returns `UPKI_OK`.
75/// The caller is responsible for freeing the config with `upki_config_free`.
76///
77/// # Safety
78///
79/// - `out` must not be `NULL`.
80/// - `path` must be a valid pointer to a null-terminated UTF-8 string.
81#[unsafe(no_mangle)]
82pub unsafe extern "C" fn upki_config_from_file(
83    path: *const c_char,
84    out: *mut *mut upki_config,
85) -> upki_result {
86    catch_unwind(|| {
87        if path.is_null() || out.is_null() {
88            return upki_result::UPKI_ERR_NULL_POINTER;
89        }
90
91        let path = unsafe { CStr::from_ptr(path) };
92        let Ok(path) = path.to_str() else {
93            return upki_result::UPKI_ERR_CONFIG_PATH;
94        };
95
96        match Config::from_file(Path::new(path)) {
97            Ok(config) => {
98                unsafe { *out = Box::into_raw(Box::new(upki_config(config))) };
99                upki_result::UPKI_OK
100            }
101            Err(err) => err.into(),
102        }
103    })
104    .unwrap_or(upki_result::UPKI_ERR_PANICKED)
105}
106
107/// Create a new `upki_config` with default settings.
108///
109/// On success, writes the config pointer to `out` and returns `UPKI_OK`.
110/// The caller is responsible for freeing the config with `upki_config_free`.
111///
112/// # Safety
113///
114/// - `out` must not be `NULL`.
115#[unsafe(no_mangle)]
116pub unsafe extern "C" fn upki_config_new(out: *mut *mut upki_config) -> upki_result {
117    catch_unwind(|| {
118        if out.is_null() {
119            return upki_result::UPKI_ERR_NULL_POINTER;
120        }
121
122        match Config::try_default() {
123            Ok(config) => {
124                unsafe { *out = Box::into_raw(Box::new(upki_config(config))) };
125                upki_result::UPKI_OK
126            }
127            Err(err) => err.into(),
128        }
129    })
130    .unwrap_or(upki_result::UPKI_ERR_PANICKED)
131}
132
133/// Free a `upki_config` created by `upki_config_new`.
134///
135/// # Safety
136///
137/// `config` must be a valid pointer returned by `upki_config_new`,
138/// or null (in which case this is a no-op).
139#[unsafe(no_mangle)]
140pub unsafe extern "C" fn upki_config_free(config: *mut upki_config) {
141    if !config.is_null() {
142        drop(unsafe { Box::from_raw(config) });
143    }
144}
145
146/// A DER-encoded certificate.
147#[repr(C)]
148pub struct upki_certificate_der {
149    /// Pointer to the DER-encoded certificate data.
150    pub data: *const u8,
151    /// Length of the certificate data in bytes.
152    pub len: usize,
153}
154
155/// Result type for upki C API functions.
156///
157/// Values 0-15 indicate success (with specific status information).
158/// Values 16 and above indicate errors.
159#[repr(C)]
160pub enum upki_result {
161    /// Operation succeeded.
162    UPKI_OK = 0,
163    /// The certificate is not covered by the revocation data.
164    UPKI_REVOCATION_NOT_COVERED = 1,
165    /// The certificate has been revoked.
166    UPKI_REVOCATION_REVOKED = 2,
167    /// The certificate is not revoked.
168    UPKI_REVOCATION_NOT_REVOKED = 3,
169
170    /// A null pointer was passed where a valid pointer was required.
171    UPKI_ERR_NULL_POINTER = 16,
172    /// The config path is not valid UTF-8.
173    UPKI_ERR_CONFIG_PATH = 17,
174    /// An unknown error variant was added to the library.
175    UPKI_ERR_UNKNOWN = 18,
176    /// An unexpected panic occurred in the library.
177    UPKI_ERR_PANICKED = 19,
178
179    // Errors from upki::Error
180    /// Failed to decode configuration file.
181    UPKI_ERR_CONFIG_DECODE = 32,
182    /// Failed to read configuration file.
183    UPKI_ERR_CONFIG_READ = 33,
184    /// No cache directory could be found.
185    UPKI_ERR_NO_CACHE_DIR = 34,
186    /// No configuration directory could be found.
187    UPKI_ERR_NO_CONFIG_DIR = 35,
188    /// The user's home directory could not be determined.
189    UPKI_ERR_NO_HOME_DIR = 36,
190
191    // Errors from upki::revocation::Error
192    /// Failed to create a directory.
193    UPKI_ERR_REVOCATION_CREATE_DIR = 64,
194    /// Failed to write a file.
195    UPKI_ERR_REVOCATION_FILE_WRITE = 65,
196    /// Failed to decode a filter file.
197    UPKI_ERR_REVOCATION_FILTER_DECODE = 66,
198    /// Failed to read a filter file.
199    UPKI_ERR_REVOCATION_FILTER_READ = 67,
200    /// A downloaded file did not match the expected hash.
201    UPKI_ERR_REVOCATION_HASH_MISMATCH = 68,
202    /// Failed to fetch a file over HTTP.
203    UPKI_ERR_REVOCATION_HTTP_FETCH = 69,
204    /// Invalid base64 encoding.
205    UPKI_ERR_REVOCATION_INVALID_BASE64 = 70,
206    /// The end-entity certificate was invalid.
207    UPKI_ERR_REVOCATION_INVALID_END_ENTITY_CERT = 71,
208    /// An intermediate certificate was invalid.
209    UPKI_ERR_REVOCATION_INVALID_INTERMEDIATE_CERT = 72,
210    /// A base64-decoded value did not have the expected length.
211    UPKI_ERR_REVOCATION_INVALID_LENGTH = 73,
212    /// Invalid SCT encoding.
213    UPKI_ERR_REVOCATION_INVALID_SCT_ENCODING = 74,
214    /// An SCT in the end-entity certificate could not be parsed.
215    UPKI_ERR_REVOCATION_INVALID_SCT_IN_CERT = 75,
216    /// A timestamp could not be parsed.
217    UPKI_ERR_REVOCATION_INVALID_TIMESTAMP = 76,
218    /// Failed to decode a manifest file.
219    UPKI_ERR_REVOCATION_MANIFEST_DECODE = 77,
220    /// Failed to encode a manifest file.
221    UPKI_ERR_REVOCATION_MANIFEST_ENCODE = 78,
222    /// Failed to read a manifest file.
223    UPKI_ERR_REVOCATION_MANIFEST_READ = 79,
224    /// Failed to write a manifest file.
225    UPKI_ERR_REVOCATION_MANIFEST_WRITE = 80,
226    /// No issuer found for the end-entity certificate.
227    UPKI_ERR_REVOCATION_NO_ISSUER = 81,
228    /// Cache is outdated.
229    UPKI_ERR_REVOCATION_OUTDATED = 82,
230    /// Failed to remove a file.
231    UPKI_ERR_REVOCATION_REMOVE_FILE = 83,
232    /// Certificate chain must contain at least 2 certificates.
233    UPKI_ERR_REVOCATION_TOO_FEW_CERTS = 84,
234}
235
236impl From<Error> for upki_result {
237    fn from(err: Error) -> Self {
238        match err {
239            Error::ConfigError { .. } => Self::UPKI_ERR_CONFIG_DECODE,
240            Error::FileRead { .. } => Self::UPKI_ERR_CONFIG_READ,
241            Error::NoCacheDirectoryFound => Self::UPKI_ERR_NO_CACHE_DIR,
242            Error::NoConfigDirectoryFound => Self::UPKI_ERR_NO_CONFIG_DIR,
243            Error::NoValidHomeDirectory => Self::UPKI_ERR_NO_HOME_DIR,
244            Error::Revocation(revocation::Error::CreateDirectory { .. }) => {
245                Self::UPKI_ERR_REVOCATION_CREATE_DIR
246            }
247            Error::Revocation(revocation::Error::FileWrite { .. }) => {
248                Self::UPKI_ERR_REVOCATION_FILE_WRITE
249            }
250            Error::Revocation(revocation::Error::FilterDecode { .. }) => {
251                Self::UPKI_ERR_REVOCATION_FILTER_DECODE
252            }
253            Error::Revocation(revocation::Error::FilterRead { .. }) => {
254                Self::UPKI_ERR_REVOCATION_FILTER_READ
255            }
256            Error::Revocation(revocation::Error::HashMismatch(_)) => {
257                Self::UPKI_ERR_REVOCATION_HASH_MISMATCH
258            }
259            Error::Revocation(revocation::Error::HttpFetch { .. }) => {
260                Self::UPKI_ERR_REVOCATION_HTTP_FETCH
261            }
262            Error::Revocation(revocation::Error::InvalidBase64 { .. }) => {
263                Self::UPKI_ERR_REVOCATION_INVALID_BASE64
264            }
265            Error::Revocation(revocation::Error::InvalidEndEntityCertificate(_)) => {
266                Self::UPKI_ERR_REVOCATION_INVALID_END_ENTITY_CERT
267            }
268            Error::Revocation(revocation::Error::InvalidIntermediateCertificate { .. }) => {
269                Self::UPKI_ERR_REVOCATION_INVALID_INTERMEDIATE_CERT
270            }
271            Error::Revocation(revocation::Error::InvalidLength { .. }) => {
272                Self::UPKI_ERR_REVOCATION_INVALID_LENGTH
273            }
274            Error::Revocation(revocation::Error::InvalidSctEncoding) => {
275                Self::UPKI_ERR_REVOCATION_INVALID_SCT_ENCODING
276            }
277            Error::Revocation(revocation::Error::InvalidSctInCertificate(_)) => {
278                Self::UPKI_ERR_REVOCATION_INVALID_SCT_IN_CERT
279            }
280            Error::Revocation(revocation::Error::InvalidTimestamp { .. }) => {
281                Self::UPKI_ERR_REVOCATION_INVALID_TIMESTAMP
282            }
283            Error::Revocation(revocation::Error::ManifestDecode { .. }) => {
284                Self::UPKI_ERR_REVOCATION_MANIFEST_DECODE
285            }
286            Error::Revocation(revocation::Error::ManifestEncode { .. }) => {
287                Self::UPKI_ERR_REVOCATION_MANIFEST_ENCODE
288            }
289            Error::Revocation(revocation::Error::ManifestRead { .. }) => {
290                Self::UPKI_ERR_REVOCATION_MANIFEST_READ
291            }
292            Error::Revocation(revocation::Error::ManifestWrite { .. }) => {
293                Self::UPKI_ERR_REVOCATION_MANIFEST_WRITE
294            }
295            Error::Revocation(revocation::Error::NoIssuer) => Self::UPKI_ERR_REVOCATION_NO_ISSUER,
296            Error::Revocation(revocation::Error::Outdated(_)) => Self::UPKI_ERR_REVOCATION_OUTDATED,
297            Error::Revocation(revocation::Error::RemoveFile { .. }) => {
298                Self::UPKI_ERR_REVOCATION_REMOVE_FILE
299            }
300            Error::Revocation(revocation::Error::TooFewCertificates) => {
301                Self::UPKI_ERR_REVOCATION_TOO_FEW_CERTS
302            }
303            _ => Self::UPKI_ERR_UNKNOWN,
304        }
305    }
306}