craton-hsm 0.10.5

A production-grade PKCS#11 v3.0-compliant Software HSM written in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Craton Software Company
//! FIPS 140-3 §9.4 Software/Firmware Integrity Test
//!
//! Verifies an Ed25519 detached signature of the loaded module binary at
//! startup to ensure the code has not been tampered with.
//!
//! The Ed25519 **public key** is embedded as a compile-time constant (safe
//! to expose).  The corresponding **private key** is kept in the build
//! pipeline and never distributed — an attacker who modifies the binary
//! cannot forge a valid signature without it.
//!
//! Both the embedded public key and the on-disk `.sig` sidecar are required:
//! a missing `.sig` file or an all-zero placeholder public key is a hard
//! error in every build (fips and non-fips).  The only escape hatch is the
//! runtime env var `CRATON_HSM_INTEGRITY_BYPASS=unsafe-dev-only`, which only
//! takes effect when the public key is still the all-zero placeholder and
//! always emits a loud `tracing::warn!` — production builds must set
//! `CRATON_HSM_INTEGRITY_PUBLIC_KEY` at build time and ship a valid `.sig`.

use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
use std::path::PathBuf;

/// Ed25519 public key for integrity verification.
///
/// Generate a real keypair with `tools/generate-integrity-keypair.sh` and
/// replace this placeholder with the output.  The corresponding private key
/// must be kept secret in the build pipeline.
///
/// # Security Properties
///
/// Unlike the previous HMAC-based approach, the public key is **safe to
/// embed** in the distributed binary.  An attacker with write access to the
/// binary and `.sig` file **cannot** forge a valid Ed25519 signature without
/// the private key (which never leaves the build environment).
///
/// This provides true tamper resistance, not just accidental-corruption
/// detection.
/// Set via the `CRATON_HSM_INTEGRITY_PUBLIC_KEY` env var at build time (64 hex chars).
/// If unset, defaults to all-zeros — the placeholder is detected at runtime and
/// `check_integrity()` refuses to start in every build (both fips and non-fips).
/// Production builds MUST set this env var. Local dev/test runs can opt in to a
/// bypass via the runtime env var `CRATON_HSM_INTEGRITY_BYPASS=unsafe-dev-only`,
/// which only takes effect while the placeholder key is still embedded.
const INTEGRITY_PUBLIC_KEY: [u8; 32] = {
    // Try to read the key from build-time environment variable.
    // Usage: CRATON_HSM_INTEGRITY_PUBLIC_KEY=<64 hex chars> cargo build --release
    const fn hex_nibble(b: u8) -> u8 {
        match b {
            b'0'..=b'9' => b - b'0',
            b'a'..=b'f' => b - b'a' + 10,
            b'A'..=b'F' => b - b'A' + 10,
            _ => panic!("CRATON_HSM_INTEGRITY_PUBLIC_KEY contains invalid hex character"),
        }
    }
    match option_env!("CRATON_HSM_INTEGRITY_PUBLIC_KEY") {
        Some(hex) => {
            let bytes = hex.as_bytes();
            assert!(
                bytes.len() == 64,
                "CRATON_HSM_INTEGRITY_PUBLIC_KEY must be exactly 64 hex chars"
            );
            let mut key = [0u8; 32];
            let mut i = 0;
            while i < 32 {
                key[i] = (hex_nibble(bytes[i * 2]) << 4) | hex_nibble(bytes[i * 2 + 1]);
                i += 1;
            }
            key
        }
        None => [0u8; 32], // Placeholder — detected at runtime
    }
};

/// Returns true if the integrity public key is still the all-zeros placeholder.
fn is_placeholder_key() -> bool {
    INTEGRITY_PUBLIC_KEY.iter().all(|&b| b == 0)
}

/// Runtime escape hatch that allows dev/test builds to skip the integrity
/// check while the public key is still the all-zero placeholder.  Production
/// builds embed a real public key, so this variable has no effect for them.
const DEV_BYPASS_ENV: &str = "CRATON_HSM_INTEGRITY_BYPASS";
const DEV_BYPASS_VALUE: &str = "unsafe-dev-only";

/// Returns true if the runtime dev-bypass env var is set to its sentinel value.
fn dev_bypass_requested() -> bool {
    std::env::var(DEV_BYPASS_ENV)
        .map(|v| v == DEV_BYPASS_VALUE)
        .unwrap_or(false)
}

