From 960558518f50956d3fae8473ecd9ffeeb8eda19a Mon Sep 17 00:00:00 2001 From: captjt Date: Tue, 10 Feb 2026 08:10:35 -0500 Subject: [PATCH 1/8] feat: integrity ffi bindings --- .github/workflows/release-native-ffi.yml | 134 ++++ README.md | 33 + include/integrity_ffi.h | 412 ++++++++++++ src/ffi/blob_store.rs | 196 ++++++ src/ffi/dsse.rs | 82 +++ src/ffi/error.rs | 122 ++++ src/ffi/intoto.rs | 84 +++ src/ffi/lineage_manifest.rs | 139 ++++ src/ffi/lineage_statements.rs | 817 +++++++++++++++++++++++ src/ffi/mod.rs | 56 ++ src/ffi/model_signing.rs | 120 ++++ src/ffi/runtime.rs | 46 ++ src/ffi/signer.rs | 268 ++++++++ src/ffi/tests.rs | 411 ++++++++++++ src/ffi/util.rs | 148 ++++ src/ffi/vc.rs | 84 +++ src/ffi/version.rs | 56 ++ src/lib.rs | 4 + 18 files changed, 3212 insertions(+) create mode 100644 .github/workflows/release-native-ffi.yml create mode 100644 include/integrity_ffi.h create mode 100644 src/ffi/blob_store.rs create mode 100644 src/ffi/dsse.rs create mode 100644 src/ffi/error.rs create mode 100644 src/ffi/intoto.rs create mode 100644 src/ffi/lineage_manifest.rs create mode 100644 src/ffi/lineage_statements.rs create mode 100644 src/ffi/mod.rs create mode 100644 src/ffi/model_signing.rs create mode 100644 src/ffi/runtime.rs create mode 100644 src/ffi/signer.rs create mode 100644 src/ffi/tests.rs create mode 100644 src/ffi/util.rs create mode 100644 src/ffi/vc.rs create mode 100644 src/ffi/version.rs diff --git a/.github/workflows/release-native-ffi.yml b/.github/workflows/release-native-ffi.yml new file mode 100644 index 0000000..668e762 --- /dev/null +++ b/.github/workflows/release-native-ffi.yml @@ -0,0 +1,134 @@ +name: Release Native FFI Artifacts + +on: + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + name: Build (${{ matrix.label }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + label: linux x86_64 + asset_suffix: linux-x86_64 + lib_path: target/release/libintegrity.so + lib_name: libintegrity.so + import_lib_candidates: "" + - os: macos-13 + label: macos 13 x86_64 + asset_suffix: macos-13-x86_64 + lib_path: target/release/libintegrity.dylib + lib_name: libintegrity.dylib + import_lib_candidates: "" + - os: macos-14 + label: macos 14 aarch64 + asset_suffix: macos-14-aarch64 + lib_path: target/release/libintegrity.dylib + lib_name: libintegrity.dylib + import_lib_candidates: "" + - os: macos-15-intel + label: macos 15 x86_64 + asset_suffix: macos-15-x86_64 + lib_path: target/release/libintegrity.dylib + lib_name: libintegrity.dylib + import_lib_candidates: "" + - os: macos-15 + label: macos 15 aarch64 + asset_suffix: macos-15-aarch64 + lib_path: target/release/libintegrity.dylib + lib_name: libintegrity.dylib + import_lib_candidates: "" + - os: windows-2022 + label: windows x86_64 + asset_suffix: windows-x86_64 + lib_path: target/release/integrity.dll + lib_name: integrity.dll + import_lib_candidates: "target/release/integrity.lib target/release/integrity.dll.lib target/release/libintegrity.dll.a" + + steps: + - name: Checkout repo + uses: eqtylab-actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: eqtylab-actions/rust-cache@v2 + + - name: Build release cdylib + run: cargo build --release --locked + + - name: Stage artifact files + shell: bash + run: | + set -euxo pipefail + pkg="integrity-ffi-${{ matrix.asset_suffix }}" + out_dir="dist/${pkg}" + mkdir -p "${out_dir}" + + cp "${{ matrix.lib_path }}" "${out_dir}/${{ matrix.lib_name }}" + cp include/integrity_ffi.h "${out_dir}/integrity_ffi.h" + cp LICENSE "${out_dir}/LICENSE" + + for candidate in ${{ matrix.import_lib_candidates }}; do + if [ -f "${candidate}" ]; then + cp "${candidate}" "${out_dir}/" + fi + done + + cat > "${out_dir}/BUILD_INFO.txt" < release/SHA256SUMS.txt + + - name: Upload release assets + uses: softprops/action-gh-release@v2 + with: + files: | + release/*.tar.gz + release/SHA256SUMS.txt diff --git a/README.md b/README.md index 32c7572..9e64e30 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,39 @@ just update-static-contexts This downloads the latest W3C contexts and regenerates the CID-indexed files. +## FFI (C ABI) + +The crate now includes a stable C ABI surface in `src/ffi/` for SDK bindings (including the planned Go SDK). + +- Public header: `include/integrity_ffi.h` +- ABI version functions: + - `ig_abi_version_major` + - `ig_abi_version_minor` + - `ig_abi_version_patch` + - `ig_abi_version_string` +- Runtime and handle model: + - Create one runtime with `ig_runtime_new` + - Create and reuse opaque handles (signers, blob stores) + - Release memory with `ig_string_free`, `ig_error_free`, `ig_bytes_free` + - Release handles with their corresponding `*_free` function + +The current ABI version is `0.2.0`. + +### Native Artifact Releases + +GitHub Actions can publish prebuilt native FFI artifacts for each supported system: +- Linux x86_64 (`libintegrity.so`) +- macOS 13 x86_64 (`libintegrity.dylib`) +- macOS 14 aarch64 (`libintegrity.dylib`) +- macOS 15 x86_64 (`libintegrity.dylib`) +- macOS 15 aarch64 (`libintegrity.dylib`) +- Windows x86_64 (`integrity.dll` plus import library when produced) + +Workflow: `.github/workflows/release-native-ffi.yml` + +- Push a version tag like `v0.2.0` to build and attach release assets to that GitHub Release. +- Use `workflow_dispatch` to run the build matrix and collect workflow artifacts without publishing a Release. + # Development Nix flake creates a dev environment with all the dependencies. diff --git a/include/integrity_ffi.h b/include/integrity_ffi.h new file mode 100644 index 0000000..661f214 --- /dev/null +++ b/include/integrity_ffi.h @@ -0,0 +1,412 @@ +#ifndef INTEGRITY_FFI_H +#define INTEGRITY_FFI_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct IgRuntimeHandle IgRuntimeHandle; +typedef struct IgSignerHandle IgSignerHandle; +typedef struct IgBlobStoreHandle IgBlobStoreHandle; + +typedef enum IgStatus { + IG_STATUS_OK = 0, + IG_STATUS_INVALID_INPUT = 1, + IG_STATUS_NULL_POINTER = 2, + IG_STATUS_UTF8_ERROR = 3, + IG_STATUS_JSON_ERROR = 4, + IG_STATUS_VERIFICATION_FAILED = 5, + IG_STATUS_NOT_SUPPORTED = 6, + IG_STATUS_RUNTIME_ERROR = 7, + IG_STATUS_INTERNAL_ERROR = 255, +} IgStatus; + +typedef struct IgBytes { + uint8_t *ptr; + size_t len; +} IgBytes; + +void ig_string_free(char *s); +void ig_error_free(char *err); +void ig_bytes_free(IgBytes bytes); + +IgStatus ig_runtime_new(IgRuntimeHandle **out_runtime, char **err_out); +void ig_runtime_free(IgRuntimeHandle *runtime); + +IgStatus ig_abi_version_string(char **out_version, char **err_out); +uint32_t ig_abi_version_major(void); +uint32_t ig_abi_version_minor(void); +uint32_t ig_abi_version_patch(void); +IgStatus ig_core_crate_version(char **out_version, char **err_out); + +void ig_signer_free(IgSignerHandle *signer); +IgStatus ig_signer_get_did(const IgSignerHandle *signer, char **out_did, char **err_out); +IgStatus ig_signer_ed25519_create(IgSignerHandle **out_signer, char **out_did, char **err_out); +IgStatus ig_signer_ed25519_import( + const uint8_t *secret_key_ptr, + size_t secret_key_len, + IgSignerHandle **out_signer, + char **out_did, + char **err_out +); +IgStatus ig_signer_p256_create(IgSignerHandle **out_signer, char **out_did, char **err_out); +IgStatus ig_signer_p256_import( + const uint8_t *secret_key_ptr, + size_t secret_key_len, + IgSignerHandle **out_signer, + char **out_did, + char **err_out +); +IgStatus ig_signer_secp256k1_create(IgSignerHandle **out_signer, char **out_did, char **err_out); +IgStatus ig_signer_secp256k1_import( + const uint8_t *secret_key_ptr, + size_t secret_key_len, + IgSignerHandle **out_signer, + char **out_did, + char **err_out +); +IgStatus ig_signer_auth_service_create( + const IgRuntimeHandle *runtime, + const char *api_key, + const char *url, + IgSignerHandle **out_signer, + char **out_did, + char **err_out +); +IgStatus ig_signer_vcomp_notary_create( + const IgRuntimeHandle *runtime, + const char *url, + const char *pub_key_hex_or_null, + IgSignerHandle **out_signer, + char **out_did, + char **err_out +); +IgStatus ig_signer_akv_create( + const IgRuntimeHandle *runtime, + const char *config_json, + const char *key_name, + IgSignerHandle **out_signer, + char **out_did, + char **err_out +); +IgStatus ig_signer_yubihsm_create( + uint16_t auth_key_id, + uint16_t signing_key_id, + const char *password, + IgSignerHandle **out_signer, + char **out_did, + char **err_out +); +IgStatus ig_signer_save( + const IgSignerHandle *signer, + const char *folder, + const char *name, + char **err_out +); +IgStatus ig_signer_load( + const char *signer_file, + IgSignerHandle **out_signer, + char **out_did, + char **err_out +); + +IgStatus ig_dsse_sign( + const IgRuntimeHandle *runtime, + const IgSignerHandle *signer, + const uint8_t *payload_ptr, + size_t payload_len, + const char *payload_type, + char **out_envelope_json, + char **err_out +); +IgStatus ig_dsse_sign_integrity_statement( + const IgRuntimeHandle *runtime, + const IgSignerHandle *signer, + const char *statement_cid, + char **out_envelope_json, + char **err_out +); +IgStatus ig_dsse_verify( + const IgRuntimeHandle *runtime, + const char *envelope_json, + bool *out_valid, + char **err_out +); + +IgStatus ig_vc_issue( + const IgRuntimeHandle *runtime, + const IgSignerHandle *signer, + const char *subject, + char **out_credential_json, + char **err_out +); +IgStatus ig_vc_sign( + const IgRuntimeHandle *runtime, + const IgSignerHandle *signer, + const char *unsigned_credential_json, + char **out_signed_credential_json, + char **err_out +); +IgStatus ig_vc_verify( + const IgRuntimeHandle *runtime, + const char *credential_json, + char **out_verify_result_json, + bool *out_valid, + char **err_out +); + +IgStatus ig_intoto_sign_statement( + const IgRuntimeHandle *runtime, + const IgSignerHandle *signer, + const char *statement_json, + char **out_dsse_envelope_json, + char **err_out +); +IgStatus ig_intoto_verify_envelope( + const IgRuntimeHandle *runtime, + const char *dsse_envelope_json, + bool *out_valid, + char **err_out +); +IgStatus ig_intoto_digest_from_cid(const char *cid, char **out_digest_json, char **err_out); + +IgStatus ig_model_signing_create_intoto_statement_from_hashes( + const IgRuntimeHandle *runtime, + const char *model_name, + const char *path_hashes_json, + bool allow_symlinks, + const char *ignore_paths_json_or_null, + char **out_statement_json, + char **err_out +); +IgStatus ig_model_signing_create_sigstore_bundle( + const char *dsse_json, + const char *signer_did_key, + char **out_sigstore_bundle_json, + char **err_out +); + +IgStatus ig_lineage_statement_create_association( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_computation( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_data( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_dsse( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_entity( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_governance( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_metadata( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_metadata_from_json( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_sigstore_bundle( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_storage( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_vc( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_did_regular( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_did_amdsev_v1( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_did_azure_v1( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_did_custom_v1( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_did_docker_v1( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_create_did_inteltdx_v0( + const IgRuntimeHandle *runtime, + const char *request_json, + char **out_statement_json, + char **err_out +); +IgStatus ig_lineage_statement_compute_cid( + const IgRuntimeHandle *runtime, + const char *statement_json, + char **out_cid, + char **err_out +); +IgStatus ig_lineage_statement_extract_id( + const char *statement_json, + char **out_id, + char **err_out +); +IgStatus ig_lineage_statement_extract_type( + const char *statement_json, + char **out_type, + char **err_out +); +IgStatus ig_lineage_statement_jsonld_filename( + const char *statement_json, + char **out_filename, + char **err_out +); +IgStatus ig_lineage_statement_referenced_cids_json( + const char *statement_json, + char **out_referenced_cids_json, + char **err_out +); +IgStatus ig_lineage_statement_registered_by( + const char *statement_json, + char **out_registered_by, + char **err_out +); + +void ig_blob_store_free(IgBlobStoreHandle *store); +IgStatus ig_blob_store_local_fs_new( + const IgRuntimeHandle *runtime, + const char *path, + IgBlobStoreHandle **out_store, + char **err_out +); +IgStatus ig_blob_store_s3_new( + const IgRuntimeHandle *runtime, + const char *region, + const char *bucket, + const char *folder, + IgBlobStoreHandle **out_store, + char **err_out +); +IgStatus ig_blob_store_gcs_new( + const IgRuntimeHandle *runtime, + const char *bucket, + const char *folder, + IgBlobStoreHandle **out_store, + char **err_out +); +IgStatus ig_blob_store_azure_blob_new( + const IgRuntimeHandle *runtime, + const char *account, + const char *key, + const char *container, + IgBlobStoreHandle **out_store, + char **err_out +); +IgStatus ig_blob_store_exists( + const IgRuntimeHandle *runtime, + const IgBlobStoreHandle *store, + const char *cid, + bool *out_exists, + char **err_out +); +IgStatus ig_blob_store_get( + const IgRuntimeHandle *runtime, + const IgBlobStoreHandle *store, + const char *cid, + IgBytes *out_blob, + bool *out_found, + char **err_out +); +IgStatus ig_blob_store_put( + const IgRuntimeHandle *runtime, + const IgBlobStoreHandle *store, + const uint8_t *blob_ptr, + size_t blob_len, + uint64_t multicodec_code, + const char *expected_cid_or_null, + char **out_cid, + char **err_out +); + +IgStatus ig_lineage_manifest_generate( + const IgRuntimeHandle *runtime, + bool include_context, + const char *statements_json, + const char *attributes_json_or_null, + const char *blobs_json, + char **out_manifest_json, + char **err_out +); +IgStatus ig_lineage_manifest_merge( + const IgRuntimeHandle *runtime, + const char *manifest_a_json, + const char *manifest_b_json, + char **out_manifest_json, + char **err_out +); +IgStatus ig_lineage_manifest_resolve_blobs( + const IgRuntimeHandle *runtime, + const char *statements_json, + const IgBlobStoreHandle *store, + uint32_t concurrency_limit, + char **out_blobs_json, + char **err_out +); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/ffi/blob_store.rs b/src/ffi/blob_store.rs new file mode 100644 index 0000000..85a8b2d --- /dev/null +++ b/src/ffi/blob_store.rs @@ -0,0 +1,196 @@ +use std::{ffi::c_char, path::PathBuf, ptr, sync::Arc}; + +use crate::{ + blob_store::{AzureBlob, BlobStore, LocalFs, GCS, S3}, + ffi::{ + error::{map_anyhow, run_ffi, FfiError, IgStatus}, + runtime::IgRuntimeHandle, + util::{ + as_mut, as_ref, bytes_from_raw, cstr_to_string, optional_cstr_to_string, write_bool, + write_c_string, write_ig_bytes, write_out_ptr, + }, + IgBytes, + }, +}; + +pub struct IgBlobStoreHandle { + pub(crate) store: Arc, +} + +fn init_blob_store( + runtime: &IgRuntimeHandle, + mut store: S, +) -> Result +where + S: BlobStore + Send + Sync + 'static, +{ + map_anyhow(runtime.block_on(store.init()))?; + Ok(IgBlobStoreHandle { + store: Arc::new(store), + }) +} + +#[no_mangle] +pub extern "C" fn ig_blob_store_free(store: *mut IgBlobStoreHandle) { + if store.is_null() { + return; + } + + unsafe { + drop(Box::from_raw(store)); + } +} + +#[no_mangle] +pub extern "C" fn ig_blob_store_local_fs_new( + runtime: *const IgRuntimeHandle, + path: *const c_char, + out_store: *mut *mut IgBlobStoreHandle, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let path = cstr_to_string(path, "path")?; + + let store = init_blob_store(runtime, LocalFs::new(PathBuf::from(path)))?; + write_out_ptr(out_store, store, "out_store") + }) +} + +#[no_mangle] +pub extern "C" fn ig_blob_store_s3_new( + runtime: *const IgRuntimeHandle, + region: *const c_char, + bucket: *const c_char, + folder: *const c_char, + out_store: *mut *mut IgBlobStoreHandle, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let region = cstr_to_string(region, "region")?; + let bucket = cstr_to_string(bucket, "bucket")?; + let folder = cstr_to_string(folder, "folder")?; + + let store = init_blob_store(runtime, S3::new(region, bucket, folder))?; + write_out_ptr(out_store, store, "out_store") + }) +} + +#[no_mangle] +pub extern "C" fn ig_blob_store_gcs_new( + runtime: *const IgRuntimeHandle, + bucket: *const c_char, + folder: *const c_char, + out_store: *mut *mut IgBlobStoreHandle, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let bucket = cstr_to_string(bucket, "bucket")?; + let folder = cstr_to_string(folder, "folder")?; + + let store = init_blob_store(runtime, GCS::new(bucket, folder))?; + write_out_ptr(out_store, store, "out_store") + }) +} + +#[no_mangle] +pub extern "C" fn ig_blob_store_azure_blob_new( + runtime: *const IgRuntimeHandle, + account: *const c_char, + key: *const c_char, + container: *const c_char, + out_store: *mut *mut IgBlobStoreHandle, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let account = cstr_to_string(account, "account")?; + let key = cstr_to_string(key, "key")?; + let container = cstr_to_string(container, "container")?; + + let store = init_blob_store(runtime, AzureBlob::new(account, key, container))?; + write_out_ptr(out_store, store, "out_store") + }) +} + +#[no_mangle] +pub extern "C" fn ig_blob_store_exists( + runtime: *const IgRuntimeHandle, + store: *const IgBlobStoreHandle, + cid: *const c_char, + out_exists: *mut bool, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let store = as_ref(store, "store")?; + let cid = cstr_to_string(cid, "cid")?; + + let exists = map_anyhow(runtime.block_on(store.store.exists(&cid)))?; + write_bool(out_exists, exists, "out_exists") + }) +} + +#[no_mangle] +pub extern "C" fn ig_blob_store_get( + runtime: *const IgRuntimeHandle, + store: *const IgBlobStoreHandle, + cid: *const c_char, + out_blob: *mut IgBytes, + out_found: *mut bool, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let store = as_ref(store, "store")?; + let cid = cstr_to_string(cid, "cid")?; + + let out_blob = as_mut(out_blob, "out_blob")?; + *out_blob = IgBytes { + ptr: ptr::null_mut(), + len: 0, + }; + + let blob = map_anyhow(runtime.block_on(store.store.get(&cid)))?; + match blob { + Some(bytes) => { + write_ig_bytes(out_blob, bytes, "out_blob")?; + write_bool(out_found, true, "out_found")?; + } + None => { + write_bool(out_found, false, "out_found")?; + } + } + + Ok(()) + }) +} + +#[no_mangle] +pub extern "C" fn ig_blob_store_put( + runtime: *const IgRuntimeHandle, + store: *const IgBlobStoreHandle, + blob_ptr: *const u8, + blob_len: usize, + multicodec_code: u64, + expected_cid_or_null: *const c_char, + out_cid: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let store = as_ref(store, "store")?; + let blob = bytes_from_raw(blob_ptr, blob_len, "blob_ptr")?; + let expected_cid = optional_cstr_to_string(expected_cid_or_null)?; + + let cid = map_anyhow(runtime.block_on(store.store.put( + blob, + multicodec_code, + expected_cid.as_deref(), + )))?; + + write_c_string(out_cid, cid, "out_cid") + }) +} diff --git a/src/ffi/dsse.rs b/src/ffi/dsse.rs new file mode 100644 index 0000000..11bf96b --- /dev/null +++ b/src/ffi/dsse.rs @@ -0,0 +1,82 @@ +use std::{ffi::c_char, str::FromStr, sync::Arc}; + +use crate::{ + dsse::{self, PayloadType}, + ffi::{ + error::{map_anyhow, run_ffi, FfiError, IgStatus}, + runtime::IgRuntimeHandle, + signer::IgSignerHandle, + util::{as_ref, bytes_from_raw, cstr_to_string, write_c_string}, + }, + signer::Signer, +}; + +#[no_mangle] +pub extern "C" fn ig_dsse_sign( + runtime: *const IgRuntimeHandle, + signer: *const IgSignerHandle, + payload_ptr: *const u8, + payload_len: usize, + payload_type: *const c_char, + out_envelope_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let signer = as_ref(signer, "signer")?; + let payload = bytes_from_raw(payload_ptr, payload_len, "payload_ptr")?; + let payload_type = cstr_to_string(payload_type, "payload_type")?; + let payload_type = map_anyhow(PayloadType::from_str(&payload_type))?; + + let signer_arc: Arc = Arc::new(signer.signer.clone()); + let envelope = map_anyhow(runtime.block_on(dsse::sign_dsse( + payload, + payload_type, + Some(signer_arc), + None, + )))?; + + let envelope_json = map_anyhow(envelope.into_json_string())?; + write_c_string(out_envelope_json, envelope_json, "out_envelope_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_dsse_sign_integrity_statement( + runtime: *const IgRuntimeHandle, + signer: *const IgSignerHandle, + statement_cid: *const c_char, + out_envelope_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let signer = as_ref(signer, "signer")?; + let statement_cid = cstr_to_string(statement_cid, "statement_cid")?; + let signer_arc: Arc = Arc::new(signer.signer.clone()); + + let envelope = map_anyhow(runtime.block_on(dsse::sign_integrity_statement_dsse( + statement_cid, + Some(signer_arc), + None, + )))?; + + let envelope_json = map_anyhow(envelope.into_json_string())?; + write_c_string(out_envelope_json, envelope_json, "out_envelope_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_dsse_verify( + _runtime: *const IgRuntimeHandle, + _envelope_json: *const c_char, + _out_valid: *mut bool, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + Err(FfiError::new( + IgStatus::NotSupported, + "DSSE verify is not implemented in the Rust core yet", + )) + }) +} diff --git a/src/ffi/error.rs b/src/ffi/error.rs new file mode 100644 index 0000000..e0530c1 --- /dev/null +++ b/src/ffi/error.rs @@ -0,0 +1,122 @@ +use std::{ + ffi::{c_char, CString}, + panic::{catch_unwind, AssertUnwindSafe}, + ptr, +}; + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IgStatus { + Ok = 0, + InvalidInput = 1, + NullPointer = 2, + Utf8Error = 3, + JsonError = 4, + VerificationFailed = 5, + NotSupported = 6, + RuntimeError = 7, + InternalError = 255, +} + +pub(crate) type FfiResult = Result; + +#[derive(Debug)] +pub(crate) struct FfiError { + pub(crate) status: IgStatus, + pub(crate) message: String, +} + +impl FfiError { + pub(crate) fn new(status: IgStatus, message: impl Into) -> Self { + Self { + status, + message: message.into(), + } + } + + pub(crate) fn from_anyhow(err: anyhow::Error) -> Self { + let status = classify_anyhow(&err); + Self { + status, + message: err.to_string(), + } + } +} + +fn classify_anyhow(err: &anyhow::Error) -> IgStatus { + if err.downcast_ref::().is_some() { + return IgStatus::JsonError; + } + + if err.downcast_ref::().is_some() + || err.downcast_ref::().is_some() + || err.downcast_ref::().is_some() + { + return IgStatus::Utf8Error; + } + + let msg = err.to_string().to_ascii_lowercase(); + if msg.contains("null pointer") { + IgStatus::NullPointer + } else if msg.contains("verification failed") || msg.contains("invalid signature") { + IgStatus::VerificationFailed + } else if msg.contains("not implemented") || msg.contains("unsupported") { + IgStatus::NotSupported + } else if msg.contains("invalid") || msg.contains("missing") || msg.contains("expected") { + IgStatus::InvalidInput + } else { + IgStatus::InternalError + } +} + +pub(crate) fn map_anyhow(res: anyhow::Result) -> FfiResult { + res.map_err(FfiError::from_anyhow) +} + +pub(crate) fn clear_error(err_out: *mut *mut c_char) { + if err_out.is_null() { + return; + } + + unsafe { + *err_out = ptr::null_mut(); + } +} + +pub(crate) fn set_error(err_out: *mut *mut c_char, message: impl Into) { + if err_out.is_null() { + return; + } + + let msg = sanitize_error_message(message.into()); + let c_message = CString::new(msg).unwrap_or_else(|_| { + CString::new("failed to encode error message").expect("static message is valid") + }); + + unsafe { + *err_out = c_message.into_raw(); + } +} + +fn sanitize_error_message(message: String) -> String { + message.replace('\0', "\\0") +} + +pub(crate) fn run_ffi(err_out: *mut *mut c_char, f: F) -> IgStatus +where + F: FnOnce() -> FfiResult<()>, +{ + clear_error(err_out); + + match catch_unwind(AssertUnwindSafe(f)) { + Ok(Ok(())) => IgStatus::Ok, + Ok(Err(err)) => { + set_error(err_out, err.message); + err.status + } + Err(_) => { + set_error(err_out, "panic in FFI function"); + IgStatus::InternalError + } + } +} diff --git a/src/ffi/intoto.rs b/src/ffi/intoto.rs new file mode 100644 index 0000000..f08766c --- /dev/null +++ b/src/ffi/intoto.rs @@ -0,0 +1,84 @@ +use std::{collections::HashMap, ffi::c_char, sync::Arc}; + +use crate::{ + ffi::{ + error::{map_anyhow, run_ffi, FfiError, IgStatus}, + runtime::IgRuntimeHandle, + signer::IgSignerHandle, + util::{as_ref, cstr_to_string, write_bool, write_c_string}, + }, + intoto_attestation::{self, models}, + signer::Signer, +}; + +#[no_mangle] +pub extern "C" fn ig_intoto_sign_statement( + runtime: *const IgRuntimeHandle, + signer: *const IgSignerHandle, + statement_json: *const c_char, + out_dsse_envelope_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let signer = as_ref(signer, "signer")?; + let statement_json = cstr_to_string(statement_json, "statement_json")?; + + let statement_model = + serde_json::from_str::(&statement_json).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse in-toto statement json: {e}"), + ) + })?; + let statement = map_anyhow(crate::intoto_attestation::Statement::try_from( + statement_model, + ))?; + + let signer_arc: Arc = Arc::new(signer.signer.clone()); + let envelope_json = map_anyhow(runtime.block_on( + intoto_attestation::sign_intoto_attestation(statement, signer_arc), + ))?; + + write_c_string( + out_dsse_envelope_json, + envelope_json, + "out_dsse_envelope_json", + ) + }) +} + +#[no_mangle] +pub extern "C" fn ig_intoto_verify_envelope( + runtime: *const IgRuntimeHandle, + dsse_envelope_json: *const c_char, + out_valid: *mut bool, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let dsse_envelope_json = cstr_to_string(dsse_envelope_json, "dsse_envelope_json")?; + + let is_valid = map_anyhow(runtime.block_on( + intoto_attestation::verify_intoto_attestation(&dsse_envelope_json), + ))?; + write_bool(out_valid, is_valid, "out_valid") + }) +} + +#[no_mangle] +pub extern "C" fn ig_intoto_digest_from_cid( + cid: *const c_char, + out_digest_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let cid = cstr_to_string(cid, "cid")?; + + let digest_map: HashMap = + map_anyhow(intoto_attestation::digest_from_cid(&cid))?; + let digest_json = map_anyhow(serde_json::to_string(&digest_map).map_err(Into::into))?; + + write_c_string(out_digest_json, digest_json, "out_digest_json") + }) +} diff --git a/src/ffi/lineage_manifest.rs b/src/ffi/lineage_manifest.rs new file mode 100644 index 0000000..96659b1 --- /dev/null +++ b/src/ffi/lineage_manifest.rs @@ -0,0 +1,139 @@ +use std::{collections::HashMap, ffi::c_char}; + +use serde_json::Value; + +use crate::{ + ffi::{ + blob_store::IgBlobStoreHandle, + error::{map_anyhow, run_ffi, FfiError, IgStatus}, + runtime::IgRuntimeHandle, + util::{as_ref, cstr_to_string, optional_cstr_to_string, write_c_string}, + }, + lineage::models::{ + manifest::{self, Manifest}, + statements::Statement, + }, +}; + +fn parse_statements(statements_json: String) -> Result, FfiError> { + serde_json::from_str::>(&statements_json).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse statements json: {e}"), + ) + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_manifest_generate( + runtime: *const IgRuntimeHandle, + include_context: bool, + statements_json: *const c_char, + attributes_json_or_null: *const c_char, + blobs_json: *const c_char, + out_manifest_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let statements_json = cstr_to_string(statements_json, "statements_json")?; + let blobs_json = cstr_to_string(blobs_json, "blobs_json")?; + + let statements = parse_statements(statements_json)?; + let blobs = serde_json::from_str::>(&blobs_json).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse blobs json: {e}"), + ) + })?; + + let attributes = match optional_cstr_to_string(attributes_json_or_null)? { + Some(attributes_json) => { + let value = serde_json::from_str::>(&attributes_json) + .map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse attributes json: {e}"), + ) + })?; + Some(value) + } + None => None, + }; + + let manifest = map_anyhow(runtime.block_on(manifest::generate_manifest( + include_context, + statements, + attributes, + blobs, + )))?; + let manifest_json = map_anyhow(serde_json::to_string(&manifest).map_err(Into::into))?; + + write_c_string(out_manifest_json, manifest_json, "out_manifest_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_manifest_merge( + runtime: *const IgRuntimeHandle, + manifest_a_json: *const c_char, + manifest_b_json: *const c_char, + out_manifest_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let manifest_a_json = cstr_to_string(manifest_a_json, "manifest_a_json")?; + let manifest_b_json = cstr_to_string(manifest_b_json, "manifest_b_json")?; + + let manifest_a = serde_json::from_str::(&manifest_a_json).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse manifest_a_json: {e}"), + ) + })?; + let manifest_b = serde_json::from_str::(&manifest_b_json).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse manifest_b_json: {e}"), + ) + })?; + + let merged = map_anyhow(runtime.block_on(manifest::merge_async(manifest_a, manifest_b)))?; + let merged_json = map_anyhow(serde_json::to_string(&merged).map_err(Into::into))?; + write_c_string(out_manifest_json, merged_json, "out_manifest_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_manifest_resolve_blobs( + runtime: *const IgRuntimeHandle, + statements_json: *const c_char, + store: *const IgBlobStoreHandle, + concurrency_limit: u32, + out_blobs_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let store = as_ref(store, "store")?; + let statements_json = cstr_to_string(statements_json, "statements_json")?; + let statements = parse_statements(statements_json)?; + + if concurrency_limit == 0 { + return Err(FfiError::new( + IgStatus::InvalidInput, + "concurrency_limit must be greater than 0", + )); + } + + let blobs = map_anyhow(runtime.block_on(manifest::resolve_blobs( + &statements, + store.store.clone(), + concurrency_limit as usize, + )))?; + + let blobs_json = map_anyhow(serde_json::to_string(&blobs).map_err(Into::into))?; + write_c_string(out_blobs_json, blobs_json, "out_blobs_json") + }) +} diff --git a/src/ffi/lineage_statements.rs b/src/ffi/lineage_statements.rs new file mode 100644 index 0000000..a2a6875 --- /dev/null +++ b/src/ffi/lineage_statements.rs @@ -0,0 +1,817 @@ +use std::ffi::c_char; + +use serde::de::DeserializeOwned; +use serde::Deserialize; +use serde_json::Value; +use ssi::vc::Credential; + +use crate::{ + ffi::{ + error::{map_anyhow, run_ffi, FfiError, IgStatus}, + runtime::IgRuntimeHandle, + util::{as_ref, cstr_to_string, write_c_string}, + }, + lineage::models::{ + dsse::Envelope as LineageDsseEnvelope, + statements::{ + common::{UrnCidWithSha256, UrnCidWithSha384}, + did_statement::{ + DidStatementEqtyVCompAmdSevV1, DidStatementEqtyVCompAzureV1, + DidStatementEqtyVCompCustomV1, DidStatementEqtyVCompDockerV1, + DidStatementEqtyVCompIntelTdxV0, DidStatementRegular, + }, + extract_statement_id, extract_statement_type, AssociationStatement, + ComputationStatement, DataStatement, DsseStatement, EntityStatement, + GovernanceStatement, MetadataStatement, SigstoreBundleStatement, Statement, + StatementTrait, StorageStatement, VcStatement, + }, + }, + sigstore_bundle::SigstoreBundle, +}; + +fn parse_request( + request_json: String, + request_name: &str, +) -> Result { + serde_json::from_str(&request_json).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse {request_name} json: {e}"), + ) + }) +} + +fn parse_statement(statement_json: String) -> Result { + serde_json::from_str::(&statement_json).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse statement json: {e}"), + ) + }) +} + +fn decode_hex_vec( + hex_value: Option, + field_name: &str, +) -> Result>, FfiError> { + match hex_value { + Some(value) => { + let bytes = hex::decode(value).map_err(|e| { + FfiError::new( + IgStatus::InvalidInput, + format!("failed to decode {field_name} hex: {e}"), + ) + })?; + Ok(Some(bytes)) + } + None => Ok(None), + } +} + +fn decode_hex_arr(value: String, field_name: &str) -> Result<[u8; N], FfiError> { + let bytes = hex::decode(value).map_err(|e| { + FfiError::new( + IgStatus::InvalidInput, + format!("failed to decode {field_name} hex: {e}"), + ) + })?; + + bytes.try_into().map_err(|_| { + FfiError::new( + IgStatus::InvalidInput, + format!("{field_name} must decode to {N} bytes"), + ) + }) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct AssociationCreateRequest { + subject: String, + association: String, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ComputationCreateRequest { + computation: Option, + input: Vec, + output: Vec, + operated_by: String, + executed_on: Option, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DataCreateRequest { + data: Vec, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DsseCreateRequest { + envelope: LineageDsseEnvelope, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct EntityCreateRequest { + entity: Vec, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct GovernanceCreateRequest { + subject: String, + document: String, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct MetadataCreateRequest { + subject: String, + metadata: String, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct MetadataFromJsonCreateRequest { + subject: String, + metadata: Value, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SigstoreBundleCreateRequest { + subject: String, + sigstore_bundle: SigstoreBundle, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct StorageCreateRequest { + data: String, + stored_on: String, + operated_by: Option, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct VcCreateRequest { + credential: Value, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DidRegularCreateRequest { + did: String, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DidAmdSevV1CreateRequest { + did: String, + measurement: Option, + sev_mode: String, + num_cpu_cores: u32, + cpu_type: String, + ovmf: UrnCidWithSha256, + kernel: UrnCidWithSha256, + initrd: UrnCidWithSha256, + append: UrnCidWithSha256, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DidAzureV1CreateRequest { + did: String, + pcr11: Option, + firmware: Option, + uki: Option, + kernel: Option, + initrd: Option, + append: Option, + rootfs: Option, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DidCustomV1CreateRequest { + did: String, + value: Value, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DockerImage { + name: String, + sha256: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DidDockerV1CreateRequest { + did: String, + image: Vec, + compose: String, + operated_by: String, + executed_on: String, + registered_by: String, + timestamp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DidIntelTdxV0CreateRequest { + did: String, + measurements: Vec, + ovmf: Option, + kernel: Option, + initrd: Option, + append: Option, + registered_by: String, + timestamp: Option, +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_association( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: AssociationCreateRequest = parse_request(request_json, "association request")?; + + let statement = map_anyhow(runtime.block_on(AssociationStatement::create( + request.subject, + request.association, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_computation( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: ComputationCreateRequest = parse_request(request_json, "computation request")?; + + let statement = map_anyhow(runtime.block_on(ComputationStatement::create( + request.computation, + request.input, + request.output, + request.operated_by, + request.executed_on, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_data( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: DataCreateRequest = parse_request(request_json, "data request")?; + + let statement = map_anyhow(runtime.block_on(DataStatement::create( + request.data, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_dsse( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: DsseCreateRequest = parse_request(request_json, "dsse request")?; + + let statement = map_anyhow(runtime.block_on(DsseStatement::create( + request.envelope, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_entity( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: EntityCreateRequest = parse_request(request_json, "entity request")?; + + let statement = map_anyhow(runtime.block_on(EntityStatement::create( + request.entity, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_governance( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: GovernanceCreateRequest = parse_request(request_json, "governance request")?; + + let statement = map_anyhow(runtime.block_on(GovernanceStatement::create( + request.subject, + request.document, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_metadata( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: MetadataCreateRequest = parse_request(request_json, "metadata request")?; + + let statement = map_anyhow(runtime.block_on(MetadataStatement::create( + request.subject, + request.metadata, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_metadata_from_json( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: MetadataFromJsonCreateRequest = + parse_request(request_json, "metadata-from-json request")?; + + let statement = map_anyhow(runtime.block_on(MetadataStatement::create_from_json( + request.subject, + request.metadata, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_sigstore_bundle( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: SigstoreBundleCreateRequest = + parse_request(request_json, "sigstore-bundle request")?; + + let statement = map_anyhow(runtime.block_on(SigstoreBundleStatement::create( + request.subject, + &request.sigstore_bundle, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_storage( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: StorageCreateRequest = parse_request(request_json, "storage request")?; + + let statement = map_anyhow(runtime.block_on(StorageStatement::create( + request.data, + request.stored_on, + request.operated_by, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_vc( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: VcCreateRequest = parse_request(request_json, "vc request")?; + + let credential: Credential = serde_json::from_value(request.credential).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse vc credential: {e}"), + ) + })?; + + let statement = map_anyhow(runtime.block_on(VcStatement::create( + credential, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_did_regular( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: DidRegularCreateRequest = parse_request(request_json, "did regular request")?; + + let statement = map_anyhow(runtime.block_on(DidStatementRegular::create( + request.did, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_did_amdsev_v1( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: DidAmdSevV1CreateRequest = + parse_request(request_json, "did amdsev v1 request")?; + + let measurement = match request.measurement { + Some(value) => Some(decode_hex_arr::<32>(value, "measurement")?), + None => None, + }; + + let statement = map_anyhow(runtime.block_on(DidStatementEqtyVCompAmdSevV1::create( + request.did, + measurement, + request.sev_mode, + request.num_cpu_cores, + request.cpu_type, + request.ovmf, + request.kernel, + request.initrd, + request.append, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_did_azure_v1( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: DidAzureV1CreateRequest = parse_request(request_json, "did azure v1 request")?; + + let pcr11 = decode_hex_vec(request.pcr11, "pcr11")?; + let firmware = decode_hex_vec(request.firmware, "firmware")?; + + let statement = map_anyhow(runtime.block_on(DidStatementEqtyVCompAzureV1::create( + request.did, + pcr11, + firmware, + request.uki, + request.kernel, + request.initrd, + request.append, + request.rootfs, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_did_custom_v1( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: DidCustomV1CreateRequest = + parse_request(request_json, "did custom v1 request")?; + + let statement = map_anyhow(runtime.block_on(DidStatementEqtyVCompCustomV1::create( + request.did, + request.value, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_did_docker_v1( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: DidDockerV1CreateRequest = + parse_request(request_json, "did docker v1 request")?; + + let image = request + .image + .into_iter() + .map(|entry| (entry.name, entry.sha256)) + .collect(); + + let statement = map_anyhow(runtime.block_on(DidStatementEqtyVCompDockerV1::create( + request.did, + image, + request.compose, + request.operated_by, + request.executed_on, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_create_did_inteltdx_v0( + runtime: *const IgRuntimeHandle, + request_json: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let request_json = cstr_to_string(request_json, "request_json")?; + let request: DidIntelTdxV0CreateRequest = + parse_request(request_json, "did inteltdx v0 request")?; + + let measurements = request + .measurements + .into_iter() + .map(|value| decode_hex_arr::<48>(value, "measurement")) + .collect::, _>>()?; + + let statement = map_anyhow(runtime.block_on(DidStatementEqtyVCompIntelTdxV0::create( + request.did, + measurements, + request.ovmf, + request.kernel, + request.initrd, + request.append, + request.registered_by, + request.timestamp, + )))?; + + let statement_json = map_anyhow(serde_json::to_string(&statement).map_err(Into::into))?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_compute_cid( + runtime: *const IgRuntimeHandle, + statement_json: *const c_char, + out_cid: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let statement_json = cstr_to_string(statement_json, "statement_json")?; + let statement = parse_statement(statement_json)?; + + let cid = map_anyhow( + runtime.block_on(crate::lineage::models::statements::compute_cid(&statement)), + )?; + write_c_string(out_cid, cid, "out_cid") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_extract_id( + statement_json: *const c_char, + out_id: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let statement_json = cstr_to_string(statement_json, "statement_json")?; + let statement_value: Value = parse_request(statement_json, "statement")?; + let statement_id = map_anyhow(extract_statement_id(&statement_value))?; + write_c_string(out_id, statement_id, "out_id") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_extract_type( + statement_json: *const c_char, + out_type: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let statement_json = cstr_to_string(statement_json, "statement_json")?; + let statement_value: Value = parse_request(statement_json, "statement")?; + let statement_type = map_anyhow(extract_statement_type(&statement_value))?; + write_c_string(out_type, statement_type, "out_type") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_jsonld_filename( + statement_json: *const c_char, + out_filename: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let statement_json = cstr_to_string(statement_json, "statement_json")?; + let statement = parse_statement(statement_json)?; + write_c_string(out_filename, statement.jsonld_filename(), "out_filename") + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_referenced_cids_json( + statement_json: *const c_char, + out_referenced_cids_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let statement_json = cstr_to_string(statement_json, "statement_json")?; + let statement = parse_statement(statement_json)?; + let cids = statement.referenced_cids(); + let cids_json = map_anyhow(serde_json::to_string(&cids).map_err(Into::into))?; + write_c_string( + out_referenced_cids_json, + cids_json, + "out_referenced_cids_json", + ) + }) +} + +#[no_mangle] +pub extern "C" fn ig_lineage_statement_registered_by( + statement_json: *const c_char, + out_registered_by: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let statement_json = cstr_to_string(statement_json, "statement_json")?; + let statement = parse_statement(statement_json)?; + let registered_by = statement.get_registered_by().to_owned(); + write_c_string(out_registered_by, registered_by, "out_registered_by") + }) +} diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs new file mode 100644 index 0000000..1d67653 --- /dev/null +++ b/src/ffi/mod.rs @@ -0,0 +1,56 @@ +use std::ffi::{c_char, CString}; + +mod blob_store; +mod dsse; +mod error; +mod intoto; +mod lineage_manifest; +mod lineage_statements; +mod model_signing; +mod runtime; +mod signer; +mod util; +mod vc; +mod version; + +pub use blob_store::IgBlobStoreHandle; +pub use error::IgStatus; +pub use runtime::IgRuntimeHandle; +pub use signer::IgSignerHandle; + +#[cfg(test)] +mod tests; + +#[repr(C)] +#[derive(Debug, Clone, Copy, Default)] +pub struct IgBytes { + pub ptr: *mut u8, + pub len: usize, +} + +#[no_mangle] +pub extern "C" fn ig_string_free(s: *mut c_char) { + if s.is_null() { + return; + } + + unsafe { + drop(CString::from_raw(s)); + } +} + +#[no_mangle] +pub extern "C" fn ig_error_free(err: *mut c_char) { + ig_string_free(err); +} + +#[no_mangle] +pub extern "C" fn ig_bytes_free(bytes: IgBytes) { + if bytes.ptr.is_null() { + return; + } + + unsafe { + drop(Vec::from_raw_parts(bytes.ptr, bytes.len, bytes.len)); + } +} diff --git a/src/ffi/model_signing.rs b/src/ffi/model_signing.rs new file mode 100644 index 0000000..de4de2e --- /dev/null +++ b/src/ffi/model_signing.rs @@ -0,0 +1,120 @@ +use std::{collections::HashMap, ffi::c_char}; + +use serde_json::Value; + +use crate::{ + ffi::{ + error::{map_anyhow, run_ffi, FfiError, IgStatus}, + runtime::IgRuntimeHandle, + util::{as_ref, cstr_to_string, optional_cstr_to_string, write_c_string}, + }, + model_signing::{self, DirectoryInfo}, +}; + +#[no_mangle] +pub extern "C" fn ig_model_signing_create_intoto_statement_from_hashes( + runtime: *const IgRuntimeHandle, + model_name: *const c_char, + path_hashes_json: *const c_char, + allow_symlinks: bool, + ignore_paths_json_or_null: *const c_char, + out_statement_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let model_name = cstr_to_string(model_name, "model_name")?; + let path_hashes_json = cstr_to_string(path_hashes_json, "path_hashes_json")?; + + let path_hashes_hex = serde_json::from_str::>(&path_hashes_json) + .map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse path_hashes_json: {e}"), + ) + })?; + + let mut path_hashes = HashMap::with_capacity(path_hashes_hex.len()); + for (path, digest_hex) in path_hashes_hex { + let digest_bytes = hex::decode(digest_hex).map_err(|e| { + FfiError::new( + IgStatus::InvalidInput, + format!("failed to decode hex digest for '{path}': {e}"), + ) + })?; + + if digest_bytes.len() != 32 { + return Err(FfiError::new( + IgStatus::InvalidInput, + format!( + "digest for '{path}' must be 32 bytes, got {}", + digest_bytes.len() + ), + )); + } + + let digest: [u8; 32] = digest_bytes.try_into().map_err(|_| { + FfiError::new( + IgStatus::InvalidInput, + format!("invalid digest length for '{path}'"), + ) + })?; + + path_hashes.insert(path, digest); + } + + let ignore_paths = match optional_cstr_to_string(ignore_paths_json_or_null)? { + Some(json) => serde_json::from_str::>(&json).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse ignore_paths_json: {e}"), + ) + })?, + None => Vec::new(), + }; + + let statement = map_anyhow(runtime.block_on( + model_signing::create_model_signing_intoto_statement( + model_name, + DirectoryInfo::PathHashMap(path_hashes), + allow_symlinks, + ignore_paths, + ), + ))?; + + let statement_json = map_anyhow(statement.into_json_string())?; + write_c_string(out_statement_json, statement_json, "out_statement_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_model_signing_create_sigstore_bundle( + dsse_json: *const c_char, + signer_did_key: *const c_char, + out_sigstore_bundle_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let dsse_json = cstr_to_string(dsse_json, "dsse_json")?; + let signer_did_key = cstr_to_string(signer_did_key, "signer_did_key")?; + + let dsse = serde_json::from_str::(&dsse_json).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse dsse json: {e}"), + ) + })?; + + let bundle = map_anyhow(model_signing::create_model_signing_sigstore_bundle( + dsse, + &signer_did_key, + ))?; + let bundle_json = map_anyhow(serde_json::to_string(&bundle).map_err(Into::into))?; + + write_c_string( + out_sigstore_bundle_json, + bundle_json, + "out_sigstore_bundle_json", + ) + }) +} diff --git a/src/ffi/runtime.rs b/src/ffi/runtime.rs new file mode 100644 index 0000000..84183bc --- /dev/null +++ b/src/ffi/runtime.rs @@ -0,0 +1,46 @@ +use std::ffi::c_char; + +use crate::ffi::{ + error::{run_ffi, IgStatus}, + util::write_out_ptr, +}; + +pub struct IgRuntimeHandle { + pub(crate) runtime: tokio::runtime::Runtime, +} + +impl IgRuntimeHandle { + pub(crate) fn block_on(&self, fut: F) -> F::Output { + self.runtime.block_on(fut) + } +} + +#[no_mangle] +pub extern "C" fn ig_runtime_new( + out_runtime: *mut *mut IgRuntimeHandle, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .map_err(|e| { + crate::ffi::error::FfiError::new( + crate::ffi::error::IgStatus::RuntimeError, + format!("failed to initialize runtime: {e}"), + ) + })?; + write_out_ptr(out_runtime, IgRuntimeHandle { runtime }, "out_runtime") + }) +} + +#[no_mangle] +pub extern "C" fn ig_runtime_free(runtime: *mut IgRuntimeHandle) { + if runtime.is_null() { + return; + } + + unsafe { + drop(Box::from_raw(runtime)); + } +} diff --git a/src/ffi/signer.rs b/src/ffi/signer.rs new file mode 100644 index 0000000..c9621fe --- /dev/null +++ b/src/ffi/signer.rs @@ -0,0 +1,268 @@ +use std::{ffi::c_char, path::PathBuf}; + +use crate::{ + ffi::{ + error::{map_anyhow, run_ffi, FfiError, IgStatus}, + runtime::IgRuntimeHandle, + util::{ + as_ref, bytes_from_raw, cstr_to_string, optional_cstr_to_string, write_c_string, + write_out_ptr, + }, + }, + signer::{ + load_signer, save_signer, AkvConfig, AkvSigner, AuthServiceSigner, Ed25519Signer, + P256Signer, Secp256k1Signer, SignerType, VCompNotarySigner, YubiHsmSigner, + }, +}; + +pub struct IgSignerHandle { + pub(crate) signer: SignerType, +} + +fn write_signer( + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + signer: SignerType, +) -> Result<(), FfiError> { + let did = signer.get_did_doc().id; + write_out_ptr(out_signer, IgSignerHandle { signer }, "out_signer")?; + write_c_string(out_did, did, "out_did")?; + Ok(()) +} + +#[no_mangle] +pub extern "C" fn ig_signer_free(signer: *mut IgSignerHandle) { + if signer.is_null() { + return; + } + + unsafe { + drop(Box::from_raw(signer)); + } +} + +#[no_mangle] +pub extern "C" fn ig_signer_get_did( + signer: *const IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let signer = as_ref(signer, "signer")?; + let did = signer.signer.get_did_doc().id; + write_c_string(out_did, did, "out_did") + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_ed25519_create( + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let signer = map_anyhow(Ed25519Signer::create())?; + write_signer(out_signer, out_did, SignerType::ED25519(signer)) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_ed25519_import( + secret_key_ptr: *const u8, + secret_key_len: usize, + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + if secret_key_len != 32 { + return Err(FfiError::new( + IgStatus::InvalidInput, + format!("ed25519 secret key must be 32 bytes, got {secret_key_len}"), + )); + } + + let secret_key = bytes_from_raw(secret_key_ptr, secret_key_len, "secret_key_ptr")?; + let signer = map_anyhow(Ed25519Signer::import(&secret_key))?; + write_signer(out_signer, out_did, SignerType::ED25519(signer)) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_p256_create( + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let signer = map_anyhow(P256Signer::create())?; + write_signer(out_signer, out_did, SignerType::P256(signer)) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_p256_import( + secret_key_ptr: *const u8, + secret_key_len: usize, + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + if secret_key_len != 32 { + return Err(FfiError::new( + IgStatus::InvalidInput, + format!("p256 secret key must be 32 bytes, got {secret_key_len}"), + )); + } + + let secret_key = bytes_from_raw(secret_key_ptr, secret_key_len, "secret_key_ptr")?; + let signer = map_anyhow(P256Signer::import(&secret_key))?; + write_signer(out_signer, out_did, SignerType::P256(signer)) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_secp256k1_create( + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let signer = map_anyhow(Secp256k1Signer::create())?; + write_signer(out_signer, out_did, SignerType::SECP256K1(signer)) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_secp256k1_import( + secret_key_ptr: *const u8, + secret_key_len: usize, + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + if secret_key_len != 32 { + return Err(FfiError::new( + IgStatus::InvalidInput, + format!("secp256k1 secret key must be 32 bytes, got {secret_key_len}"), + )); + } + + let secret_key = bytes_from_raw(secret_key_ptr, secret_key_len, "secret_key_ptr")?; + let signer = map_anyhow(Secp256k1Signer::import(&secret_key))?; + write_signer(out_signer, out_did, SignerType::SECP256K1(signer)) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_auth_service_create( + runtime: *const IgRuntimeHandle, + api_key: *const c_char, + url: *const c_char, + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let api_key = cstr_to_string(api_key, "api_key")?; + let url = cstr_to_string(url, "url")?; + + let signer = map_anyhow(runtime.block_on(AuthServiceSigner::create(api_key, url)))?; + write_signer(out_signer, out_did, SignerType::AuthService(signer)) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_vcomp_notary_create( + runtime: *const IgRuntimeHandle, + url: *const c_char, + pub_key_hex_or_null: *const c_char, + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let url = cstr_to_string(url, "url")?; + let pub_key = optional_cstr_to_string(pub_key_hex_or_null)?; + + let signer = map_anyhow(runtime.block_on(VCompNotarySigner::create(&url, pub_key)))?; + write_signer(out_signer, out_did, SignerType::VCompNotarySigner(signer)) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_akv_create( + runtime: *const IgRuntimeHandle, + config_json: *const c_char, + key_name: *const c_char, + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let config_json = cstr_to_string(config_json, "config_json")?; + let key_name = cstr_to_string(key_name, "key_name")?; + let config = serde_json::from_str::(&config_json).map_err(|e| { + FfiError::new( + IgStatus::JsonError, + format!("failed to parse akv config json: {e}"), + ) + })?; + + let signer = map_anyhow(runtime.block_on(AkvSigner::create(&config, key_name)))?; + write_signer(out_signer, out_did, SignerType::AKV(signer)) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_yubihsm_create( + auth_key_id: u16, + signing_key_id: u16, + password: *const c_char, + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let password = cstr_to_string(password, "password")?; + + let signer = map_anyhow(YubiHsmSigner::create(auth_key_id, signing_key_id, password))?; + write_signer(out_signer, out_did, SignerType::YubiHsm2Signer(signer)) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_save( + signer: *const IgSignerHandle, + folder: *const c_char, + name: *const c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let signer = as_ref(signer, "signer")?; + let folder = cstr_to_string(folder, "folder")?; + let name = cstr_to_string(name, "name")?; + + map_anyhow(save_signer(&signer.signer, PathBuf::from(folder), &name))?; + Ok(()) + }) +} + +#[no_mangle] +pub extern "C" fn ig_signer_load( + signer_file: *const c_char, + out_signer: *mut *mut IgSignerHandle, + out_did: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let signer_file = cstr_to_string(signer_file, "signer_file")?; + let signer = map_anyhow(load_signer(PathBuf::from(signer_file)))?; + write_signer(out_signer, out_did, signer) + }) +} diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs new file mode 100644 index 0000000..1d6a8d1 --- /dev/null +++ b/src/ffi/tests.rs @@ -0,0 +1,411 @@ +use std::{ + ffi::{c_char, CStr, CString}, + ptr, +}; + +use serde_json::Value; + +use super::{ + blob_store, dsse, error::IgStatus, intoto, lineage_statements, model_signing, runtime, signer, + vc, IgBytes, +}; + +fn cstring(s: &str) -> CString { + CString::new(s).expect("test string has no NUL") +} + +fn take_owned_c_string(ptr: *mut c_char) -> String { + assert!(!ptr.is_null(), "expected non-null C string pointer"); + let s = unsafe { CStr::from_ptr(ptr) } + .to_str() + .expect("valid utf-8") + .to_owned(); + super::ig_string_free(ptr); + s +} + +fn assert_ok(status: IgStatus, err: *mut c_char) { + if status == IgStatus::Ok { + assert!(err.is_null(), "expected null err_out on success"); + return; + } + + let err_msg = if err.is_null() { + String::from("") + } else { + let s = unsafe { CStr::from_ptr(err) } + .to_str() + .expect("valid utf-8") + .to_owned(); + super::ig_error_free(err); + s + }; + + panic!("expected IgStatus::Ok, got {:?}: {}", status, err_msg); +} + +#[test] +fn ffi_runtime_signer_dsse_smoke() { + let mut runtime_handle = ptr::null_mut(); + let mut err_out = ptr::null_mut(); + let status = runtime::ig_runtime_new(&mut runtime_handle, &mut err_out); + assert_ok(status, err_out); + + let mut signer_handle = ptr::null_mut(); + let mut signer_did = ptr::null_mut(); + let status = + signer::ig_signer_ed25519_create(&mut signer_handle, &mut signer_did, &mut err_out); + assert_ok(status, err_out); + let signer_did = take_owned_c_string(signer_did); + assert!(signer_did.starts_with("did:key:")); + + let payload = b"hello ffi"; + let payload_type = cstring("application/vnd.in-toto+json"); + let mut envelope_json_ptr = ptr::null_mut(); + let status = dsse::ig_dsse_sign( + runtime_handle, + signer_handle, + payload.as_ptr(), + payload.len(), + payload_type.as_ptr(), + &mut envelope_json_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + + let envelope_json = take_owned_c_string(envelope_json_ptr); + let envelope: Value = serde_json::from_str(&envelope_json).expect("valid json envelope"); + assert_eq!( + envelope["payloadType"], + Value::String(String::from("application/vnd.in-toto+json")) + ); + assert!( + envelope["signatures"] + .as_array() + .unwrap_or(&Vec::new()) + .len() + == 1 + ); + + signer::ig_signer_free(signer_handle); + runtime::ig_runtime_free(runtime_handle); +} + +#[test] +fn ffi_vc_issue_and_verify_smoke() { + let mut runtime_handle = ptr::null_mut(); + let mut err_out = ptr::null_mut(); + let status = runtime::ig_runtime_new(&mut runtime_handle, &mut err_out); + assert_ok(status, err_out); + + let mut signer_handle = ptr::null_mut(); + let mut signer_did = ptr::null_mut(); + let status = + signer::ig_signer_ed25519_create(&mut signer_handle, &mut signer_did, &mut err_out); + assert_ok(status, err_out); + super::ig_string_free(signer_did); + + let subject = cstring("did:key:z6MksNPQf5wQwQfA2a5JY9xY8h6CZ9nHp4Y5qpc4kTYqN6xw"); + let mut vc_json_ptr = ptr::null_mut(); + let status = vc::ig_vc_issue( + runtime_handle, + signer_handle, + subject.as_ptr(), + &mut vc_json_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + + let vc_json = take_owned_c_string(vc_json_ptr); + let issued_vc: Value = serde_json::from_str(&vc_json).expect("valid issued vc json"); + assert!(issued_vc.get("proof").is_some()); + + let vc_json_c = cstring(&vc_json); + let mut verify_result_ptr = ptr::null_mut(); + let mut is_valid = false; + let status = vc::ig_vc_verify( + runtime_handle, + vc_json_c.as_ptr(), + &mut verify_result_ptr, + &mut is_valid, + &mut err_out, + ); + assert_ok(status, err_out); + + let verify_result = take_owned_c_string(verify_result_ptr); + assert!(verify_result.contains("VC verification result")); + assert!(is_valid); + + signer::ig_signer_free(signer_handle); + runtime::ig_runtime_free(runtime_handle); +} + +#[test] +fn ffi_blob_store_local_fs_roundtrip() { + let mut runtime_handle = ptr::null_mut(); + let mut err_out = ptr::null_mut(); + let status = runtime::ig_runtime_new(&mut runtime_handle, &mut err_out); + assert_ok(status, err_out); + + let tmp_dir = std::env::temp_dir().join(format!("integrity-ffi-test-{}", uuid::Uuid::new_v4())); + std::fs::create_dir_all(&tmp_dir).expect("create temp dir"); + let path = cstring(tmp_dir.to_string_lossy().as_ref()); + + let mut store_handle = ptr::null_mut(); + let status = blob_store::ig_blob_store_local_fs_new( + runtime_handle, + path.as_ptr(), + &mut store_handle, + &mut err_out, + ); + assert_ok(status, err_out); + + let blob = b"ffi blob"; + let mut cid_ptr = ptr::null_mut(); + let status = blob_store::ig_blob_store_put( + runtime_handle, + store_handle, + blob.as_ptr(), + blob.len(), + 0x55, + ptr::null(), + &mut cid_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + + let cid = take_owned_c_string(cid_ptr); + assert!(!cid.is_empty()); + let cid_c = cstring(&cid); + + let mut exists = false; + let status = blob_store::ig_blob_store_exists( + runtime_handle, + store_handle, + cid_c.as_ptr(), + &mut exists, + &mut err_out, + ); + assert_ok(status, err_out); + assert!(exists); + + let mut out_blob = IgBytes::default(); + let mut found = false; + let status = blob_store::ig_blob_store_get( + runtime_handle, + store_handle, + cid_c.as_ptr(), + &mut out_blob, + &mut found, + &mut err_out, + ); + assert_ok(status, err_out); + assert!(found); + + let roundtrip = unsafe { std::slice::from_raw_parts(out_blob.ptr, out_blob.len) }; + assert_eq!(roundtrip, blob); + super::ig_bytes_free(out_blob); + + blob_store::ig_blob_store_free(store_handle); + runtime::ig_runtime_free(runtime_handle); + let _ = std::fs::remove_dir_all(tmp_dir); +} + +#[test] +fn ffi_model_signing_and_intoto_digest_smoke() { + let mut runtime_handle = ptr::null_mut(); + let mut err_out = ptr::null_mut(); + let status = runtime::ig_runtime_new(&mut runtime_handle, &mut err_out); + assert_ok(status, err_out); + + let model_name = cstring("demo-model"); + let mut hashes = std::collections::HashMap::new(); + hashes.insert(String::from("weights.bin"), hex::encode([7_u8; 32])); + let hashes_json = cstring(&serde_json::to_string(&hashes).unwrap()); + + let mut statement_json_ptr = ptr::null_mut(); + let status = model_signing::ig_model_signing_create_intoto_statement_from_hashes( + runtime_handle, + model_name.as_ptr(), + hashes_json.as_ptr(), + false, + ptr::null(), + &mut statement_json_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + + let statement_json = take_owned_c_string(statement_json_ptr); + let statement: Value = serde_json::from_str(&statement_json).expect("valid statement json"); + assert!(statement.get("predicate").is_some()); + + let cid = cstring("bafkr4icb7a4uceploe5cefs4i3eqvohq7wjztsjafd6w2kejiszd75n7oy"); + let mut digest_json_ptr = ptr::null_mut(); + let status = + intoto::ig_intoto_digest_from_cid(cid.as_ptr(), &mut digest_json_ptr, &mut err_out); + assert_ok(status, err_out); + + let digest_json = take_owned_c_string(digest_json_ptr); + let digest_map: std::collections::HashMap = + serde_json::from_str(&digest_json).expect("valid digest json"); + assert_eq!( + digest_map.get("cid").map(String::as_str), + Some(cid.to_str().unwrap()) + ); + + runtime::ig_runtime_free(runtime_handle); +} + +#[test] +fn ffi_lineage_statement_create_and_utils_smoke() { + let mut runtime_handle = ptr::null_mut(); + let mut err_out = ptr::null_mut(); + let status = runtime::ig_runtime_new(&mut runtime_handle, &mut err_out); + assert_ok(status, err_out); + + let request = serde_json::json!({ + "subject": "bafkr4ibthuzk3zug7ghmx63yjqaiu6rx4hhfdv3453j5bodskgw57bx2ya", + "association": "baga6yaq6echz7kjzuhzubnsq2mqkw5oxpkrio5nwb4fibzkwaqke3hqbc25g4", + "registeredBy": "did:key:z6Mkw2PvzC9DHXiYQHMDRwyxCCV9n4EDc6vqqp1uyi9nrwsP", + "timestamp": "2025-01-01T00:00:00Z" + }); + let request_c = cstring(&request.to_string()); + + let mut statement_ptr = ptr::null_mut(); + let status = lineage_statements::ig_lineage_statement_create_association( + runtime_handle, + request_c.as_ptr(), + &mut statement_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + + let statement_json = take_owned_c_string(statement_ptr); + let statement: Value = serde_json::from_str(&statement_json).expect("valid statement json"); + let statement_type = statement + .get("@type") + .and_then(Value::as_str) + .expect("statement type"); + assert_eq!(statement_type, "AssociationRegistration"); + let statement_id = statement + .get("@id") + .and_then(Value::as_str) + .expect("statement id"); + + let statement_json_c = cstring(&statement_json); + + let mut cid_ptr = ptr::null_mut(); + let status = lineage_statements::ig_lineage_statement_compute_cid( + runtime_handle, + statement_json_c.as_ptr(), + &mut cid_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + let computed_id = take_owned_c_string(cid_ptr); + assert_eq!(computed_id, statement_id); + + let mut extracted_id_ptr = ptr::null_mut(); + let status = lineage_statements::ig_lineage_statement_extract_id( + statement_json_c.as_ptr(), + &mut extracted_id_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + let extracted_id = take_owned_c_string(extracted_id_ptr); + assert_eq!(extracted_id, statement_id); + + let mut extracted_type_ptr = ptr::null_mut(); + let status = lineage_statements::ig_lineage_statement_extract_type( + statement_json_c.as_ptr(), + &mut extracted_type_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + let extracted_type = take_owned_c_string(extracted_type_ptr); + assert_eq!(extracted_type, "AssociationRegistration"); + + let mut filename_ptr = ptr::null_mut(); + let status = lineage_statements::ig_lineage_statement_jsonld_filename( + statement_json_c.as_ptr(), + &mut filename_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + let filename = take_owned_c_string(filename_ptr); + assert!(filename.ends_with(".jsonld")); + + let mut refs_ptr = ptr::null_mut(); + let status = lineage_statements::ig_lineage_statement_referenced_cids_json( + statement_json_c.as_ptr(), + &mut refs_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + let refs_json = take_owned_c_string(refs_ptr); + let refs: Vec = serde_json::from_str(&refs_json).expect("valid refs"); + assert_eq!(refs.len(), 2); + + let mut registered_by_ptr = ptr::null_mut(); + let status = lineage_statements::ig_lineage_statement_registered_by( + statement_json_c.as_ptr(), + &mut registered_by_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + let registered_by = take_owned_c_string(registered_by_ptr); + assert_eq!( + registered_by, + "did:key:z6Mkw2PvzC9DHXiYQHMDRwyxCCV9n4EDc6vqqp1uyi9nrwsP" + ); + + runtime::ig_runtime_free(runtime_handle); +} + +#[test] +fn ffi_lineage_statement_create_did_regular_smoke() { + let mut runtime_handle = ptr::null_mut(); + let mut err_out = ptr::null_mut(); + let status = runtime::ig_runtime_new(&mut runtime_handle, &mut err_out); + assert_ok(status, err_out); + + let request = serde_json::json!({ + "did": "did:key:z6Mkw2PvzC9DHXiYQHMDRwyxCCV9n4EDc6vqqp1uyi9nrwsP", + "registeredBy": "did:key:z6Mkw2PvzC9DHXiYQHMDRwyxCCV9n4EDc6vqqp1uyi9nrwsP", + "timestamp": "2025-01-01T00:00:00Z" + }); + let request_c = cstring(&request.to_string()); + + let mut statement_ptr = ptr::null_mut(); + let status = lineage_statements::ig_lineage_statement_create_did_regular( + runtime_handle, + request_c.as_ptr(), + &mut statement_ptr, + &mut err_out, + ); + assert_ok(status, err_out); + + let statement_json = take_owned_c_string(statement_ptr); + let statement: Value = serde_json::from_str(&statement_json).expect("valid statement json"); + let statement_type = statement + .get("@type") + .and_then(Value::as_str) + .expect("statement type"); + assert_eq!(statement_type, "DidRegistration"); + + runtime::ig_runtime_free(runtime_handle); +} + +#[test] +fn ffi_versions_smoke() { + assert_eq!(super::version::ig_abi_version_major(), 0); + assert_eq!(super::version::ig_abi_version_minor(), 2); + + let mut err_out = ptr::null_mut(); + let mut version_ptr = ptr::null_mut(); + let status = super::version::ig_abi_version_string(&mut version_ptr, &mut err_out); + assert_ok(status, err_out); + + let version = take_owned_c_string(version_ptr); + assert_eq!(version, "0.2.0"); +} diff --git a/src/ffi/util.rs b/src/ffi/util.rs new file mode 100644 index 0000000..1029c9d --- /dev/null +++ b/src/ffi/util.rs @@ -0,0 +1,148 @@ +use std::ffi::{c_char, CStr, CString}; + +use crate::ffi::{ + error::{FfiError, FfiResult, IgStatus}, + IgBytes, +}; + +pub(crate) fn as_ref<'a, T>(ptr: *const T, name: &str) -> FfiResult<&'a T> { + if ptr.is_null() { + return Err(FfiError::new( + IgStatus::NullPointer, + format!("{name} is a null pointer"), + )); + } + + Ok(unsafe { &*ptr }) +} + +pub(crate) fn as_mut<'a, T>(ptr: *mut T, name: &str) -> FfiResult<&'a mut T> { + if ptr.is_null() { + return Err(FfiError::new( + IgStatus::NullPointer, + format!("{name} is a null pointer"), + )); + } + + Ok(unsafe { &mut *ptr }) +} + +pub(crate) fn cstr_to_string(ptr: *const c_char, name: &str) -> FfiResult { + if ptr.is_null() { + return Err(FfiError::new( + IgStatus::NullPointer, + format!("{name} is a null pointer"), + )); + } + + let s = unsafe { CStr::from_ptr(ptr) } + .to_str() + .map_err(|e| FfiError::new(IgStatus::Utf8Error, format!("invalid utf-8 in {name}: {e}")))? + .to_owned(); + + Ok(s) +} + +pub(crate) fn optional_cstr_to_string(ptr: *const c_char) -> FfiResult> { + if ptr.is_null() { + return Ok(None); + } + + let s = unsafe { CStr::from_ptr(ptr) } + .to_str() + .map_err(|e| FfiError::new(IgStatus::Utf8Error, format!("invalid utf-8: {e}")))? + .to_owned(); + + Ok(Some(s)) +} + +pub(crate) fn bytes_from_raw(ptr: *const u8, len: usize, name: &str) -> FfiResult> { + if ptr.is_null() { + return if len == 0 { + Ok(Vec::new()) + } else { + Err(FfiError::new( + IgStatus::NullPointer, + format!("{name} is a null pointer"), + )) + }; + } + + let slice = unsafe { std::slice::from_raw_parts(ptr, len) }; + Ok(slice.to_vec()) +} + +pub(crate) fn write_c_string(out: *mut *mut c_char, value: String, name: &str) -> FfiResult<()> { + if out.is_null() { + return Err(FfiError::new( + IgStatus::NullPointer, + format!("{name} is a null pointer"), + )); + } + + let value = value.replace('\0', "\\0"); + let c_value = CString::new(value).map_err(|e| { + FfiError::new( + IgStatus::Utf8Error, + format!("failed to encode {name} as C string: {e}"), + ) + })?; + + unsafe { + *out = c_value.into_raw(); + } + + Ok(()) +} + +pub(crate) fn write_out_ptr(out: *mut *mut T, value: T, name: &str) -> FfiResult<()> { + if out.is_null() { + return Err(FfiError::new( + IgStatus::NullPointer, + format!("{name} is a null pointer"), + )); + } + + let boxed = Box::new(value); + unsafe { + *out = Box::into_raw(boxed); + } + + Ok(()) +} + +pub(crate) fn write_bool(out: *mut bool, value: bool, name: &str) -> FfiResult<()> { + if out.is_null() { + return Err(FfiError::new( + IgStatus::NullPointer, + format!("{name} is a null pointer"), + )); + } + + unsafe { + *out = value; + } + + Ok(()) +} + +pub(crate) fn write_ig_bytes(out: *mut IgBytes, mut value: Vec, name: &str) -> FfiResult<()> { + if out.is_null() { + return Err(FfiError::new( + IgStatus::NullPointer, + format!("{name} is a null pointer"), + )); + } + + let bytes = IgBytes { + ptr: value.as_mut_ptr(), + len: value.len(), + }; + std::mem::forget(value); + + unsafe { + *out = bytes; + } + + Ok(()) +} diff --git a/src/ffi/vc.rs b/src/ffi/vc.rs new file mode 100644 index 0000000..5c88833 --- /dev/null +++ b/src/ffi/vc.rs @@ -0,0 +1,84 @@ +use std::ffi::c_char; + +use ssi::vc::Credential; + +use crate::{ + ffi::{ + error::{map_anyhow, run_ffi, FfiError, IgStatus}, + runtime::IgRuntimeHandle, + signer::IgSignerHandle, + util::{as_ref, cstr_to_string, write_bool, write_c_string}, + }, + vc, +}; + +#[no_mangle] +pub extern "C" fn ig_vc_issue( + runtime: *const IgRuntimeHandle, + signer: *const IgSignerHandle, + subject: *const c_char, + out_credential_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let signer = as_ref(signer, "signer")?; + let subject = cstr_to_string(subject, "subject")?; + + let credential = + map_anyhow(runtime.block_on(vc::issue_vc(&subject, signer.signer.clone())))?; + let credential_json = map_anyhow(serde_json::to_string(&credential).map_err(Into::into))?; + + write_c_string(out_credential_json, credential_json, "out_credential_json") + }) +} + +#[no_mangle] +pub extern "C" fn ig_vc_sign( + runtime: *const IgRuntimeHandle, + signer: *const IgSignerHandle, + unsigned_credential_json: *const c_char, + out_signed_credential_json: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let signer = as_ref(signer, "signer")?; + let unsigned_credential_json = + cstr_to_string(unsigned_credential_json, "unsigned_credential_json")?; + + let unsigned_credential = Credential::from_json_unsigned(&unsigned_credential_json) + .map_err(|e| { + FfiError::new(IgStatus::InvalidInput, format!("invalid unsigned vc: {e}")) + })?; + + let signed = + map_anyhow(runtime.block_on(vc::sign_vc(&unsigned_credential, signer.signer.clone())))?; + let signed_json = map_anyhow(serde_json::to_string(&signed).map_err(Into::into))?; + + write_c_string( + out_signed_credential_json, + signed_json, + "out_signed_credential_json", + ) + }) +} + +#[no_mangle] +pub extern "C" fn ig_vc_verify( + runtime: *const IgRuntimeHandle, + credential_json: *const c_char, + out_verify_result_json: *mut *mut c_char, + out_valid: *mut bool, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + let runtime = as_ref(runtime, "runtime")?; + let credential_json = cstr_to_string(credential_json, "credential_json")?; + + let result = map_anyhow(runtime.block_on(vc::verify_vc(&credential_json)))?; + write_c_string(out_verify_result_json, result, "out_verify_result_json")?; + write_bool(out_valid, true, "out_valid")?; + Ok(()) + }) +} diff --git a/src/ffi/version.rs b/src/ffi/version.rs new file mode 100644 index 0000000..2fe3bb7 --- /dev/null +++ b/src/ffi/version.rs @@ -0,0 +1,56 @@ +use std::ffi::c_char; + +use crate::ffi::{ + error::{run_ffi, IgStatus}, + util::write_c_string, +}; + +const ABI_VERSION_MAJOR: u32 = 0; +const ABI_VERSION_MINOR: u32 = 2; +const ABI_VERSION_PATCH: u32 = 0; + +#[no_mangle] +pub extern "C" fn ig_abi_version_major() -> u32 { + ABI_VERSION_MAJOR +} + +#[no_mangle] +pub extern "C" fn ig_abi_version_minor() -> u32 { + ABI_VERSION_MINOR +} + +#[no_mangle] +pub extern "C" fn ig_abi_version_patch() -> u32 { + ABI_VERSION_PATCH +} + +#[no_mangle] +pub extern "C" fn ig_abi_version_string( + out_version: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + write_c_string( + out_version, + format!( + "{}.{}.{}", + ABI_VERSION_MAJOR, ABI_VERSION_MINOR, ABI_VERSION_PATCH + ), + "out_version", + ) + }) +} + +#[no_mangle] +pub extern "C" fn ig_core_crate_version( + out_version: *mut *mut c_char, + err_out: *mut *mut c_char, +) -> IgStatus { + run_ffi(err_out, || { + write_c_string( + out_version, + env!("CARGO_PKG_VERSION").to_string(), + "out_version", + ) + }) +} diff --git a/src/lib.rs b/src/lib.rs index dd60d70..ec0612a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,10 @@ pub mod cid; #[cfg(not(target_arch = "wasm32"))] pub mod dsse; +/// C ABI / FFI bridge for external SDKs +#[cfg(not(target_arch = "wasm32"))] +pub mod ffi; + /// In-Toto attestation format support #[cfg(not(target_arch = "wasm32"))] pub mod intoto_attestation; From 1a84932c5833c860d75ae813dbab71be32805c30 Mon Sep 17 00:00:00 2001 From: captjt Date: Tue, 10 Feb 2026 08:30:49 -0500 Subject: [PATCH 2/8] fix: cargo fmt --check --- src/ffi/lineage_statements.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ffi/lineage_statements.rs b/src/ffi/lineage_statements.rs index a2a6875..930061a 100644 --- a/src/ffi/lineage_statements.rs +++ b/src/ffi/lineage_statements.rs @@ -1,7 +1,6 @@ use std::ffi::c_char; -use serde::de::DeserializeOwned; -use serde::Deserialize; +use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use ssi::vc::Credential; From ca7035f25c4ccbb0296efa671dca518b353b8f05 Mon Sep 17 00:00:00 2001 From: captjt Date: Tue, 10 Feb 2026 10:28:37 -0500 Subject: [PATCH 3/8] ci: rerun fmt-check From 3724f26c8d274ef7aab9fb6c27198a1fd19b388e Mon Sep 17 00:00:00 2001 From: captjt Date: Tue, 10 Feb 2026 10:38:23 -0500 Subject: [PATCH 4/8] fix: linting unsafe errors --- src/ffi/mod.rs | 20 ++++++++++++++++---- src/ffi/tests.rs | 16 ++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index 1d67653..dd5d9b3 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -28,8 +28,11 @@ pub struct IgBytes { pub len: usize, } +/// # Safety +/// `s` must be a pointer returned by this library via `CString::into_raw` +/// and must not have been freed previously. #[no_mangle] -pub extern "C" fn ig_string_free(s: *mut c_char) { +pub unsafe extern "C" fn ig_string_free(s: *mut c_char) { if s.is_null() { return; } @@ -39,13 +42,22 @@ pub extern "C" fn ig_string_free(s: *mut c_char) { } } +/// # Safety +/// `err` must be a pointer returned by this library via `CString::into_raw` +/// and must not have been freed previously. #[no_mangle] -pub extern "C" fn ig_error_free(err: *mut c_char) { - ig_string_free(err); +pub unsafe extern "C" fn ig_error_free(err: *mut c_char) { + unsafe { + ig_string_free(err); + } } +/// # Safety +/// `bytes.ptr` must point to a heap allocation created by this library, with +/// an allocation capacity equal to `bytes.len`, and must not be freed +/// previously. #[no_mangle] -pub extern "C" fn ig_bytes_free(bytes: IgBytes) { +pub unsafe extern "C" fn ig_bytes_free(bytes: IgBytes) { if bytes.ptr.is_null() { return; } diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index 1d6a8d1..a3f5417 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -20,7 +20,9 @@ fn take_owned_c_string(ptr: *mut c_char) -> String { .to_str() .expect("valid utf-8") .to_owned(); - super::ig_string_free(ptr); + unsafe { + super::ig_string_free(ptr); + } s } @@ -37,7 +39,9 @@ fn assert_ok(status: IgStatus, err: *mut c_char) { .to_str() .expect("valid utf-8") .to_owned(); - super::ig_error_free(err); + unsafe { + super::ig_error_free(err); + } s }; @@ -103,7 +107,9 @@ fn ffi_vc_issue_and_verify_smoke() { let status = signer::ig_signer_ed25519_create(&mut signer_handle, &mut signer_did, &mut err_out); assert_ok(status, err_out); - super::ig_string_free(signer_did); + unsafe { + super::ig_string_free(signer_did); + } let subject = cstring("did:key:z6MksNPQf5wQwQfA2a5JY9xY8h6CZ9nHp4Y5qpc4kTYqN6xw"); let mut vc_json_ptr = ptr::null_mut(); @@ -204,7 +210,9 @@ fn ffi_blob_store_local_fs_roundtrip() { let roundtrip = unsafe { std::slice::from_raw_parts(out_blob.ptr, out_blob.len) }; assert_eq!(roundtrip, blob); - super::ig_bytes_free(out_blob); + unsafe { + super::ig_bytes_free(out_blob); + } blob_store::ig_blob_store_free(store_handle); runtime::ig_runtime_free(runtime_handle); From fa9bd6c72cc73484b6f79111250a6324b23564b5 Mon Sep 17 00:00:00 2001 From: captjt Date: Tue, 10 Feb 2026 10:49:57 -0500 Subject: [PATCH 5/8] fix: lint-docs errors --- src/ffi/blob_store.rs | 1 + src/ffi/error.rs | 10 ++++++++++ src/ffi/mod.rs | 6 ++++++ src/ffi/runtime.rs | 1 + src/ffi/signer.rs | 1 + 5 files changed, 19 insertions(+) diff --git a/src/ffi/blob_store.rs b/src/ffi/blob_store.rs index 85a8b2d..16ec5c4 100644 --- a/src/ffi/blob_store.rs +++ b/src/ffi/blob_store.rs @@ -13,6 +13,7 @@ use crate::{ }, }; +/// Opaque handle to a configured blob store instance for FFI consumers. pub struct IgBlobStoreHandle { pub(crate) store: Arc, } diff --git a/src/ffi/error.rs b/src/ffi/error.rs index e0530c1..f385f7a 100644 --- a/src/ffi/error.rs +++ b/src/ffi/error.rs @@ -6,15 +6,25 @@ use std::{ #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Status codes returned by FFI entrypoints. pub enum IgStatus { + /// Operation completed successfully. Ok = 0, + /// Input value failed validation. InvalidInput = 1, + /// Required pointer input was null. NullPointer = 2, + /// UTF-8 or string encoding/decoding error. Utf8Error = 3, + /// JSON parsing or serialization error. JsonError = 4, + /// Signature or proof verification failed. VerificationFailed = 5, + /// Requested operation is not supported in the current build/runtime. NotSupported = 6, + /// Runtime subsystem initialization or execution error. RuntimeError = 7, + /// Unexpected internal failure. InternalError = 255, } diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index dd5d9b3..01c25b7 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -21,10 +21,16 @@ pub use signer::IgSignerHandle; #[cfg(test)] mod tests; +/// Owned byte buffer returned across the C ABI boundary. +/// +/// Memory in `ptr` is allocated by this library and must be released with +/// [`ig_bytes_free`]. #[repr(C)] #[derive(Debug, Clone, Copy, Default)] pub struct IgBytes { + /// Pointer to the first byte of the buffer, or null when empty. pub ptr: *mut u8, + /// Number of valid bytes at `ptr`. pub len: usize, } diff --git a/src/ffi/runtime.rs b/src/ffi/runtime.rs index 84183bc..642b59c 100644 --- a/src/ffi/runtime.rs +++ b/src/ffi/runtime.rs @@ -5,6 +5,7 @@ use crate::ffi::{ util::write_out_ptr, }; +/// Opaque handle to the Tokio runtime used by asynchronous FFI operations. pub struct IgRuntimeHandle { pub(crate) runtime: tokio::runtime::Runtime, } diff --git a/src/ffi/signer.rs b/src/ffi/signer.rs index c9621fe..0ac726e 100644 --- a/src/ffi/signer.rs +++ b/src/ffi/signer.rs @@ -15,6 +15,7 @@ use crate::{ }, }; +/// Opaque handle to a signer implementation used by FFI consumers. pub struct IgSignerHandle { pub(crate) signer: SignerType, } From 787775695f55993e3dece8850569d61609d1eade Mon Sep 17 00:00:00 2001 From: captjt Date: Tue, 10 Feb 2026 11:05:45 -0500 Subject: [PATCH 6/8] ci: add dry-run flag for FFI release workflow --- .github/workflows/release-native-ffi.yml | 43 +++++++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-native-ffi.yml b/.github/workflows/release-native-ffi.yml index 668e762..e0c82db 100644 --- a/.github/workflows/release-native-ffi.yml +++ b/.github/workflows/release-native-ffi.yml @@ -5,6 +5,12 @@ on: tags: - "v*" workflow_dispatch: + inputs: + dry_run: + description: "Build and package release artifacts without publishing a GitHub release" + required: true + default: true + type: boolean permissions: contents: write @@ -100,11 +106,10 @@ jobs: path: dist/integrity-ffi-${{ matrix.asset_suffix }} if-no-files-found: error - publish-release: - name: Publish GitHub Release Assets + package: + name: Package Release Bundles needs: build runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') steps: - name: Download all build artifacts uses: actions/download-artifact@v4 @@ -126,9 +131,37 @@ jobs: sha256sum release/*.tar.gz > release/SHA256SUMS.txt + - name: Upload packaged release artifacts + uses: actions/upload-artifact@v4 + with: + name: integrity-ffi-release-bundles + path: | + release/*.tar.gz + release/SHA256SUMS.txt + if-no-files-found: error + + - name: Dry-run summary + if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} + run: | + echo "Dry-run enabled: packaged artifacts were uploaded and no GitHub release will be created." + + publish-release: + name: Publish GitHub Release Assets + needs: package + runs-on: ubuntu-latest + if: >- + startsWith(github.ref, 'refs/tags/') && + !(github.event_name == 'workflow_dispatch' && inputs.dry_run) + steps: + - name: Download packaged release artifacts + uses: actions/download-artifact@v4 + with: + path: release + name: integrity-ffi-release-bundles + - name: Upload release assets uses: softprops/action-gh-release@v2 with: files: | - release/*.tar.gz - release/SHA256SUMS.txt + release/**/*.tar.gz + release/**/SHA256SUMS.txt From 77ebf5622a883fea950196f795c892139865d246 Mon Sep 17 00:00:00 2001 From: captjt Date: Tue, 10 Feb 2026 13:05:54 -0500 Subject: [PATCH 7/8] feat: hide ffi behind flag --- .github/workflows/ci-check.yml | 3 +++ Cargo.toml | 1 + README.md | 13 ++++++++++++- src/lib.rs | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 8c5d466..29276b8 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -47,6 +47,9 @@ jobs: - name: build run: nix develop . -c just build + - name: build-ffi-feature + run: nix develop . -c cargo check --locked --features ffi + - name: build-wasm run: nix develop . -c just build-wasm diff --git a/Cargo.toml b/Cargo.toml index a018270..e7d9307 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ crate-type = ["rlib", "cdylib"] [features] default = [] +ffi = [] s3 = [] tokio-tests = [] diff --git a/README.md b/README.md index 9e64e30..5ae99b7 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ The schema is defined in ### Usage The Integrity Graph common context is referenced in code via: + ```rust use integrity::json_ld::ig_common_context_link; @@ -33,6 +34,7 @@ let context_urn = ig_common_context_link(); ``` These contexts are embedded at compile time and used by the JSON-LD processor to: + - Expand compact JSON-LD documents to their canonical form - Resolve context references without network requests - Ensure deterministic content addressing of linked data @@ -40,6 +42,7 @@ These contexts are embedded at compile time and used by the JSON-LD processor to ### Regenerating Contexts To update the static contexts (e.g., after schema changes): + ```bash just update-static-contexts ``` @@ -48,7 +51,8 @@ This downloads the latest W3C contexts and regenerates the CID-indexed files. ## FFI (C ABI) -The crate now includes a stable C ABI surface in `src/ffi/` for SDK bindings (including the planned Go SDK). +The crate includes a stable C ABI surface in `src/ffi/` for SDK bindings (including the Go SDK). +FFI is feature-gated and enabled with `--features ffi`. - Public header: `include/integrity_ffi.h` - ABI version functions: @@ -67,6 +71,7 @@ The current ABI version is `0.2.0`. ### Native Artifact Releases GitHub Actions can publish prebuilt native FFI artifacts for each supported system: + - Linux x86_64 (`libintegrity.so`) - macOS 13 x86_64 (`libintegrity.dylib`) - macOS 14 aarch64 (`libintegrity.dylib`) @@ -79,6 +84,12 @@ Workflow: `.github/workflows/release-native-ffi.yml` - Push a version tag like `v0.2.0` to build and attach release assets to that GitHub Release. - Use `workflow_dispatch` to run the build matrix and collect workflow artifacts without publishing a Release. +Build native FFI artifacts locally: + +```bash +cargo build --release --locked --features ffi +``` + # Development Nix flake creates a dev environment with all the dependencies. diff --git a/src/lib.rs b/src/lib.rs index ec0612a..084cc07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ pub mod cid; pub mod dsse; /// C ABI / FFI bridge for external SDKs -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "ffi"))] pub mod ffi; /// In-Toto attestation format support From 40c75bd8cff197489805ad043d7b0075fe1c48ad Mon Sep 17 00:00:00 2001 From: Tyler Date: Tue, 10 Feb 2026 12:56:08 -0700 Subject: [PATCH 8/8] run workflow from nix shell --- .github/workflows/release-native-ffi.yml | 44 +++++++++++++++++++----- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-native-ffi.yml b/.github/workflows/release-native-ffi.yml index e0c82db..b8c8632 100644 --- a/.github/workflows/release-native-ffi.yml +++ b/.github/workflows/release-native-ffi.yml @@ -61,22 +61,32 @@ jobs: import_lib_candidates: "target/release/integrity.lib target/release/integrity.dll.lib target/release/libintegrity.dll.a" steps: + - name: Install Nix + uses: eqtylab-actions/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Cachix + uses: eqtylab-actions/cachix-action@v14 + with: + name: eqtylab + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipPush: true + continue-on-error: true + - name: Checkout repo uses: eqtylab-actions/checkout@v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - name: Rust cache uses: eqtylab-actions/rust-cache@v2 - name: Build release cdylib - run: cargo build --release --locked + run: nix develop . -c cargo build --release --locked - name: Stage artifact files shell: bash run: | - set -euxo pipefail + nix develop . -c bash -euxo pipefail < "${out_dir}/BUILD_INFO.txt" < "${out_dir}/BUILD_INFO.txt" < release/SHA256SUMS.txt + SCRIPT_EOF - name: Upload packaged release artifacts uses: actions/upload-artifact@v4