Skip to main content

hide_ffi/
lib.rs

1//! C ABI for HIDE. Every language binding calls this; none reimplements the
2//! cryptography, so there is exactly one implementation to review.
3//!
4//! Rules this boundary keeps:
5//!
6//! - Nothing crossing the boundary is trusted: every pointer is checked for
7//!   null and every length is validated before use.
8//! - Secret keys never leave Rust. Callers hold an opaque handle; there is no
9//!   function that exports key material.
10//! - Buffers allocated here are freed here (`hide_buffer_free`), because a
11//!   caller freeing Rust memory with libc `free` is undefined behaviour.
12//! - No function unwinds across the boundary. A panic in Rust crossing into C
13//!   is undefined behaviour, so every entry point catches it.
14
15use std::{
16    ffi::{CStr, c_char},
17    panic::{AssertUnwindSafe, catch_unwind},
18    ptr, slice,
19};
20
21use hide_crypto::{RecipientPublic, RecipientSecret};
22use hide_keyring::KeyFormat;
23use hide_object::Metadata;
24use zeroize::Zeroizing;
25
26/// Status codes. Zero is success; everything else is a failure the caller must
27/// handle. Values are stable across versions: bindings switch on them.
28pub const HIDE_OK: i32 = 0;
29pub const HIDE_ERR_INVALID_ARGUMENT: i32 = 1;
30pub const HIDE_ERR_WRONG_PASSPHRASE: i32 = 2;
31pub const HIDE_ERR_NOT_A_KEY: i32 = 3;
32pub const HIDE_ERR_AUTHENTICATION: i32 = 4;
33pub const HIDE_ERR_NO_MATCHING_RECIPIENT: i32 = 5;
34pub const HIDE_ERR_MALFORMED: i32 = 6;
35pub const HIDE_ERR_TOO_LARGE: i32 = 7;
36pub const HIDE_ERR_PANIC: i32 = 98;
37pub const HIDE_ERR_INTERNAL: i32 = 99;
38
39/// Key file kinds reported by [`hide_inspect_key`].
40pub const HIDE_KEY_RAW: i32 = 0;
41pub const HIDE_KEY_PROTECTED: i32 = 1;
42
43pub const HIDE_PUBLIC_KEY_LEN: usize = 1216;
44pub const HIDE_MIN_PASSPHRASE_LEN: usize = hide_keyring::MIN_PASSPHRASE_LEN;
45
46/// Bounds every allocation driven by a caller-supplied length.
47const MAX_INPUT: usize = 1 << 30;
48const MAX_KEY_FILE: usize = 4096;
49
50/// An owned buffer handed to the caller. Free it with [`hide_buffer_free`].
51#[repr(C)]
52pub struct HideBuffer {
53    pub data: *mut u8,
54    pub len: usize,
55    /// Kept so the exact original allocation can be reconstructed on free.
56    capacity: usize,
57}
58
59impl HideBuffer {
60    fn from_vec(mut bytes: Vec<u8>) -> Self {
61        let buffer = Self {
62            data: bytes.as_mut_ptr(),
63            len: bytes.len(),
64            capacity: bytes.capacity(),
65        };
66        std::mem::forget(bytes);
67        buffer
68    }
69}
70
71/// An empty buffer, for initialising a local before passing its address in.
72/// Callers must start from this rather than from uninitialised memory, because
73/// [`hide_buffer_free`] reads the pointer it is given.
74#[unsafe(no_mangle)]
75pub extern "C" fn hide_buffer_empty() -> HideBuffer {
76    HideBuffer {
77        data: ptr::null_mut(),
78        len: 0,
79        capacity: 0,
80    }
81}
82
83/// An opaque secret key. The caller only ever holds this pointer; there is no
84/// accessor that returns the underlying bytes.
85pub struct HideSecretKey(RecipientSecret);
86
87/// Reads a caller-provided slice, refusing null and absurd lengths.
88///
89/// # Safety
90/// `data` must be valid for `len` bytes, or null when `len` is zero.
91unsafe fn borrow<'a>(data: *const u8, len: usize, limit: usize) -> Option<&'a [u8]> {
92    if len > limit {
93        return None;
94    }
95    if len == 0 {
96        return Some(&[]);
97    }
98    if data.is_null() {
99        return None;
100    }
101    Some(unsafe { slice::from_raw_parts(data, len) })
102}
103
104/// # Safety
105/// `text` must be null or a valid NUL-terminated C string.
106unsafe fn borrow_str<'a>(text: *const c_char) -> Option<Option<&'a str>> {
107    if text.is_null() {
108        return Some(None);
109    }
110    match unsafe { CStr::from_ptr(text) }.to_str() {
111        Ok(value) => Some(Some(value)),
112        Err(_) => None,
113    }
114}
115
116fn object_error_code(error: &hide_object::ObjectError) -> i32 {
117    use hide_object::ObjectError;
118    match error {
119        ObjectError::NoMatchingRecipient => HIDE_ERR_NO_MATCHING_RECIPIENT,
120        ObjectError::Crypto(_) => HIDE_ERR_AUTHENTICATION,
121        ObjectError::ObjectTooLarge => HIDE_ERR_TOO_LARGE,
122        _ => HIDE_ERR_MALFORMED,
123    }
124}
125
126fn keyring_error_code(error: &hide_keyring::KeyringError) -> i32 {
127    use hide_keyring::KeyringError;
128    match error {
129        KeyringError::WrongPassphrase => HIDE_ERR_WRONG_PASSPHRASE,
130        KeyringError::NotAKeyFile | KeyringError::UnsupportedVersion(_) => HIDE_ERR_NOT_A_KEY,
131        KeyringError::PassphraseTooShort(_) => HIDE_ERR_INVALID_ARGUMENT,
132        _ => HIDE_ERR_MALFORMED,
133    }
134}
135
136/// Runs `body`, converting a panic into a status code rather than unwinding
137/// into C, which would be undefined behaviour.
138fn guard(body: impl FnOnce() -> i32) -> i32 {
139    catch_unwind(AssertUnwindSafe(body)).unwrap_or(HIDE_ERR_PANIC)
140}
141
142/// Human-readable text for a status code. The returned string is static and
143/// must not be freed.
144#[unsafe(no_mangle)]
145pub extern "C" fn hide_error_message(code: i32) -> *const c_char {
146    let text: &[u8] = match code {
147        HIDE_OK => b"success\0",
148        HIDE_ERR_INVALID_ARGUMENT => b"invalid argument\0",
149        HIDE_ERR_WRONG_PASSPHRASE => b"incorrect passphrase, or the key file was modified\0",
150        HIDE_ERR_NOT_A_KEY => b"not a HIDE key file\0",
151        HIDE_ERR_AUTHENTICATION => b"authentication failed; the data was altered\0",
152        HIDE_ERR_NO_MATCHING_RECIPIENT => b"no matching recipient for this key\0",
153        HIDE_ERR_MALFORMED => b"malformed or corrupt input\0",
154        HIDE_ERR_TOO_LARGE => b"input is too large\0",
155        HIDE_ERR_CHALLENGE_EXPIRED => b"the challenge expired before it was answered\0",
156        HIDE_ERR_CHALLENGE_REPLAYED => b"this challenge was already answered\0",
157        HIDE_ERR_PANIC => b"internal error (panic)\0",
158        _ => b"internal error\0",
159    };
160    text.as_ptr().cast()
161}
162
163/// The library version, as a static NUL-terminated string.
164#[unsafe(no_mangle)]
165pub extern "C" fn hide_version() -> *const c_char {
166    concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr().cast()
167}
168
169/// Frees a buffer produced by this library.
170///
171/// # Safety
172/// `buffer` must come from this library and must not be freed twice.
173#[unsafe(no_mangle)]
174pub unsafe extern "C" fn hide_buffer_free(buffer: *mut HideBuffer) {
175    if buffer.is_null() {
176        return;
177    }
178    let buffer = unsafe { &mut *buffer };
179    if buffer.data.is_null() {
180        return;
181    }
182    // Reconstruct the exact allocation, then zeroize: a freed buffer may still
183    // hold plaintext.
184    let mut bytes = unsafe { Vec::from_raw_parts(buffer.data, buffer.len, buffer.capacity) };
185    bytes.iter_mut().for_each(|byte| *byte = 0);
186    drop(bytes);
187    buffer.data = ptr::null_mut();
188    buffer.len = 0;
189    buffer.capacity = 0;
190}
191
192/// Generates a key pair. The secret is returned as an opaque handle; the public
193/// key is returned as bytes, which are safe to share.
194///
195/// # Safety
196/// Both out-pointers must be valid and non-null.
197#[unsafe(no_mangle)]
198pub unsafe extern "C" fn hide_keypair_generate(
199    out_secret: *mut *mut HideSecretKey,
200    out_public: *mut HideBuffer,
201) -> i32 {
202    guard(|| {
203        if out_secret.is_null() || out_public.is_null() {
204            return HIDE_ERR_INVALID_ARGUMENT;
205        }
206        let Ok(secret) = RecipientSecret::generate() else {
207            return HIDE_ERR_INTERNAL;
208        };
209        let Ok(public) = secret.public_key() else {
210            return HIDE_ERR_INTERNAL;
211        };
212        unsafe {
213            *out_public = HideBuffer::from_vec(public.to_bytes());
214            *out_secret = Box::into_raw(Box::new(HideSecretKey(secret)));
215        }
216        HIDE_OK
217    })
218}
219
220/// Reports whether a key file is raw or passphrase-protected, without needing
221/// the passphrase.
222///
223/// # Safety
224/// `data` must be valid for `len` bytes.
225#[unsafe(no_mangle)]
226pub unsafe extern "C" fn hide_inspect_key(data: *const u8, len: usize, out_kind: *mut i32) -> i32 {
227    guard(|| {
228        if out_kind.is_null() {
229            return HIDE_ERR_INVALID_ARGUMENT;
230        }
231        let Some(bytes) = (unsafe { borrow(data, len, MAX_KEY_FILE) }) else {
232            return HIDE_ERR_INVALID_ARGUMENT;
233        };
234        let kind = match hide_keyring::inspect(bytes) {
235            KeyFormat::Raw => HIDE_KEY_RAW,
236            KeyFormat::Protected => HIDE_KEY_PROTECTED,
237        };
238        unsafe { *out_kind = kind };
239        HIDE_OK
240    })
241}
242
243/// Loads a secret key from a file's bytes. Pass `passphrase = NULL` for a raw
244/// key; a protected key without a passphrase fails rather than guessing.
245///
246/// # Safety
247/// `data` must be valid for `len` bytes; `passphrase` must be null or a valid
248/// C string.
249#[unsafe(no_mangle)]
250pub unsafe extern "C" fn hide_secret_key_open(
251    data: *const u8,
252    len: usize,
253    passphrase: *const c_char,
254    out_secret: *mut *mut HideSecretKey,
255) -> i32 {
256    guard(|| {
257        if out_secret.is_null() {
258            return HIDE_ERR_INVALID_ARGUMENT;
259        }
260        let Some(bytes) = (unsafe { borrow(data, len, MAX_KEY_FILE) }) else {
261            return HIDE_ERR_INVALID_ARGUMENT;
262        };
263        let Some(passphrase) = (unsafe { borrow_str(passphrase) }) else {
264            return HIDE_ERR_INVALID_ARGUMENT;
265        };
266        match hide_keyring::open(bytes, passphrase) {
267            Ok(secret) => {
268                unsafe { *out_secret = Box::into_raw(Box::new(HideSecretKey(secret))) };
269                HIDE_OK
270            }
271            Err(error) => keyring_error_code(&error),
272        }
273    })
274}
275
276/// Seals a secret key with a passphrase, producing the bytes to store on disk.
277///
278/// # Safety
279/// `secret` must come from this library; `passphrase` must be a valid C string.
280#[unsafe(no_mangle)]
281pub unsafe extern "C" fn hide_secret_key_protect(
282    secret: *const HideSecretKey,
283    passphrase: *const c_char,
284    out: *mut HideBuffer,
285) -> i32 {
286    guard(|| {
287        if secret.is_null() || out.is_null() {
288            return HIDE_ERR_INVALID_ARGUMENT;
289        }
290        let Some(Some(passphrase)) = (unsafe { borrow_str(passphrase) }) else {
291            return HIDE_ERR_INVALID_ARGUMENT;
292        };
293        let secret = unsafe { &*secret };
294        match hide_keyring::protect(&secret.0, passphrase) {
295            Ok(sealed) => {
296                unsafe { *out = HideBuffer::from_vec(sealed) };
297                HIDE_OK
298            }
299            Err(error) => keyring_error_code(&error),
300        }
301    })
302}
303
304/// Derives the public key belonging to a secret key.
305///
306/// # Safety
307/// `secret` must come from this library.
308#[unsafe(no_mangle)]
309pub unsafe extern "C" fn hide_secret_key_public(
310    secret: *const HideSecretKey,
311    out: *mut HideBuffer,
312) -> i32 {
313    guard(|| {
314        if secret.is_null() || out.is_null() {
315            return HIDE_ERR_INVALID_ARGUMENT;
316        }
317        let secret = unsafe { &*secret };
318        match secret.0.public_key() {
319            Ok(public) => {
320                unsafe { *out = HideBuffer::from_vec(public.to_bytes()) };
321                HIDE_OK
322            }
323            Err(_) => HIDE_ERR_INTERNAL,
324        }
325    })
326}
327
328/// Releases a secret key. The key material is zeroized.
329///
330/// # Safety
331/// `secret` must come from this library and must not be freed twice.
332#[unsafe(no_mangle)]
333pub unsafe extern "C" fn hide_secret_key_free(secret: *mut HideSecretKey) {
334    if secret.is_null() {
335        return;
336    }
337    drop(unsafe { Box::from_raw(secret) });
338}
339
340/// Encrypts a buffer for one or more recipients.
341///
342/// `recipients` is a flat array of `recipient_count` public keys, each exactly
343/// `HIDE_PUBLIC_KEY_LEN` bytes.
344///
345/// # Safety
346/// All pointers must be valid for the stated lengths.
347#[unsafe(no_mangle)]
348pub unsafe extern "C" fn hide_encrypt(
349    plaintext: *const u8,
350    plaintext_len: usize,
351    recipients: *const u8,
352    recipient_count: usize,
353    filename: *const c_char,
354    media_type: *const c_char,
355    out: *mut HideBuffer,
356) -> i32 {
357    guard(|| {
358        if out.is_null() {
359            return HIDE_ERR_INVALID_ARGUMENT;
360        }
361        if recipient_count == 0 || recipient_count > 64 {
362            return HIDE_ERR_INVALID_ARGUMENT;
363        }
364        let Some(plaintext) = (unsafe { borrow(plaintext, plaintext_len, MAX_INPUT) }) else {
365            return HIDE_ERR_INVALID_ARGUMENT;
366        };
367        let Some(keys) =
368            (unsafe { borrow(recipients, recipient_count * HIDE_PUBLIC_KEY_LEN, MAX_INPUT) })
369        else {
370            return HIDE_ERR_INVALID_ARGUMENT;
371        };
372
373        let mut parsed = Vec::with_capacity(recipient_count);
374        for chunk in keys.chunks_exact(HIDE_PUBLIC_KEY_LEN) {
375            match RecipientPublic::from_bytes(chunk) {
376                Ok(key) => parsed.push(key),
377                Err(_) => return HIDE_ERR_INVALID_ARGUMENT,
378            }
379        }
380
381        let (Some(filename), Some(media_type)) = (unsafe { borrow_str(filename) }, unsafe {
382            borrow_str(media_type)
383        }) else {
384            return HIDE_ERR_INVALID_ARGUMENT;
385        };
386        let metadata = Metadata {
387            filename: filename.map(str::to_owned),
388            media_type: media_type.map(str::to_owned),
389            signature: None,
390        };
391
392        let mut container = Vec::new();
393        match hide_object::encrypt(&mut &*plaintext, &mut container, &parsed, &metadata) {
394            Ok(_) => {
395                unsafe { *out = HideBuffer::from_vec(container) };
396                HIDE_OK
397            }
398            Err(error) => object_error_code(&error),
399        }
400    })
401}
402
403/// Decrypts a container. Nothing is written to `out` unless the whole payload
404/// authenticates, so a caller cannot act on unverified plaintext.
405///
406/// Metadata is returned as length-prefixed buffers rather than C strings:
407/// the values are attacker-controlled, and a length is not something a caller
408/// can get wrong by scanning for a terminator. Free them like any buffer.
409///
410/// # Safety
411/// All pointers must be valid for the stated lengths.
412#[unsafe(no_mangle)]
413pub unsafe extern "C" fn hide_decrypt(
414    container: *const u8,
415    container_len: usize,
416    secret: *const HideSecretKey,
417    out: *mut HideBuffer,
418    out_filename: *mut HideBuffer,
419    out_media_type: *mut HideBuffer,
420) -> i32 {
421    guard(|| {
422        if secret.is_null() || out.is_null() {
423            return HIDE_ERR_INVALID_ARGUMENT;
424        }
425        let Some(container) = (unsafe { borrow(container, container_len, MAX_INPUT) }) else {
426            return HIDE_ERR_INVALID_ARGUMENT;
427        };
428        let secret = unsafe { &*secret };
429
430        // Held in a zeroizing buffer so a failed decryption leaves no plaintext.
431        let mut plaintext = Zeroizing::new(Vec::new());
432        let verified =
433            match hide_object::decrypt_to_staging(&mut &*container, &mut *plaintext, &secret.0) {
434                Ok(verified) => verified,
435                Err(error) => return object_error_code(&error),
436            };
437
438        if !out_filename.is_null() {
439            let value = verified.metadata.filename.unwrap_or_default();
440            unsafe { *out_filename = HideBuffer::from_vec(value.into_bytes()) };
441        }
442        if !out_media_type.is_null() {
443            let value = verified.metadata.media_type.unwrap_or_default();
444            unsafe { *out_media_type = HideBuffer::from_vec(value.into_bytes()) };
445        }
446
447        unsafe { *out = HideBuffer::from_vec(std::mem::take(&mut *plaintext)) };
448        HIDE_OK
449    })
450}
451
452/// Encodes a public key as pasteable armored text, returned as UTF-8 bytes.
453///
454/// # Safety
455/// `data` must be valid for `len` bytes.
456#[unsafe(no_mangle)]
457pub unsafe extern "C" fn hide_public_key_armor(
458    data: *const u8,
459    len: usize,
460    out: *mut HideBuffer,
461) -> i32 {
462    guard(|| {
463        if out.is_null() {
464            return HIDE_ERR_INVALID_ARGUMENT;
465        }
466        let Some(bytes) = (unsafe { borrow(data, len, MAX_KEY_FILE) }) else {
467            return HIDE_ERR_INVALID_ARGUMENT;
468        };
469        if bytes.len() != HIDE_PUBLIC_KEY_LEN {
470            return HIDE_ERR_INVALID_ARGUMENT;
471        }
472        let armored = hide_keyring::encode_public(bytes);
473        unsafe { *out = HideBuffer::from_vec(armored.into_bytes()) };
474        HIDE_OK
475    })
476}
477
478/// Decodes armored public-key text back into bytes.
479///
480/// # Safety
481/// `text` must be a valid NUL-terminated C string.
482#[unsafe(no_mangle)]
483pub unsafe extern "C" fn hide_public_key_dearmor(text: *const c_char, out: *mut HideBuffer) -> i32 {
484    guard(|| {
485        if out.is_null() {
486            return HIDE_ERR_INVALID_ARGUMENT;
487        }
488        let Some(Some(text)) = (unsafe { borrow_str(text) }) else {
489            return HIDE_ERR_INVALID_ARGUMENT;
490        };
491        match hide_keyring::decode_public(text) {
492            Ok(bytes) if bytes.len() == HIDE_PUBLIC_KEY_LEN => {
493                unsafe { *out = HideBuffer::from_vec(bytes) };
494                HIDE_OK
495            }
496            Ok(_) => HIDE_ERR_INVALID_ARGUMENT,
497            Err(error) => keyring_error_code(&error),
498        }
499    })
500}
501
502/// A hybrid signature: Ed25519 followed by ML-DSA-65.
503pub const HIDE_SIGNATURE_LEN: usize = hide_sign::SIGNATURE_LENGTH;
504/// A hybrid verifying key.
505pub const HIDE_VERIFYING_KEY_LEN: usize = hide_sign::VERIFYING_KEY_LENGTH;
506/// A challenge nonce.
507pub const HIDE_NONCE_LEN: usize = hide_sign::NONCE_LENGTH;
508
509/// Reasons a challenge answer was refused, beyond the generic codes above.
510pub const HIDE_ERR_CHALLENGE_EXPIRED: i32 = 8;
511pub const HIDE_ERR_CHALLENGE_REPLAYED: i32 = 9;
512
513/// An opaque signing identity. As with secret keys, no function exports the
514/// seed: a binding can sign, and cannot leak.
515pub struct HideSigningIdentity(hide_sign::SigningIdentity);
516
517/// Creates an identity and returns it sealed under `passphrase`, ready to
518/// write to disk. One seed backs both encryption and signing, so a caller has
519/// a single thing to back up; the seed itself never crosses the boundary.
520///
521/// # Safety
522/// `passphrase` must be a valid C string and `out` must be non-null.
523#[unsafe(no_mangle)]
524pub unsafe extern "C" fn hide_identity_generate(
525    passphrase: *const c_char,
526    out_key_file: *mut HideBuffer,
527) -> i32 {
528    guard(|| {
529        if out_key_file.is_null() {
530            return HIDE_ERR_INVALID_ARGUMENT;
531        }
532        let Some(Some(passphrase)) = (unsafe { borrow_str(passphrase) }) else {
533            return HIDE_ERR_INVALID_ARGUMENT;
534        };
535        let identity = match hide_keyring::Identity::generate() {
536            Ok(identity) => identity,
537            Err(_) => return HIDE_ERR_INTERNAL,
538        };
539        match hide_keyring::protect_identity(identity.expose_seed_for_sealing(), passphrase) {
540            Ok(sealed) => {
541                unsafe { *out_key_file = HideBuffer::from_vec(sealed) };
542                HIDE_OK
543            }
544            Err(error) => keyring_error_code(&error),
545        }
546    })
547}
548
549/// The verifier's record of answered challenges. Replay can only be detected
550/// by the verifier, so this must outlive a single request.
551pub struct HideSpentNonces(hide_sign::SpentNonces);
552
553/// Loads a signing identity from a key file's bytes.
554///
555/// See [`hide_identity_generate`] for creating one in the first place.
556///
557/// A key file written before signatures existed carries no signing seed, and
558/// fails here rather than being silently downgraded.
559///
560/// # Safety
561/// `data` must be valid for `len` bytes; `passphrase` must be null or a valid
562/// C string.
563#[unsafe(no_mangle)]
564pub unsafe extern "C" fn hide_signing_identity_open(
565    data: *const u8,
566    len: usize,
567    passphrase: *const c_char,
568    out_identity: *mut *mut HideSigningIdentity,
569) -> i32 {
570    guard(|| {
571        if out_identity.is_null() {
572            return HIDE_ERR_INVALID_ARGUMENT;
573        }
574        let Some(bytes) = (unsafe { borrow(data, len, MAX_KEY_FILE) }) else {
575            return HIDE_ERR_INVALID_ARGUMENT;
576        };
577        let Some(passphrase) = (unsafe { borrow_str(passphrase) }) else {
578            return HIDE_ERR_INVALID_ARGUMENT;
579        };
580
581        let seed = match hide_keyring::inspect(bytes) {
582            KeyFormat::Raw => {
583                if bytes.len() != 32 {
584                    return HIDE_ERR_NOT_A_KEY;
585                }
586                let mut seed = Zeroizing::new([0_u8; 32]);
587                seed.copy_from_slice(bytes);
588                hide_keyring::Identity::from_seed(seed).signing_seed()
589            }
590            KeyFormat::Protected => {
591                let Some(passphrase) = passphrase else {
592                    return HIDE_ERR_INVALID_ARGUMENT;
593                };
594                match hide_keyring::unprotect_seed(bytes, passphrase) {
595                    Ok((seed, hide_keyring::KeyPurpose::Identity)) => {
596                        hide_keyring::Identity::from_seed(seed).signing_seed()
597                    }
598                    // Encryption-only: predates signatures, so there is no
599                    // signing seed to derive and inventing one is not an option.
600                    Ok(_) => return HIDE_ERR_NOT_A_KEY,
601                    Err(error) => return keyring_error_code(&error),
602                }
603            }
604        };
605
606        match hide_sign::SigningIdentity::from_bytes(&seed[..]) {
607            Ok(identity) => {
608                unsafe { *out_identity = Box::into_raw(Box::new(HideSigningIdentity(identity))) };
609                HIDE_OK
610            }
611            Err(_) => HIDE_ERR_NOT_A_KEY,
612        }
613    })
614}
615
616/// The shareable verifying key for a signing identity.
617///
618/// # Safety
619/// `identity` must come from this library.
620#[unsafe(no_mangle)]
621pub unsafe extern "C" fn hide_signing_identity_public(
622    identity: *const HideSigningIdentity,
623    out: *mut HideBuffer,
624) -> i32 {
625    guard(|| {
626        if identity.is_null() || out.is_null() {
627            return HIDE_ERR_INVALID_ARGUMENT;
628        }
629        let identity = unsafe { &*identity };
630        let public = identity.0.verifying_key().to_bytes();
631        unsafe { *out = HideBuffer::from_vec(public.to_vec()) };
632        HIDE_OK
633    })
634}
635
636/// Releases a signing identity, zeroizing the seed.
637///
638/// # Safety
639/// `identity` must come from this library and must not be freed twice.
640#[unsafe(no_mangle)]
641pub unsafe extern "C" fn hide_signing_identity_free(identity: *mut HideSigningIdentity) {
642    if identity.is_null() {
643        return;
644    }
645    drop(unsafe { Box::from_raw(identity) });
646}
647
648/// Signs a message under a caller-chosen context.
649///
650/// The context separates uses of one identity: a signature made for one
651/// purpose must not verify as another. Bindings should pass the same context
652/// they will verify with, and never let a remote party choose it.
653///
654/// # Safety
655/// All pointers must be valid for the stated lengths.
656#[unsafe(no_mangle)]
657pub unsafe extern "C" fn hide_sign_message(
658    identity: *const HideSigningIdentity,
659    context: *const u8,
660    context_len: usize,
661    message: *const u8,
662    message_len: usize,
663    out: *mut HideBuffer,
664) -> i32 {
665    guard(|| {
666        if identity.is_null() || out.is_null() {
667            return HIDE_ERR_INVALID_ARGUMENT;
668        }
669        let Some(context) = (unsafe { borrow(context, context_len, MAX_KEY_FILE) }) else {
670            return HIDE_ERR_INVALID_ARGUMENT;
671        };
672        let Some(message) = (unsafe { borrow(message, message_len, MAX_INPUT) }) else {
673            return HIDE_ERR_INVALID_ARGUMENT;
674        };
675        let identity = unsafe { &*identity };
676        let signature = identity.0.sign(context, message);
677        unsafe { *out = HideBuffer::from_vec(signature.to_vec()) };
678        HIDE_OK
679    })
680}
681
682/// Verifies a signature. Returns `HIDE_OK` only if **both** halves verify.
683///
684/// # Safety
685/// All pointers must be valid for the stated lengths.
686#[unsafe(no_mangle)]
687pub unsafe extern "C" fn hide_verify_message(
688    public_key: *const u8,
689    public_key_len: usize,
690    context: *const u8,
691    context_len: usize,
692    message: *const u8,
693    message_len: usize,
694    signature: *const u8,
695    signature_len: usize,
696) -> i32 {
697    guard(|| {
698        let Some(public_key) = (unsafe { borrow(public_key, public_key_len, MAX_KEY_FILE) }) else {
699            return HIDE_ERR_INVALID_ARGUMENT;
700        };
701        let Some(context) = (unsafe { borrow(context, context_len, MAX_KEY_FILE) }) else {
702            return HIDE_ERR_INVALID_ARGUMENT;
703        };
704        let Some(message) = (unsafe { borrow(message, message_len, MAX_INPUT) }) else {
705            return HIDE_ERR_INVALID_ARGUMENT;
706        };
707        let Some(signature) = (unsafe { borrow(signature, signature_len, MAX_KEY_FILE * 4) })
708        else {
709            return HIDE_ERR_INVALID_ARGUMENT;
710        };
711        let Ok(verifying) = hide_sign::VerifyingIdentity::from_bytes(public_key) else {
712            return HIDE_ERR_NOT_A_KEY;
713        };
714        match verifying.verify(context, message, signature) {
715            Ok(()) => HIDE_OK,
716            Err(_) => HIDE_ERR_AUTHENTICATION,
717        }
718    })
719}
720
721/// Creates a challenge for a prover to answer. The encoded challenge is not
722/// secret and is handed to the prover as-is.
723///
724/// # Safety
725/// `audience` must be a valid C string; `out` must be non-null.
726#[unsafe(no_mangle)]
727pub unsafe extern "C" fn hide_challenge_new(
728    audience: *const c_char,
729    now: u64,
730    valid_for: u64,
731    out: *mut HideBuffer,
732) -> i32 {
733    guard(|| {
734        if out.is_null() {
735            return HIDE_ERR_INVALID_ARGUMENT;
736        }
737        let Some(Some(audience)) = (unsafe { borrow_str(audience) }) else {
738            return HIDE_ERR_INVALID_ARGUMENT;
739        };
740        match hide_sign::Challenge::new(audience, now, valid_for) {
741            Ok(challenge) => {
742                unsafe { *out = HideBuffer::from_vec(challenge.encode()) };
743                HIDE_OK
744            }
745            Err(_) => HIDE_ERR_INTERNAL,
746        }
747    })
748}
749
750/// Answers a challenge, producing a signature over it.
751///
752/// # Safety
753/// All pointers must be valid for the stated lengths.
754#[unsafe(no_mangle)]
755pub unsafe extern "C" fn hide_challenge_answer(
756    identity: *const HideSigningIdentity,
757    challenge: *const u8,
758    challenge_len: usize,
759    out: *mut HideBuffer,
760) -> i32 {
761    guard(|| {
762        if identity.is_null() || out.is_null() {
763            return HIDE_ERR_INVALID_ARGUMENT;
764        }
765        let Some(bytes) = (unsafe { borrow(challenge, challenge_len, MAX_KEY_FILE) }) else {
766            return HIDE_ERR_INVALID_ARGUMENT;
767        };
768        let Ok(challenge) = hide_sign::Challenge::decode(bytes) else {
769            return HIDE_ERR_MALFORMED;
770        };
771        let identity = unsafe { &*identity };
772        unsafe { *out = HideBuffer::from_vec(challenge.answer(&identity.0).to_vec()) };
773        HIDE_OK
774    })
775}
776
777/// Creates the verifier's record of spent nonces.
778#[unsafe(no_mangle)]
779pub extern "C" fn hide_spent_nonces_new() -> *mut HideSpentNonces {
780    Box::into_raw(Box::new(HideSpentNonces(hide_sign::SpentNonces::new())))
781}
782
783/// Releases the record of spent nonces.
784///
785/// # Safety
786/// `spent` must come from this library and must not be freed twice.
787#[unsafe(no_mangle)]
788pub unsafe extern "C" fn hide_spent_nonces_free(spent: *mut HideSpentNonces) {
789    if spent.is_null() {
790        return;
791    }
792    drop(unsafe { Box::from_raw(spent) });
793}
794
795/// Accepts a challenge answer exactly once. A valid signature replayed a
796/// second time returns `HIDE_ERR_CHALLENGE_REPLAYED`, which is the entire
797/// reason this call takes a `spent` record rather than being a pure function.
798///
799/// # Safety
800/// All pointers must be valid for the stated lengths.
801#[unsafe(no_mangle)]
802pub unsafe extern "C" fn hide_challenge_accept(
803    spent: *mut HideSpentNonces,
804    challenge: *const u8,
805    challenge_len: usize,
806    signature: *const u8,
807    signature_len: usize,
808    public_key: *const u8,
809    public_key_len: usize,
810    now: u64,
811) -> i32 {
812    guard(|| {
813        if spent.is_null() {
814            return HIDE_ERR_INVALID_ARGUMENT;
815        }
816        let Some(challenge_bytes) = (unsafe { borrow(challenge, challenge_len, MAX_KEY_FILE) })
817        else {
818            return HIDE_ERR_INVALID_ARGUMENT;
819        };
820        let Some(signature) = (unsafe { borrow(signature, signature_len, MAX_KEY_FILE * 4) })
821        else {
822            return HIDE_ERR_INVALID_ARGUMENT;
823        };
824        let Some(public_key) = (unsafe { borrow(public_key, public_key_len, MAX_KEY_FILE) }) else {
825            return HIDE_ERR_INVALID_ARGUMENT;
826        };
827        let Ok(challenge) = hide_sign::Challenge::decode(challenge_bytes) else {
828            return HIDE_ERR_MALFORMED;
829        };
830        let Ok(prover) = hide_sign::VerifyingIdentity::from_bytes(public_key) else {
831            return HIDE_ERR_NOT_A_KEY;
832        };
833        let spent = unsafe { &mut *spent };
834        match spent.0.accept(&challenge, signature, &prover, now) {
835            Ok(()) => HIDE_OK,
836            Err(hide_sign::ChallengeError::Expired) => HIDE_ERR_CHALLENGE_EXPIRED,
837            Err(hide_sign::ChallengeError::Replayed) => HIDE_ERR_CHALLENGE_REPLAYED,
838            Err(hide_sign::ChallengeError::NotSigned) => HIDE_ERR_AUTHENTICATION,
839        }
840    })
841}
842
843#[cfg(test)]
844mod tests {
845    use std::ffi::CString;
846
847    use super::*;
848
849    /// Drives the API exactly as a C caller would, pointers and all.
850    unsafe fn keypair() -> (*mut HideSecretKey, HideBuffer) {
851        let mut secret = ptr::null_mut();
852        let mut public = hide_buffer_empty();
853        assert_eq!(
854            unsafe { hide_keypair_generate(&mut secret, &mut public) },
855            HIDE_OK
856        );
857        assert_eq!(public.len, HIDE_PUBLIC_KEY_LEN);
858        (secret, public)
859    }
860
861    #[test]
862    fn round_trips_through_the_c_api() {
863        unsafe {
864            let (secret, public) = keypair();
865            let message = b"nume,suma\nAna,9000\n";
866
867            let mut container = hide_buffer_empty();
868            assert_eq!(
869                hide_encrypt(
870                    message.as_ptr(),
871                    message.len(),
872                    public.data,
873                    1,
874                    c"salarii.csv".as_ptr(),
875                    ptr::null(),
876                    &mut container,
877                ),
878                HIDE_OK
879            );
880            assert!(container.len > message.len());
881
882            let mut plaintext = hide_buffer_empty();
883            let mut filename = hide_buffer_empty();
884            assert_eq!(
885                hide_decrypt(
886                    container.data,
887                    container.len,
888                    secret,
889                    &mut plaintext,
890                    &mut filename,
891                    ptr::null_mut(),
892                ),
893                HIDE_OK
894            );
895            assert_eq!(
896                slice::from_raw_parts(plaintext.data, plaintext.len),
897                message
898            );
899            assert_eq!(
900                slice::from_raw_parts(filename.data, filename.len),
901                b"salarii.csv"
902            );
903
904            hide_buffer_free(&mut filename);
905            hide_buffer_free(&mut plaintext);
906            hide_buffer_free(&mut container);
907            hide_buffer_free(&mut { public });
908            hide_secret_key_free(secret);
909        }
910    }
911
912    #[test]
913    fn a_freed_buffer_is_cleared_and_safe_to_free_twice() {
914        unsafe {
915            let (secret, mut public) = keypair();
916            let mut container = hide_buffer_empty();
917            hide_encrypt(
918                b"x".as_ptr(),
919                1,
920                public.data,
921                1,
922                ptr::null(),
923                ptr::null(),
924                &mut container,
925            );
926            hide_buffer_free(&mut container);
927            assert!(container.data.is_null(), "freed buffer kept its pointer");
928            // A second free must be a no-op rather than a double free.
929            hide_buffer_free(&mut container);
930            hide_buffer_free(&mut public);
931            hide_secret_key_free(secret);
932        }
933    }
934
935    #[test]
936    fn tampering_is_refused_and_yields_no_plaintext() {
937        unsafe {
938            let (secret, mut public) = keypair();
939            let mut container = hide_buffer_empty();
940            hide_encrypt(
941                b"confidential".as_ptr(),
942                12,
943                public.data,
944                1,
945                ptr::null(),
946                ptr::null(),
947                &mut container,
948            );
949
950            let mut damaged = slice::from_raw_parts(container.data, container.len).to_vec();
951            let last = damaged.len() - 1;
952            damaged[last] ^= 1;
953
954            let mut plaintext = hide_buffer_empty();
955            let code = hide_decrypt(
956                damaged.as_ptr(),
957                damaged.len(),
958                secret,
959                &mut plaintext,
960                ptr::null_mut(),
961                ptr::null_mut(),
962            );
963            assert_ne!(code, HIDE_OK);
964            assert!(plaintext.data.is_null(), "published unverified plaintext");
965
966            hide_buffer_free(&mut container);
967            hide_buffer_free(&mut public);
968            hide_secret_key_free(secret);
969        }
970    }
971
972    #[test]
973    fn the_wrong_key_cannot_decrypt() {
974        unsafe {
975            let (secret, mut public) = keypair();
976            let (other, mut other_public) = keypair();
977            let mut container = hide_buffer_empty();
978            hide_encrypt(
979                b"secret".as_ptr(),
980                6,
981                public.data,
982                1,
983                ptr::null(),
984                ptr::null(),
985                &mut container,
986            );
987
988            let mut plaintext = hide_buffer_empty();
989            assert_eq!(
990                hide_decrypt(
991                    container.data,
992                    container.len,
993                    other,
994                    &mut plaintext,
995                    ptr::null_mut(),
996                    ptr::null_mut(),
997                ),
998                HIDE_ERR_NO_MATCHING_RECIPIENT
999            );
1000
1001            hide_buffer_free(&mut container);
1002            hide_buffer_free(&mut public);
1003            hide_buffer_free(&mut other_public);
1004            hide_secret_key_free(secret);
1005            hide_secret_key_free(other);
1006        }
1007    }
1008
1009    #[test]
1010    fn null_and_absurd_arguments_are_rejected_rather_than_crashing() {
1011        unsafe {
1012            let mut out = hide_buffer_empty();
1013            let mut secret = ptr::null_mut();
1014
1015            assert_eq!(
1016                hide_keypair_generate(ptr::null_mut(), &mut out),
1017                HIDE_ERR_INVALID_ARGUMENT
1018            );
1019            assert_eq!(
1020                hide_keypair_generate(&mut secret, ptr::null_mut()),
1021                HIDE_ERR_INVALID_ARGUMENT
1022            );
1023            // A non-null length with a null pointer must not be dereferenced.
1024            assert_eq!(
1025                hide_encrypt(
1026                    ptr::null(),
1027                    10,
1028                    ptr::null(),
1029                    1,
1030                    ptr::null(),
1031                    ptr::null(),
1032                    &mut out
1033                ),
1034                HIDE_ERR_INVALID_ARGUMENT
1035            );
1036            // A length that would over-allocate must be refused up front.
1037            assert_eq!(
1038                hide_encrypt(
1039                    b"x".as_ptr(),
1040                    usize::MAX,
1041                    ptr::null(),
1042                    1,
1043                    ptr::null(),
1044                    ptr::null(),
1045                    &mut out
1046                ),
1047                HIDE_ERR_INVALID_ARGUMENT
1048            );
1049            assert_eq!(
1050                hide_decrypt(
1051                    ptr::null(),
1052                    0,
1053                    ptr::null(),
1054                    &mut out,
1055                    ptr::null_mut(),
1056                    ptr::null_mut()
1057                ),
1058                HIDE_ERR_INVALID_ARGUMENT
1059            );
1060            let mut kind = -1;
1061            assert_eq!(
1062                hide_inspect_key(ptr::null(), 32, &mut kind),
1063                HIDE_ERR_INVALID_ARGUMENT
1064            );
1065            // Freeing null is a no-op, not a crash.
1066            hide_buffer_free(ptr::null_mut());
1067            hide_secret_key_free(ptr::null_mut());
1068        }
1069    }
1070
1071    #[test]
1072    fn protected_keys_round_trip_and_reject_a_wrong_passphrase() {
1073        unsafe {
1074            let (secret, mut public) = keypair();
1075            let mut sealed = hide_buffer_empty();
1076            assert_eq!(
1077                hide_secret_key_protect(secret, c"correct horse".as_ptr(), &mut sealed),
1078                HIDE_OK
1079            );
1080
1081            let mut kind = -1;
1082            assert_eq!(
1083                hide_inspect_key(sealed.data, sealed.len, &mut kind),
1084                HIDE_OK
1085            );
1086            assert_eq!(kind, HIDE_KEY_PROTECTED);
1087
1088            let mut reopened = ptr::null_mut();
1089            assert_eq!(
1090                hide_secret_key_open(sealed.data, sealed.len, c"wrong".as_ptr(), &mut reopened),
1091                HIDE_ERR_WRONG_PASSPHRASE
1092            );
1093            assert_eq!(
1094                hide_secret_key_open(sealed.data, sealed.len, ptr::null(), &mut reopened),
1095                HIDE_ERR_WRONG_PASSPHRASE
1096            );
1097            assert_eq!(
1098                hide_secret_key_open(
1099                    sealed.data,
1100                    sealed.len,
1101                    c"correct horse".as_ptr(),
1102                    &mut reopened
1103                ),
1104                HIDE_OK
1105            );
1106
1107            // The reopened key must be the same one.
1108            let mut derived = hide_buffer_empty();
1109            assert_eq!(hide_secret_key_public(reopened, &mut derived), HIDE_OK);
1110            assert_eq!(
1111                slice::from_raw_parts(derived.data, derived.len),
1112                slice::from_raw_parts(public.data, public.len)
1113            );
1114
1115            hide_buffer_free(&mut derived);
1116            hide_buffer_free(&mut sealed);
1117            hide_buffer_free(&mut public);
1118            hide_secret_key_free(reopened);
1119            hide_secret_key_free(secret);
1120        }
1121    }
1122
1123    #[test]
1124    fn armor_round_trips_and_rejects_rubbish() {
1125        unsafe {
1126            let (secret, mut public) = keypair();
1127            let mut armored = hide_buffer_empty();
1128            assert_eq!(
1129                hide_public_key_armor(public.data, public.len, &mut armored),
1130                HIDE_OK
1131            );
1132
1133            // The armored form must survive a trip through a C string.
1134            let text =
1135                CString::new(slice::from_raw_parts(armored.data, armored.len)).expect("no NUL");
1136            let mut decoded = hide_buffer_empty();
1137            assert_eq!(
1138                hide_public_key_dearmor(text.as_ptr(), &mut decoded),
1139                HIDE_OK
1140            );
1141            assert_eq!(
1142                slice::from_raw_parts(decoded.data, decoded.len),
1143                slice::from_raw_parts(public.data, public.len)
1144            );
1145            assert_ne!(
1146                hide_public_key_dearmor(c"not a key".as_ptr(), &mut decoded),
1147                HIDE_OK
1148            );
1149
1150            hide_buffer_free(&mut decoded);
1151            hide_buffer_free(&mut armored);
1152            hide_buffer_free(&mut public);
1153            hide_secret_key_free(secret);
1154        }
1155    }
1156
1157    /// A sealed identity file, the way a real caller would have one on disk.
1158    fn identity_file(passphrase: &str) -> Vec<u8> {
1159        let identity = hide_keyring::Identity::generate().expect("randomness");
1160        hide_keyring::protect_identity(identity.expose_seed_for_sealing(), passphrase)
1161            .expect("sealing works")
1162    }
1163
1164    unsafe fn open_identity(file: &[u8], passphrase: &CString) -> *mut HideSigningIdentity {
1165        let mut identity = ptr::null_mut();
1166        assert_eq!(
1167            unsafe {
1168                hide_signing_identity_open(
1169                    file.as_ptr(),
1170                    file.len(),
1171                    passphrase.as_ptr(),
1172                    &mut identity,
1173                )
1174            },
1175            HIDE_OK
1176        );
1177        identity
1178    }
1179
1180    #[test]
1181    fn signs_and_verifies_across_the_boundary() {
1182        unsafe {
1183            let passphrase = CString::new("correct horse battery staple").unwrap();
1184            let file = identity_file(passphrase.to_str().unwrap());
1185            let identity = open_identity(&file, &passphrase);
1186
1187            let mut public = hide_buffer_empty();
1188            assert_eq!(hide_signing_identity_public(identity, &mut public), HIDE_OK);
1189            assert_eq!(public.len, HIDE_VERIFYING_KEY_LEN);
1190
1191            let context = b"HIDE/0.5 ffi test";
1192            let message = b"the message that crossed the boundary";
1193            let mut signature = hide_buffer_empty();
1194            assert_eq!(
1195                hide_sign_message(
1196                    identity,
1197                    context.as_ptr(),
1198                    context.len(),
1199                    message.as_ptr(),
1200                    message.len(),
1201                    &mut signature,
1202                ),
1203                HIDE_OK
1204            );
1205            assert_eq!(signature.len, HIDE_SIGNATURE_LEN);
1206
1207            assert_eq!(
1208                hide_verify_message(
1209                    public.data,
1210                    public.len,
1211                    context.as_ptr(),
1212                    context.len(),
1213                    message.as_ptr(),
1214                    message.len(),
1215                    signature.data,
1216                    signature.len,
1217                ),
1218                HIDE_OK
1219            );
1220
1221            // A different context must not verify, or the separation the C API
1222            // advertises would be decorative.
1223            let other = b"HIDE/0.5 something else";
1224            assert_eq!(
1225                hide_verify_message(
1226                    public.data,
1227                    public.len,
1228                    other.as_ptr(),
1229                    other.len(),
1230                    message.as_ptr(),
1231                    message.len(),
1232                    signature.data,
1233                    signature.len,
1234                ),
1235                HIDE_ERR_AUTHENTICATION
1236            );
1237
1238            let altered = b"the message that crossed the boundaryX";
1239            assert_eq!(
1240                hide_verify_message(
1241                    public.data,
1242                    public.len,
1243                    context.as_ptr(),
1244                    context.len(),
1245                    altered.as_ptr(),
1246                    altered.len(),
1247                    signature.data,
1248                    signature.len,
1249                ),
1250                HIDE_ERR_AUTHENTICATION
1251            );
1252
1253            hide_buffer_free(&mut signature);
1254            hide_buffer_free(&mut public);
1255            hide_signing_identity_free(identity);
1256        }
1257    }
1258
1259    /// A key file that predates signatures has no signing seed, and inventing
1260    /// one would silently sign under a key nobody expects.
1261    #[test]
1262    fn an_encryption_only_key_cannot_sign() {
1263        unsafe {
1264            let passphrase = CString::new("correct horse battery staple").unwrap();
1265            let secret = hide_crypto::RecipientSecret::generate().expect("randomness");
1266            let file =
1267                hide_keyring::protect(&secret, passphrase.to_str().unwrap()).expect("sealing");
1268
1269            let mut identity = ptr::null_mut();
1270            assert_eq!(
1271                hide_signing_identity_open(
1272                    file.as_ptr(),
1273                    file.len(),
1274                    passphrase.as_ptr(),
1275                    &mut identity,
1276                ),
1277                HIDE_ERR_NOT_A_KEY
1278            );
1279            assert!(identity.is_null());
1280        }
1281    }
1282
1283    #[test]
1284    fn a_wrong_passphrase_is_named_as_such() {
1285        unsafe {
1286            let file = identity_file("correct horse battery staple");
1287            let wrong = CString::new("not the passphrase at all").unwrap();
1288            let mut identity = ptr::null_mut();
1289            assert_eq!(
1290                hide_signing_identity_open(
1291                    file.as_ptr(),
1292                    file.len(),
1293                    wrong.as_ptr(),
1294                    &mut identity
1295                ),
1296                HIDE_ERR_WRONG_PASSPHRASE
1297            );
1298        }
1299    }
1300
1301    #[test]
1302    fn a_challenge_is_answered_once_and_then_refused() {
1303        unsafe {
1304            let passphrase = CString::new("correct horse battery staple").unwrap();
1305            let file = identity_file(passphrase.to_str().unwrap());
1306            let identity = open_identity(&file, &passphrase);
1307
1308            let mut public = hide_buffer_empty();
1309            assert_eq!(hide_signing_identity_public(identity, &mut public), HIDE_OK);
1310
1311            let audience = CString::new("ssh://host.example").unwrap();
1312            let mut challenge = hide_buffer_empty();
1313            assert_eq!(
1314                hide_challenge_new(audience.as_ptr(), 1_000, 60, &mut challenge),
1315                HIDE_OK
1316            );
1317
1318            let mut answer = hide_buffer_empty();
1319            assert_eq!(
1320                hide_challenge_answer(identity, challenge.data, challenge.len, &mut answer),
1321                HIDE_OK
1322            );
1323
1324            let spent = hide_spent_nonces_new();
1325            assert_eq!(
1326                hide_challenge_accept(
1327                    spent,
1328                    challenge.data,
1329                    challenge.len,
1330                    answer.data,
1331                    answer.len,
1332                    public.data,
1333                    public.len,
1334                    1_000,
1335                ),
1336                HIDE_OK
1337            );
1338            // The same valid answer, a second time.
1339            assert_eq!(
1340                hide_challenge_accept(
1341                    spent,
1342                    challenge.data,
1343                    challenge.len,
1344                    answer.data,
1345                    answer.len,
1346                    public.data,
1347                    public.len,
1348                    1_000,
1349                ),
1350                HIDE_ERR_CHALLENGE_REPLAYED
1351            );
1352
1353            hide_spent_nonces_free(spent);
1354            hide_buffer_free(&mut answer);
1355            hide_buffer_free(&mut challenge);
1356            hide_buffer_free(&mut public);
1357            hide_signing_identity_free(identity);
1358        }
1359    }
1360
1361    #[test]
1362    fn an_answer_after_the_window_is_refused_across_the_boundary() {
1363        unsafe {
1364            let passphrase = CString::new("correct horse battery staple").unwrap();
1365            let file = identity_file(passphrase.to_str().unwrap());
1366            let identity = open_identity(&file, &passphrase);
1367
1368            let mut public = hide_buffer_empty();
1369            assert_eq!(hide_signing_identity_public(identity, &mut public), HIDE_OK);
1370
1371            let audience = CString::new("ssh://host.example").unwrap();
1372            let mut challenge = hide_buffer_empty();
1373            assert_eq!(
1374                hide_challenge_new(audience.as_ptr(), 1_000, 60, &mut challenge),
1375                HIDE_OK
1376            );
1377            let mut answer = hide_buffer_empty();
1378            assert_eq!(
1379                hide_challenge_answer(identity, challenge.data, challenge.len, &mut answer),
1380                HIDE_OK
1381            );
1382
1383            let spent = hide_spent_nonces_new();
1384            assert_eq!(
1385                hide_challenge_accept(
1386                    spent,
1387                    challenge.data,
1388                    challenge.len,
1389                    answer.data,
1390                    answer.len,
1391                    public.data,
1392                    public.len,
1393                    1_100,
1394                ),
1395                HIDE_ERR_CHALLENGE_EXPIRED
1396            );
1397
1398            hide_spent_nonces_free(spent);
1399            hide_buffer_free(&mut answer);
1400            hide_buffer_free(&mut challenge);
1401            hide_buffer_free(&mut public);
1402            hide_signing_identity_free(identity);
1403        }
1404    }
1405
1406    /// Null and absurd lengths must be refused, not dereferenced.
1407    #[test]
1408    fn the_signing_boundary_refuses_nulls_without_crashing() {
1409        unsafe {
1410            let mut out = hide_buffer_empty();
1411            assert_eq!(
1412                hide_sign_message(ptr::null(), ptr::null(), 0, ptr::null(), 0, &mut out),
1413                HIDE_ERR_INVALID_ARGUMENT
1414            );
1415            assert_eq!(
1416                hide_verify_message(
1417                    ptr::null(),
1418                    0,
1419                    ptr::null(),
1420                    0,
1421                    ptr::null(),
1422                    0,
1423                    ptr::null(),
1424                    0
1425                ),
1426                HIDE_ERR_NOT_A_KEY
1427            );
1428            assert_eq!(
1429                hide_challenge_answer(ptr::null(), ptr::null(), 0, &mut out),
1430                HIDE_ERR_INVALID_ARGUMENT
1431            );
1432            assert_eq!(
1433                hide_challenge_accept(
1434                    ptr::null_mut(),
1435                    ptr::null(),
1436                    0,
1437                    ptr::null(),
1438                    0,
1439                    ptr::null(),
1440                    0,
1441                    0
1442                ),
1443                HIDE_ERR_INVALID_ARGUMENT
1444            );
1445            // Freeing null is a no-op, not a crash.
1446            hide_signing_identity_free(ptr::null_mut());
1447            hide_spent_nonces_free(ptr::null_mut());
1448        }
1449    }
1450}