/// Run the software integrity check (POST §9.4).
///
/// Returns `Ok(())` only if the embedded `INTEGRITY_PUBLIC_KEY` is a real
/// (non-zero) key and the `.sig` sidecar is present and verifies under
/// Ed25519.  Both conditions are enforced in every build (fips and non-fips).
///
/// Returns `Err(...)` if any of:
/// - The embedded public key is the all-zeros placeholder
///   (build with `CRATON_HSM_INTEGRITY_PUBLIC_KEY=<hex>` set)
/// - The `.sig` sidecar is missing
/// - The signature is malformed or doesn't verify (tampered binary)
///
/// The single escape hatch is the runtime env var
/// `CRATON_HSM_INTEGRITY_BYPASS=unsafe-dev-only`, which is honored ONLY while
/// the embedded public key is still the placeholder.  When honored, a loud
/// `tracing::warn!` is emitted and the check returns `Ok(())` without
/// inspecting the binary.  This is intended exclusively for `cargo test` and
/// other local dev workflows.
pub fn check_integrity() -> Result<(), String> {
    // Reject the all-zero placeholder public key in every build.  An
    // unsigned binary cannot be authenticated, so we refuse to start unless
    // the caller explicitly opts in to the dev bypass.
    if is_placeholder_key() {
        if dev_bypass_requested() {
            tracing::warn!(
                "Software integrity test: BYPASSED via {}={} — this build is UNSIGNED \
                 and MUST NOT be used in production. Set CRATON_HSM_INTEGRITY_PUBLIC_KEY \
                 at build time and ship a .sig sidecar for any non-development use.",
                DEV_BYPASS_ENV,
                DEV_BYPASS_VALUE
            );
            return Ok(());
        }
        return Err(format!(
            "Software integrity test: INTEGRITY_PUBLIC_KEY is still the all-zeros placeholder. \
             Set CRATON_HSM_INTEGRITY_PUBLIC_KEY at build time, or (for local dev/test only) \
             set {}={} at runtime.",
            DEV_BYPASS_ENV, DEV_BYPASS_VALUE
        ));
    }

    // A real public key is embedded.  The dev-bypass env var is intentionally
    // ignored in this branch — once a build has a real key, the .sig file is
    // mandatory.
    let module_path = match get_module_path() {
        Some(p) => p,
        None => {
            return Err(
                "Software integrity test: could not determine the loaded module path; \
             refusing to bypass signature verification"
                    .to_string(),
            )
        }
    };

    let sig_path = module_path.with_extension("sig");

    // Check for legacy .hmac sidecar and warn about migration
    let hmac_path = module_path.with_extension("hmac");
    if hmac_path.exists() && !sig_path.exists() {
        tracing::warn!(
            "Software integrity test: found legacy .hmac file at {} but no .sig file. \
             Migrate to Ed25519 signatures using tools/sign-integrity.sh. \
             The HMAC-based check has been removed.",
            hmac_path.display()
        );
    }

    // The .sig sidecar is mandatory in every build whenever a real public
    // key is embedded.  Refuse to start if it's missing — silently passing
    // would defeat the FIPS 140-3 §9.4 software-integrity test.
    if !sig_path.exists() {
        return Err(format!(
            "Software integrity test: no .sig file found at {} — required for all builds \
             when CRATON_HSM_INTEGRITY_PUBLIC_KEY is set. Sign the binary with \
             tools/sign-integrity.sh.",
            sig_path.display()
        ));
    }

    // Read the expected signature from the sidecar file (hex-encoded, 128 chars = 64 bytes)
    let sig_hex = match std::fs::read_to_string(&sig_path) {
        Ok(s) => s.trim().to_lowercase(),
        Err(e) => {
            return Err(format!(
                "Software integrity test: failed to read {}: {}",
                sig_path.display(),
                e
            ));
        }
    };

    if sig_hex.len() != 128 {
        return Err(format!(
            "Software integrity test: .sig file has invalid length {} (expected 128 hex chars / 64 bytes)",
            sig_hex.len()
        ));
    }

    // Read the module binary
    let module_bytes = match std::fs::read(&module_path) {
        Ok(b) => b,
        Err(e) => {
            return Err(format!(
                "Software integrity test: failed to read module binary {}: {}",
                module_path.display(),
                e
            ));
        }
    };

    // Decode signature from hex
    let sig_bytes = hex::decode(&sig_hex).map_err(|e| {
        format!(
            "Software integrity test: .sig file contains invalid hex: {}",
            e
        )
    })?;

    // Compute SHA-256 hash of the module binary.
    // We sign/verify the hash rather than the raw binary — Ed25519 already
    // hashes internally (SHA-512), but pre-hashing allows the signing tool
    // to work with a fixed-size digest rather than streaming the entire binary.
    let hash = Sha256::digest(&module_bytes);

    // Parse the Ed25519 public key
    let verifying_key = VerifyingKey::from_bytes(&INTEGRITY_PUBLIC_KEY).map_err(|e| {
        format!(
            "Software integrity test: invalid embedded public key: {}",
            e
        )
    })?;

    // Parse the signature
    let sig_array: [u8; 64] = sig_bytes.try_into().map_err(|_| {
        "Software integrity test: signature has wrong length (expected 64 bytes)".to_string()
    })?;
    let signature = Signature::from_bytes(&sig_array);

    // Verify the Ed25519 signature over the SHA-256 hash.
    // Ed25519 verification is inherently constant-time.
    if verifying_key.verify(&hash, &signature).is_err() {
        tracing::error!(
            "Software integrity test FAILED: Ed25519 signature verification failed for {}",
            module_path.display()
        );
        // Do NOT log signature or hash values — this is defense in depth.
        tracing::error!("  Re-sign the binary with tools/sign-integrity.sh if this is expected.");
        return Err(
            "Software integrity test: signature verification failed — module may be tampered"
                .to_string(),
        );
    }

    tracing::info!(
        "Software integrity test passed for {}",
        module_path.display()
    );
    Ok(())
}

