use crate::core::{Attestation, DevicePublicKey, MAX_ATTESTATION_JSON_SIZE, MAX_JSON_BATCH_SIZE};
use crate::error::AttestationError;
use crate::types::CanonicalDid;
use crate::verifier::Verifier;
use crate::witness::WitnessVerifyConfig;
use auths_keri::witness::SignedReceipt;
use log::error;
use std::os::raw::c_int;
use std::panic;
use std::slice;
fn pk_from_bytes_ffi(bytes: &[u8]) -> Result<DevicePublicKey, c_int> {
let curve = match bytes.len() {
32 => auths_crypto::CurveType::Ed25519,
33 | 65 => auths_crypto::CurveType::P256,
_ => return Err(ERR_VERIFY_INVALID_PK_LEN),
};
DevicePublicKey::try_new(curve, bytes).map_err(|_| ERR_VERIFY_INVALID_PK_LEN)
}
#[allow(clippy::expect_used)]
fn with_runtime<F: std::future::Future>(f: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("FFI: failed to create tokio runtime")
.block_on(f)
}
pub const VERIFY_SUCCESS: c_int = 0;
pub const ERR_VERIFY_NULL_ARGUMENT: c_int = -1;
pub const ERR_VERIFY_JSON_PARSE: c_int = -2;
pub const ERR_VERIFY_INVALID_PK_LEN: c_int = -3;
pub const ERR_VERIFY_ISSUER_SIG_FAIL: c_int = -4;
pub const ERR_VERIFY_DEVICE_SIG_FAIL: c_int = -5;
pub const ERR_VERIFY_EXPIRED: c_int = -6;
pub const ERR_VERIFY_REVOKED: c_int = -7;
pub const ERR_VERIFY_SERIALIZATION: c_int = -8;
pub const ERR_VERIFY_INSUFFICIENT_WITNESSES: c_int = -9;
pub const ERR_VERIFY_WITNESS_PARSE: c_int = -10;
pub const ERR_VERIFY_INPUT_TOO_LARGE: c_int = -11;
pub const ERR_VERIFY_FUTURE_TIMESTAMP: c_int = -12;
pub const ERR_VERIFY_BUFFER_TOO_SMALL: c_int = -13;
pub const ERR_VERIFY_INVALID_UTF8: c_int = -14;
pub const ERR_VERIFY_OTHER: c_int = -99;
pub const ERR_VERIFY_PANIC: c_int = -127;
fn attestation_error_to_code(e: &AttestationError) -> c_int {
match e {
AttestationError::IssuerSignatureFailed(_) => ERR_VERIFY_ISSUER_SIG_FAIL,
AttestationError::DeviceSignatureFailed(_) => ERR_VERIFY_DEVICE_SIG_FAIL,
AttestationError::AttestationExpired { .. } => ERR_VERIFY_EXPIRED,
AttestationError::AttestationRevoked => ERR_VERIFY_REVOKED,
AttestationError::TimestampInFuture { .. } => ERR_VERIFY_FUTURE_TIMESTAMP,
AttestationError::SerializationError(_) => ERR_VERIFY_SERIALIZATION,
AttestationError::InvalidInput(_) => ERR_VERIFY_INVALID_PK_LEN,
AttestationError::InputTooLarge(_) => ERR_VERIFY_INPUT_TOO_LARGE,
AttestationError::BundleExpired { .. } => ERR_VERIFY_EXPIRED,
AttestationError::AttestationTooOld { .. } => ERR_VERIFY_EXPIRED,
_ => ERR_VERIFY_OTHER,
}
}
fn check_batch_sizes(sizes: &[usize], caller: &str) -> Option<c_int> {
for &size in sizes {
if size > MAX_JSON_BATCH_SIZE {
error!("FFI {}: JSON too large ({} bytes)", caller, size);
return Some(ERR_VERIFY_INPUT_TOO_LARGE);
}
}
None
}
#[derive(serde::Deserialize)]
struct WitnessKeyEntry {
did: String,
pk_hex: String,
}
type WitnessKeys = Vec<(String, Vec<u8>)>;
fn parse_witness_inputs(
receipts_json: &[u8],
witness_keys_json: &[u8],
) -> Result<(Vec<SignedReceipt>, WitnessKeys), c_int> {
let receipts: Vec<SignedReceipt> = serde_json::from_slice(receipts_json).map_err(|e| {
error!("FFI: receipts JSON parse error: {}", e);
ERR_VERIFY_WITNESS_PARSE
})?;
let key_entries: Vec<WitnessKeyEntry> =
serde_json::from_slice(witness_keys_json).map_err(|e| {
error!("FFI: witness keys JSON parse error: {}", e);
ERR_VERIFY_WITNESS_PARSE
})?;
let witness_keys: Vec<(String, Vec<u8>)> = key_entries
.into_iter()
.map(|e| {
hex::decode(&e.pk_hex)
.map(|pk| (e.did, pk))
.map_err(|err| err.to_string())
})
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
error!("FFI: witness key hex decode error: {}", e);
ERR_VERIFY_WITNESS_PARSE
})?;
Ok((receipts, witness_keys))
}
unsafe fn write_report_to_buffer(
report: &impl serde::Serialize,
result_ptr: *mut u8,
result_len: *mut usize,
caller: &str,
) -> c_int {
let report_json = match serde_json::to_vec(report) {
Ok(j) => j,
Err(e) => {
error!("FFI {}: serialization error: {}", caller, e);
return ERR_VERIFY_SERIALIZATION;
}
};
let max_len = unsafe { *result_len };
if report_json.len() > max_len {
error!("FFI {}: output buffer too small", caller);
return ERR_VERIFY_SERIALIZATION;
}
unsafe {
std::ptr::copy_nonoverlapping(report_json.as_ptr(), result_ptr, report_json.len());
*result_len = report_json.len();
}
VERIFY_SUCCESS
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_verify_attestation_json(
attestation_json_ptr: *const u8,
attestation_json_len: usize,
issuer_pk_ptr: *const u8,
issuer_pk_len: usize,
) -> c_int {
let result = panic::catch_unwind(|| {
if attestation_json_ptr.is_null() || issuer_pk_ptr.is_null() {
error!("FFI verify failed: Received null pointer argument.");
return ERR_VERIFY_NULL_ARGUMENT;
}
if attestation_json_len > MAX_ATTESTATION_JSON_SIZE {
error!(
"FFI verify failed: input too large ({} bytes, max {})",
attestation_json_len, MAX_ATTESTATION_JSON_SIZE
);
return ERR_VERIFY_INPUT_TOO_LARGE;
}
let attestation_json_slice =
unsafe { slice::from_raw_parts(attestation_json_ptr, attestation_json_len) };
let issuer_pk_slice = unsafe { slice::from_raw_parts(issuer_pk_ptr, issuer_pk_len) };
let issuer_pk = match pk_from_bytes_ffi(issuer_pk_slice) {
Ok(pk) => pk,
Err(code) => {
error!(
"FFI verify failed: invalid issuer PK length ({} bytes)",
issuer_pk_len
);
return code;
}
};
let att: Attestation = match serde_json::from_slice(attestation_json_slice) {
Ok(a) => a,
Err(e) => {
error!("FFI verify failed: JSON deserialization error: {}", e);
return ERR_VERIFY_JSON_PARSE;
}
};
let verifier = Verifier::native();
match with_runtime(verifier.verify_with_keys(&att, &issuer_pk)) {
Ok(_) => VERIFY_SUCCESS,
Err(e) => {
error!("FFI verify failed: Verification logic error: {}", e);
attestation_error_to_code(&e)
}
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_verify_attestation_json: panic occurred");
ERR_VERIFY_PANIC
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_verify_chain_with_witnesses(
chain_json_ptr: *const u8,
chain_json_len: usize,
root_pk_ptr: *const u8,
root_pk_len: usize,
receipts_json_ptr: *const u8,
receipts_json_len: usize,
witness_keys_json_ptr: *const u8,
witness_keys_json_len: usize,
threshold: u32,
result_ptr: *mut u8,
result_len: *mut usize,
) -> c_int {
let result = panic::catch_unwind(|| {
if chain_json_ptr.is_null()
|| root_pk_ptr.is_null()
|| receipts_json_ptr.is_null()
|| witness_keys_json_ptr.is_null()
|| result_ptr.is_null()
|| result_len.is_null()
{
error!("FFI verify_chain_with_witnesses: null pointer argument");
return ERR_VERIFY_NULL_ARGUMENT;
}
if let Some(code) = check_batch_sizes(
&[chain_json_len, receipts_json_len, witness_keys_json_len],
"verify_chain_with_witnesses",
) {
return code;
}
let chain_json = unsafe { slice::from_raw_parts(chain_json_ptr, chain_json_len) };
let root_pk_slice = unsafe { slice::from_raw_parts(root_pk_ptr, root_pk_len) };
let receipts_json = unsafe { slice::from_raw_parts(receipts_json_ptr, receipts_json_len) };
let witness_keys_json =
unsafe { slice::from_raw_parts(witness_keys_json_ptr, witness_keys_json_len) };
let root_pk = match pk_from_bytes_ffi(root_pk_slice) {
Ok(pk) => pk,
Err(code) => {
error!(
"FFI verify_chain_with_witnesses: invalid root PK length ({} bytes)",
root_pk_len
);
return code;
}
};
let attestations: Vec<Attestation> = match serde_json::from_slice(chain_json) {
Ok(a) => a,
Err(e) => {
error!(
"FFI verify_chain_with_witnesses: chain JSON parse error: {}",
e
);
return ERR_VERIFY_JSON_PARSE;
}
};
let (receipts, witness_keys) = match parse_witness_inputs(receipts_json, witness_keys_json)
{
Ok(pair) => pair,
Err(code) => return code,
};
let config = WitnessVerifyConfig {
receipts: &receipts,
witness_keys: &witness_keys,
threshold: threshold as usize,
};
let verifier = Verifier::native();
let report = match with_runtime(verifier.verify_chain_with_witnesses(
&attestations,
&root_pk,
&config,
)) {
Ok(r) => r,
Err(e) => {
error!("FFI verify_chain_with_witnesses: verification error: {}", e);
return attestation_error_to_code(&e);
}
};
unsafe {
write_report_to_buffer(
&report,
result_ptr,
result_len,
"verify_chain_with_witnesses",
)
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_verify_chain_with_witnesses: panic occurred");
ERR_VERIFY_PANIC
})
}
unsafe fn write_str_to_buffer(payload: &str, result_ptr: *mut u8, result_len: *mut usize) -> c_int {
let bytes = payload.as_bytes();
let capacity = unsafe { *result_len };
if bytes.len() > capacity {
unsafe { *result_len = bytes.len() };
return ERR_VERIFY_BUFFER_TOO_SMALL;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), result_ptr, bytes.len());
*result_len = bytes.len();
}
VERIFY_SUCCESS
}
unsafe fn verify_json_into_buffer(
request_ptr: *const u8,
request_len: usize,
result_ptr: *mut u8,
result_len: *mut usize,
caller: &str,
core: fn(&str) -> String,
) -> c_int {
if request_ptr.is_null() || result_ptr.is_null() || result_len.is_null() {
error!("FFI {caller}: null pointer argument");
return ERR_VERIFY_NULL_ARGUMENT;
}
if request_len > MAX_JSON_BATCH_SIZE {
error!("FFI {caller}: request too large ({request_len} bytes)");
return ERR_VERIFY_INPUT_TOO_LARGE;
}
let request_bytes = unsafe { slice::from_raw_parts(request_ptr, request_len) };
let request = match std::str::from_utf8(request_bytes) {
Ok(s) => s,
Err(_) => {
error!("FFI {caller}: request was not valid UTF-8");
return ERR_VERIFY_INVALID_UTF8;
}
};
let verdict = core(request);
unsafe { write_str_to_buffer(&verdict, result_ptr, result_len) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn auths_verify_presentation_json(
request_ptr: *const u8,
request_len: usize,
result_ptr: *mut u8,
result_len: *mut usize,
) -> c_int {
let result = panic::catch_unwind(|| unsafe {
verify_json_into_buffer(
request_ptr,
request_len,
result_ptr,
result_len,
"auths_verify_presentation_json",
crate::contract::verify_presentation_json,
)
});
result.unwrap_or_else(|_| {
error!("FFI auths_verify_presentation_json: panic occurred");
ERR_VERIFY_PANIC
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn auths_verify_credential_json(
request_ptr: *const u8,
request_len: usize,
result_ptr: *mut u8,
result_len: *mut usize,
) -> c_int {
let result = panic::catch_unwind(|| unsafe {
verify_json_into_buffer(
request_ptr,
request_len,
result_ptr,
result_len,
"auths_verify_credential_json",
crate::contract::verify_credential_json,
)
});
result.unwrap_or_else(|_| {
error!("FFI auths_verify_credential_json: panic occurred");
ERR_VERIFY_PANIC
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_verify_chain_json(
chain_json_ptr: *const u8,
chain_json_len: usize,
root_pk_ptr: *const u8,
root_pk_len: usize,
result_ptr: *mut u8,
result_len: *mut usize,
) -> c_int {
let result = panic::catch_unwind(|| {
if chain_json_ptr.is_null()
|| root_pk_ptr.is_null()
|| result_ptr.is_null()
|| result_len.is_null()
{
error!("FFI verify_chain_json: null pointer argument");
return ERR_VERIFY_NULL_ARGUMENT;
}
if chain_json_len > MAX_JSON_BATCH_SIZE {
error!(
"FFI verify_chain_json: chain JSON too large ({} bytes)",
chain_json_len
);
return ERR_VERIFY_INPUT_TOO_LARGE;
}
let chain_json = unsafe { slice::from_raw_parts(chain_json_ptr, chain_json_len) };
let root_pk_slice = unsafe { slice::from_raw_parts(root_pk_ptr, root_pk_len) };
let root_pk = match pk_from_bytes_ffi(root_pk_slice) {
Ok(pk) => pk,
Err(code) => {
error!(
"FFI verify_chain_json: invalid root PK length ({} bytes)",
root_pk_len
);
return code;
}
};
let attestations: Vec<Attestation> = match serde_json::from_slice(chain_json) {
Ok(a) => a,
Err(e) => {
error!("FFI verify_chain_json: chain JSON parse error: {}", e);
return ERR_VERIFY_JSON_PARSE;
}
};
let verifier = Verifier::native();
let report = match with_runtime(verifier.verify_chain(&attestations, &root_pk)) {
Ok(r) => r,
Err(e) => {
error!("FFI verify_chain_json: verification error: {}", e);
return attestation_error_to_code(&e);
}
};
unsafe { write_report_to_buffer(&report, result_ptr, result_len, "verify_chain_json") }
});
result.unwrap_or_else(|_| {
error!("FFI ffi_verify_chain_json: panic occurred");
ERR_VERIFY_PANIC
})
}
#[allow(clippy::too_many_lines)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn ffi_verify_device_authorization_json(
identity_did_ptr: *const u8,
identity_did_len: usize,
device_did_ptr: *const u8,
device_did_len: usize,
chain_json_ptr: *const u8,
chain_json_len: usize,
identity_pk_ptr: *const u8,
identity_pk_len: usize,
result_ptr: *mut u8,
result_len: *mut usize,
) -> c_int {
let result = panic::catch_unwind(|| {
if identity_did_ptr.is_null()
|| device_did_ptr.is_null()
|| chain_json_ptr.is_null()
|| identity_pk_ptr.is_null()
|| result_ptr.is_null()
|| result_len.is_null()
{
error!("FFI verify_device_authorization_json: null pointer argument");
return ERR_VERIFY_NULL_ARGUMENT;
}
if chain_json_len > MAX_JSON_BATCH_SIZE {
error!(
"FFI verify_device_authorization_json: chain JSON too large ({} bytes)",
chain_json_len
);
return ERR_VERIFY_INPUT_TOO_LARGE;
}
let identity_did_bytes =
unsafe { slice::from_raw_parts(identity_did_ptr, identity_did_len) };
let device_did_bytes = unsafe { slice::from_raw_parts(device_did_ptr, device_did_len) };
let chain_json = unsafe { slice::from_raw_parts(chain_json_ptr, chain_json_len) };
let identity_pk_slice = unsafe { slice::from_raw_parts(identity_pk_ptr, identity_pk_len) };
let identity_pk = match pk_from_bytes_ffi(identity_pk_slice) {
Ok(pk) => pk,
Err(code) => {
error!(
"FFI verify_device_authorization_json: invalid identity PK length ({} bytes)",
identity_pk_len
);
return code;
}
};
let identity_did = match std::str::from_utf8(identity_did_bytes) {
Ok(s) => s,
Err(e) => {
error!(
"FFI verify_device_authorization_json: invalid identity DID UTF-8: {}",
e
);
return ERR_VERIFY_JSON_PARSE;
}
};
let device_did_str = match std::str::from_utf8(device_did_bytes) {
Ok(s) => s,
Err(e) => {
error!(
"FFI verify_device_authorization_json: invalid device DID UTF-8: {}",
e
);
return ERR_VERIFY_JSON_PARSE;
}
};
let device_did = match CanonicalDid::parse(device_did_str) {
Ok(d) => d,
Err(e) => {
error!(
"FFI verify_device_authorization_json: invalid device DID: {}",
e
);
return ERR_VERIFY_JSON_PARSE;
}
};
let attestations: Vec<Attestation> = match serde_json::from_slice(chain_json) {
Ok(a) => a,
Err(e) => {
error!(
"FFI verify_device_authorization_json: chain JSON parse error: {}",
e
);
return ERR_VERIFY_JSON_PARSE;
}
};
let verifier = Verifier::native();
let report = match with_runtime(verifier.verify_device_authorization(
identity_did,
&device_did,
&attestations,
&identity_pk,
)) {
Ok(r) => r,
Err(e) => {
error!(
"FFI verify_device_authorization_json: verification error: {}",
e
);
return attestation_error_to_code(&e);
}
};
unsafe {
write_report_to_buffer(
&report,
result_ptr,
result_len,
"verify_device_authorization_json",
)
}
});
result.unwrap_or_else(|_| {
error!("FFI ffi_verify_device_authorization_json: panic occurred");
ERR_VERIFY_PANIC
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catch_unwind_maps_panic_to_panic_code() {
let result =
panic::catch_unwind(|| -> c_int { panic!("boom") }).unwrap_or(ERR_VERIFY_PANIC);
assert_eq!(result, ERR_VERIFY_PANIC);
}
#[test]
fn write_str_to_buffer_reports_required_length_then_succeeds() {
let payload = "{\"kind\":\"valid\"}";
let mut tiny = [0u8; 4];
let mut len = tiny.len();
let rc = unsafe { write_str_to_buffer(payload, tiny.as_mut_ptr(), &mut len) };
assert_eq!(rc, ERR_VERIFY_BUFFER_TOO_SMALL);
assert_eq!(len, payload.len(), "required length reported back");
let mut big = vec![0u8; len];
let mut big_len = big.len();
let rc = unsafe { write_str_to_buffer(payload, big.as_mut_ptr(), &mut big_len) };
assert_eq!(rc, VERIFY_SUCCESS);
assert_eq!(&big[..big_len], payload.as_bytes());
}
}