/// Compute the SHA-256 hash of a file (used by the signing tool).
///
/// Returns the hex-encoded SHA-256 digest of the file contents.
pub fn compute_hash(path: &std::path::Path) -> Result<String, String> {
    let module_bytes =
        std::fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;

    let hash = Sha256::digest(&module_bytes);
    Ok(hex::encode(hash))
}

/// Determine the path of the currently loaded module.
///
/// On Unix: uses a function pointer address + `dladdr` to find the shared library path.
/// On Windows: uses `GetModuleHandleExW` to find the DLL path.
fn get_module_path() -> Option<PathBuf> {
    #[cfg(unix)]
    {
        get_module_path_unix()
    }

    #[cfg(windows)]
    {
        get_module_path_windows()
    }

    #[cfg(not(any(unix, windows)))]
    {
        None
    }
}

#[cfg(unix)]
fn get_module_path_unix() -> Option<PathBuf> {
    use std::ffi::CStr;

    // Use a function pointer from our module as the address to look up
    let addr = check_integrity as *const () as *mut libc::c_void;

    let mut info: libc::Dl_info = unsafe { std::mem::zeroed() };
    let result = unsafe { libc::dladdr(addr, &mut info) };

    if result != 0 && !info.dli_fname.is_null() {
        let path = unsafe { CStr::from_ptr(info.dli_fname) };
        path.to_str().ok().map(PathBuf::from)
    } else {
        None
    }
}

#[cfg(windows)]
fn get_module_path_windows() -> Option<PathBuf> {
    use std::ffi::OsString;
    use std::os::windows::ffi::OsStringExt;

    // GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 0x04
    // GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x02
    const FLAGS: u32 = 0x04 | 0x02;

    let addr = check_integrity as *const () as *const u8;
    let mut hmodule: windows_sys::Win32::Foundation::HMODULE = std::ptr::null_mut();

    let ok = unsafe {
        windows_sys::Win32::System::LibraryLoader::GetModuleHandleExW(
            FLAGS,
            addr as *const u16,
            &mut hmodule,
        )
    };

    if ok == 0 || hmodule.is_null() {
        return None;
    }

    let mut buf = vec![0u16; 4096];
    let len = unsafe {
        windows_sys::Win32::System::LibraryLoader::GetModuleFileNameW(
            hmodule,
            buf.as_mut_ptr(),
            buf.len() as u32,
        )
    };

    if len == 0 {
        return None;
    }

    let path = OsString::from_wide(&buf[..len as usize]);
    Some(PathBuf::from(path))
}

#[cfg(test)]
mod tests {
    use super::*;

    static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());

    #[test]
    #[cfg_attr(miri, ignore)]
    fn test_compute_hash_deterministic() {
        // Write a temp file, compute hash twice, verify same result
        let dir = std::env::temp_dir().join("craton_hsm_integrity_test");
        let _ = std::fs::create_dir_all(&dir);
        let test_file = dir.join("test_binary.bin");
        std::fs::write(&test_file, b"test module content").unwrap();

        let hash1 = compute_hash(&test_file).unwrap();
        let hash2 = compute_hash(&test_file).unwrap();
        assert_eq!(hash1, hash2, "SHA-256 hash should be deterministic");
        assert_eq!(hash1.len(), 64, "SHA-256 hex should be 64 chars");

        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    #[cfg_attr(miri, ignore)]
    fn test_integrity_check_placeholder_key_rejected_by_default() {
        // Without the dev-bypass env var, an all-zeros placeholder key MUST
        // cause check_integrity() to fail — this is the security invariant
        // that the previous "Ok(())" behavior silently violated.
        //
        // Serialize with all other env-var-mutating tests to avoid races under
        // --test-threads=N.
        if !is_placeholder_key() {
            return;
        }
        let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
        let prev = std::env::var(DEV_BYPASS_ENV).ok();
        unsafe { std::env::remove_var(DEV_BYPASS_ENV) };

        let result = check_integrity();

        // Restore previous value before asserting so a failure doesn't leak state.
        unsafe {
            match prev {
                Some(v) => std::env::set_var(DEV_BYPASS_ENV, v),
                None => std::env::remove_var(DEV_BYPASS_ENV),
            }
        }

        assert!(
            result.is_err(),
            "placeholder pubkey + no bypass must be a hard error"
        );
    }

    #[test]
    #[cfg_attr(miri, ignore)]
    fn test_integrity_check_dev_bypass_allows_placeholder() {
        // With the explicit dev-bypass env var AND a placeholder key, the
        // check returns Ok(()) (and logs a loud warning).  This is the only
        // sanctioned way to run `cargo test` against an unsigned build.
        if !is_placeholder_key() {
            return;
        }
        let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
        let prev = std::env::var(DEV_BYPASS_ENV).ok();
        unsafe { std::env::set_var(DEV_BYPASS_ENV, DEV_BYPASS_VALUE) };

        let result = check_integrity();

        unsafe {
            match prev {
                Some(v) => std::env::set_var(DEV_BYPASS_ENV, v),
                None => std::env::remove_var(DEV_BYPASS_ENV),
            }
        }

        assert!(
            result.is_ok(),
            "dev bypass with placeholder pubkey must pass: {:?}",
            result.err()
        );
    }

    #[test]
    #[cfg_attr(miri, ignore)]
    fn test_different_content_different_hash() {
        let dir = std::env::temp_dir().join("craton_hsm_integrity_test2");
        let _ = std::fs::create_dir_all(&dir);

        let file1 = dir.join("file1.bin");
        let file2 = dir.join("file2.bin");
        std::fs::write(&file1, b"content A").unwrap();
        std::fs::write(&file2, b"content B").unwrap();

        let hash1 = compute_hash(&file1).unwrap();
        let hash2 = compute_hash(&file2).unwrap();
        assert_ne!(
            hash1, hash2,
            "Different content should produce different hash"
        );

        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn test_signature_verification_roundtrip() {
        use ed25519_dalek::{Signer, SigningKey};

        // Generate a test keypair
        let signing_key = SigningKey::from_bytes(&[42u8; 32]);
        let verifying_key = signing_key.verifying_key();

        // Simulate: hash some content, sign it, verify it
        let content = b"test module binary content";
        let hash = Sha256::digest(content);
        let signature = signing_key.sign(&hash);

        assert!(
            verifying_key.verify(&hash, &signature).is_ok(),
            "Ed25519 signature should verify"
        );

        // Tampered content should fail
        let tampered = b"tampered module binary content";
        let tampered_hash = Sha256::digest(tampered);
        assert!(
            verifying_key.verify(&tampered_hash, &signature).is_err(),
            "Tampered content should fail verification"
        );
    }
}