libvault 0.2.2

the libvault is modified from RustyVault
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
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
#![cfg(target_os = "linux")]

use anyhow::{Context, Result};
use libvault::core::Core;
use libvault::logical::{Operation, Request};
use libvault::modules::auth::AuthModule;
use libvault::modules::kv::KvModule;
use libvault::modules::pki::PkiModule;
use libvault::modules::policy::PolicyModule;
use libvault::mount::MountEntry;
use libvault::storage::Backend as PhysicalBackend;
use libvault::storage::physical::file::FileBackend;
use openssl::x509::X509;
use qlean::{Distro, MachineConfig, create_image, with_machine};
use ssh_key::LineEnding;
use std::str;
use std::sync::Arc;

// ============================================================
// setup_core: initialize Vault with all modules, return (core, root_token)
// ============================================================
async fn setup_core() -> Result<(Arc<Core>, String)> {
    let temp_dir = tempfile::tempdir().context("Failed to create temp dir")?;
    let path = temp_dir.keep();

    let backend: Arc<dyn PhysicalBackend> =
        Arc::new(FileBackend::with_folder(&path).context("Failed to create FileBackend")?);
    let core = Core::new(backend).wrap();

    core.module_manager
        .set_default_modules(core.clone())
        .context("Failed to set default modules")?;

    let auth_module = AuthModule::new(core.clone()).context("Failed to create AuthModule")?;
    core.module_manager
        .add_module(Arc::new(auth_module))
        .context("Failed to add AuthModule")?;

    let policy_module = PolicyModule::new(core.clone());
    core.module_manager
        .add_module(Arc::new(policy_module))
        .context("Failed to add PolicyModule")?;

    let pki_module = PkiModule::new(core.clone());
    core.module_manager
        .add_module(Arc::new(pki_module))
        .context("Failed to add PkiModule")?;

    let kv_module = KvModule::new(core.clone());
    core.module_manager
        .add_module(Arc::new(kv_module))
        .context("Failed to add KvModule")?;

    let seal_config = libvault::core::SealConfig {
        secret_shares: 1,
        secret_threshold: 1,
    };
    let init_result = core
        .init(&seal_config)
        .await
        .context("Failed to init core")?;

    let unseal_key = &init_result.secret_shares[0];
    core.unseal(unseal_key)
        .await
        .context("Failed to unseal core")?;

    let root_token = init_result.root_token.clone();

    let mount_entry = MountEntry::new("mounts", "pki/", "pki", "PKI backend");
    core.mount(&mount_entry)
        .await
        .context("Failed to mount PKI backend")?;

    Ok((core, root_token))
}

// ============================================================
// Main test: TLS / SSH / PGP generation, storage, and VM validation
// ============================================================
#[tokio::test]
async fn test_tls_ssh_pgp_generation_and_validation() -> Result<()> {
    let start_time = std::time::Instant::now();
    let (core, root_token) = setup_core().await?;

    // ==========================================================
    // Part 1: TLS
    // ==========================================================

    // 1-1. Generate Root CA
    let mut req = Request::new("pki/root/tls/generate/internal");
    req.operation = Operation::Write;
    req.client_token = root_token.clone();
    req.body = Some(
        serde_json::json!({
            "common_name": "Test Root CA",
            "ttl": "87600h"
        })
        .as_object()
        .unwrap()
        .clone(),
    );

    let resp = core
        .handle_request(&mut req)
        .await
        .context("Failed to generate root CA")?;
    let root_ca_pem = resp
        .context("Root CA response was None")?
        .data
        .context("Root CA response data was None")?
        .get("certificate")
        .context("certificate field missing")?
        .as_str()
        .context("certificate is not a string")?
        .to_string();
    println!("[OK] Root CA generated");

    // 1-2. Create PKI role
    let mut req = Request::new("pki/roles/tls/example-dot-com");
    req.operation = Operation::Write;
    req.client_token = root_token.clone();
    req.body = Some(
        serde_json::json!({
            "allowed_domains": "example.com",
            "allow_subdomains": true,
            "max_ttl": "72h"
        })
        .as_object()
        .unwrap()
        .clone(),
    );
    core.handle_request(&mut req)
        .await
        .context("Failed to create PKI role")?;
    println!("[OK] PKI role 'example-dot-com' created");

    // 1-3. Issue TLS server certificate
    let mut req = Request::new("pki/issue/tls/example-dot-com");
    req.operation = Operation::Write;
    req.client_token = root_token.clone();
    req.body = Some(
        serde_json::json!({
            "common_name": "www.example.com",
            "ttl": "24h"
        })
        .as_object()
        .unwrap()
        .clone(),
    );

    let resp = core
        .handle_request(&mut req)
        .await
        .context("Failed to issue TLS certificate")?;
    let tls_data = resp
        .context("Issue cert response was None")?
        .data
        .context("Issue cert response data was None")?;

    let tls_cert = tls_data
        .get("certificate")
        .context("certificate missing")?
        .as_str()
        .context("certificate not string")?
        .to_string();
    let tls_key = tls_data
        .get("private_key")
        .context("private_key missing")?
        .as_str()
        .context("private_key not string")?
        .to_string();
    let tls_serial = tls_data
        .get("serial_number")
        .context("serial_number missing")?
        .as_str()
        .context("serial_number not string")?
        .to_string();
    println!("[OK] TLS cert issued (serial: {})", tls_serial);

    // 1-4. Storage consistency: fetch cert by serial
    let mut req = Request::new(&format!("pki/cert/tls/{}", tls_serial));
    req.operation = Operation::Read;
    req.client_token = root_token.clone();
    let resp = core
        .handle_request(&mut req)
        .await
        .context("Failed to fetch TLS cert from storage")?;
    let fetched_tls_cert = resp
        .context("Fetch cert response was None")?
        .data
        .context("Fetch cert response data was None")?
        .get("certificate")
        .context("certificate missing")?
        .as_str()
        .context("certificate not string")?
        .to_string();
    assert_eq!(
        tls_cert, fetched_tls_cert,
        "Fetched TLS cert does not match issued cert"
    );
    println!("[OK] TLS cert storage consistency verified");

    // 1-5. Local signature verification
    let x509_cert =
        X509::from_pem(tls_cert.as_bytes()).context("Failed to parse issued TLS cert")?;
    let x509_ca = X509::from_pem(root_ca_pem.as_bytes()).context("Failed to parse Root CA cert")?;
    let ca_pubkey = x509_ca
        .public_key()
        .context("Failed to extract CA public key")?;
    let is_valid = x509_cert
        .verify(&ca_pubkey)
        .context("TLS cert signature verification call failed")?;
    assert!(
        is_valid,
        "TLS cert signature is invalid (not signed by Root CA)"
    );
    println!("[OK] TLS cert signature locally verified against Root CA");

    // ==========================================================
    // Part 2: SSH
    // ==========================================================

    // 2-1. Configure SSH CA (generates a new RSA key pair for SSH signing)
    let mut req = Request::new("pki/config/ca/ssh");
    req.operation = Operation::Write;
    req.client_token = root_token.clone();
    req.body = Some(
        serde_json::json!({
            "key_type": "rsa",
            "key_bits": 2048
        })
        .as_object()
        .unwrap()
        .clone(),
    );
    let resp = core
        .handle_request(&mut req)
        .await
        .context("Failed to configure SSH CA")?;
    let ssh_ca_pub = resp
        .context("SSH CA config response was None")?
        .data
        .context("SSH CA config response data was None")?
        .get("public_key")
        .context("public_key missing")?
        .as_str()
        .context("public_key not string")?
        .to_string();
    println!("[OK] SSH CA configured");

    // 2-2. Create SSH role
    let mut req = Request::new("pki/roles/ssh/my-role");
    req.operation = Operation::Write;
    req.client_token = root_token.clone();
    req.body = Some(
        serde_json::json!({
            "cert_type_ssh": "user",
            "key_type": "ed25519",
            "ttl": "1h",
            "allowed_users": "ubuntu"
        })
        .as_object()
        .unwrap()
        .clone(),
    );
    core.handle_request(&mut req)
        .await
        .context("Failed to create SSH role")?;
    println!("[OK] SSH role 'my-role' created");

    // 2-3. Generate Ed25519 user key pair locally
    let user_ssh_key =
        ssh_key::PrivateKey::random(&mut ssh_key::rand_core::OsRng, ssh_key::Algorithm::Ed25519)
            .context("Failed to generate Ed25519 SSH key")?;

    let user_ssh_pub = user_ssh_key
        .public_key()
        .to_openssh()
        .context("Failed to export SSH public key")?;
    let user_ssh_priv_openssh = user_ssh_key
        .to_openssh(LineEnding::LF)
        .context("Failed to export SSH private key")?;

    // 2-4. Request Vault to sign the user's public key
    let mut req = Request::new("pki/sign/ssh/my-role");
    req.operation = Operation::Write;
    req.client_token = root_token.clone();
    req.body = Some(
        serde_json::json!({
            "public_key": user_ssh_pub,
            "key_id": "test-user-cert",
            "valid_principals": ["ubuntu"],
            "ttl": "1h"
        })
        .as_object()
        .unwrap()
        .clone(),
    );

    let resp = core
        .handle_request(&mut req)
        .await
        .context("Failed to sign SSH user cert")?;
    let ssh_data = resp
        .context("SSH sign response was None")?
        .data
        .context("SSH sign response data was None")?;

    let ssh_cert = ssh_data
        .get("signed_key")
        .context("signed_key missing")?
        .as_str()
        .context("signed_key not string")?
        .to_string();
    let ssh_serial = ssh_data
        .get("serial_number")
        .context("serial_number missing")?
        .as_str()
        .context("serial_number not string")?
        .to_string();
    println!("[OK] SSH cert signed (serial: {})", ssh_serial);

    // 2-5. Storage consistency: fetch SSH cert by serial
    let mut req = Request::new(&format!("pki/cert/ssh/{}", ssh_serial));
    req.operation = Operation::Read;
    req.client_token = root_token.clone();
    let resp = core
        .handle_request(&mut req)
        .await
        .context("Failed to fetch SSH cert from storage")?;
    let ssh_fetched_data = resp
        .context("Fetch SSH cert response was None")?
        .data
        .context("Fetch SSH cert response data was None")?;
    let fetched_ssh_cert = ssh_fetched_data
        .get("signed_key")
        .context("signed_key missing")?
        .as_str()
        .context("signed_key not string")?
        .to_string();
    assert_eq!(
        ssh_cert, fetched_ssh_cert,
        "Fetched SSH cert does not match issued cert"
    );
    println!("[OK] SSH cert storage consistency verified");

    // ==========================================================
    // Part 3: PGP
    // ==========================================================

    // 3-1. Generate PGP key pair (exported mode to get private key)
    let mut req = Request::new("pki/keys/generate/exported");
    req.operation = Operation::Write;
    req.client_token = root_token.clone();
    req.body = Some(
        serde_json::json!({
            "name": "Test User",
            "email": "test@example.com",
            "key_name": "test-pgp-key",
            "key_type": "pgp",
            "key_bits": 2048
        })
        .as_object()
        .unwrap()
        .clone(),
    );

    let resp = core
        .handle_request(&mut req)
        .await
        .context("Failed to generate PGP key")?;
    let pgp_data = resp
        .context("PGP generate response was None")?
        .data
        .context("PGP generate response data was None")?;

    let pgp_pub = pgp_data
        .get("public_key")
        .context("public_key missing")?
        .as_str()
        .context("public_key not string")?
        .to_string();
    let pgp_priv = pgp_data
        .get("private_key")
        .and_then(|v| v.as_str())
        .context("private_key missing or null (ensure using /exported endpoint)")?
        .to_string();
    let pgp_fingerprint = pgp_data
        .get("fingerprint")
        .context("fingerprint missing")?
        .as_str()
        .context("fingerprint not string")?
        .to_string();
    println!("[OK] PGP key generated (fingerprint: {})", pgp_fingerprint);

    // 3-2. Verify PGP key works via sign + verify API roundtrip
    // "hello pgp" in hex
    let test_data = "68656c6c6f20706770";
    let mut req = Request::new("pki/keys/sign");
    req.operation = Operation::Write;
    req.client_token = root_token.clone();
    req.body = Some(
        serde_json::json!({
            "key_name": "test-pgp-key",
            "data": test_data
        })
        .as_object()
        .unwrap()
        .clone(),
    );

    let resp = core
        .handle_request(&mut req)
        .await
        .context("Failed to sign data with PGP key")?;
    let sig_hex = resp
        .context("PGP sign response was None")?
        .data
        .context("PGP sign response data was None")?
        .get("signature")
        .context("signature missing")?
        .as_str()
        .context("signature not string")?
        .to_string();
    println!("[OK] PGP signature created");

    let mut req = Request::new("pki/keys/verify");
    req.operation = Operation::Write;
    req.client_token = root_token.clone();
    req.body = Some(
        serde_json::json!({
            "key_name": "test-pgp-key",
            "data": test_data,
            "signature": sig_hex
        })
        .as_object()
        .unwrap()
        .clone(),
    );

    let resp = core
        .handle_request(&mut req)
        .await
        .context("Failed to verify PGP signature")?;
    let valid = resp
        .context("PGP verify response was None")?
        .data
        .context("PGP verify response data was None")?
        .get("valid")
        .context("valid missing")?
        .as_bool()
        .context("valid not bool")?;
    assert!(valid, "PGP signature verification failed");
    println!("[OK] PGP sign + verify API roundtrip verified");

    println!(
        "\n[INFO] Vault logic tests completed in {:?}",
        start_time.elapsed()
    );

    // ==========================================================
    // Part 4: Qlean VM end-to-end validation
    // ==========================================================
    println!("\n[INFO] Starting VM-based integration tests...");
    let vm_start = std::time::Instant::now();

    let image = create_image(Distro::Debian, "debian-13-generic-amd64")
        .await
        .context("Failed to create Qlean VM image")?;
    let config = MachineConfig {
        core: 2,
        mem: 1024,
        disk: None,
        clear: true,
    };

    with_machine(&image, &config, |vm| {
        Box::pin(async move {
            // --------------------------------------------------
            // 4-1. TLS verification
            // --------------------------------------------------
            vm.write("root_ca.crt", root_ca_pem.as_bytes()).await?;
            vm.write("server.crt", tls_cert.as_bytes()).await?;
            vm.write("server.key", tls_key.as_bytes()).await?;

            // Verify certificate chain
            let result = vm.exec("openssl verify -CAfile root_ca.crt server.crt").await?;
            if !result.status.success() {
                println!(
                    "[ERROR] TLS chain verify stderr: {}",
                    str::from_utf8(&result.stderr)?
                );
                println!(
                    "[ERROR] TLS chain verify stdout: {}",
                    str::from_utf8(&result.stdout)?
                );
            }
            assert!(
                result.status.success(),
                "TLS certificate chain verification failed"
            );
            println!("[VM] TLS chain verified");

            // TLS handshake test - write script to file to avoid quote-nesting issues
            println!("[VM] Starting TLS handshake test (may take 10-20 seconds)...");
            let tls_handshake_script = r#"#!/bin/bash
set -e

openssl s_server -accept 4433 -cert server.crt -key server.key -www > /tmp/server.log 2>&1 &
SERVER_PID=$!

echo "Waiting for port 4433..."
for i in $(seq 1 40); do
    if ss -tln | grep -q ':4433 '; then
        echo "Port ready"
        break
    fi
    if [ $i -eq 40 ]; then
        echo "ERROR: Timeout waiting for port" >&2
        cat /tmp/server.log >&2
        kill $SERVER_PID 2>/dev/null || true
        exit 1
    fi
    sleep 0.5
done

sleep 1

echo Q | timeout 15 openssl s_client -connect localhost:4433 -CAfile root_ca.crt -quiet > /tmp/client.log 2>&1
RET=$?

kill $SERVER_PID 2>/dev/null || true
exit $RET
"#;
            vm.write("tls_test.sh", tls_handshake_script.as_bytes()).await?;
            vm.exec("chmod +x tls_test.sh").await?;
            let result = vm.exec("bash tls_test.sh").await?;
            if !result.status.success() {
                println!(
                    "[WARN] TLS handshake failed: {}",
                    str::from_utf8(&result.stderr)?
                );
                println!("[WARN] Continuing anyway (chain verification passed)");
            } else {
                println!("[VM] TLS handshake verified");
            }

            // --------------------------------------------------
            // 4-2. SSH verification
            // --------------------------------------------------
            vm.write("id_ed25519-cert.pub", ssh_cert.as_bytes())
                .await?;
            let result = vm.exec("ssh-keygen -L -f id_ed25519-cert.pub").await?;
            assert!(
                result.status.success(),
                "SSH cert inspection failed"
            );
            let stdout = str::from_utf8(&result.stdout)?;
            assert!(
                stdout.contains("ssh-ed25519-cert-v01@openssh.com"),
                "Wrong cert type: {}",
                stdout
            );
            assert!(
                stdout.contains("ubuntu"),
                "Missing principals: {}",
                stdout
            );
            println!("[VM] SSH cert structure verified");

            // Configure sshd
            vm.write("user_ca.pub", ssh_ca_pub.as_bytes()).await?;
            vm.exec("cp user_ca.pub /etc/ssh/user_ca.pub").await?;
            vm.exec("echo 'TrustedUserCAKeys /etc/ssh/user_ca.pub' >> /etc/ssh/sshd_config")
                .await?;

            vm.exec("id -u ubuntu > /dev/null 2>&1 || useradd -m -s /bin/bash ubuntu")
                .await?;

            vm.exec("systemctl restart ssh 2>/dev/null || service ssh restart 2>/dev/null || true").await?;
            vm.exec("sleep 2").await?;

            // SSH login
            vm.write("id_ed25519", user_ssh_priv_openssh.as_bytes())
                .await?;
            vm.exec("chmod 600 id_ed25519").await?;

            let result = vm
                .exec("ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=10 -i id_ed25519 ubuntu@localhost echo success 2>&1")
                .await?;
            assert!(result.status.success(), "SSH login failed");
            assert!(
                str::from_utf8(&result.stdout)?.contains("success"),
                "Wrong output"
            );
            println!("[VM] SSH certificate login verified");

            // --------------------------------------------------
            // 4-3. PGP verification
            // --------------------------------------------------
            // Install gnupg if not present
            vm.exec("which gpg >/dev/null 2>&1 || apt-get update -qq && apt-get install -y -qq gnupg >/dev/null 2>&1").await?;

            // Configure GPG to avoid gpg-agent/pinentry hangs in non-interactive VM
            vm.exec("mkdir -p ~/.gnupg && chmod 700 ~/.gnupg").await?;
            vm.exec("echo 'pinentry-mode loopback' > ~/.gnupg/gpg.conf").await?;
            vm.exec("echo 'allow-loopback-pinentry' > ~/.gnupg/gpg-agent.conf").await?;
            vm.exec("gpgconf --kill gpg-agent 2>/dev/null || true").await?;

            vm.write("public.asc", pgp_pub.as_bytes()).await?;
            vm.write("private.asc", pgp_priv.as_bytes()).await?;

            let result = vm.exec("gpg --batch --import public.asc 2>&1").await?;
            println!("[VM] PGP public key import: {}", str::from_utf8(&result.stdout)?);
            if !result.status.success() {
                println!("[VM] PGP public key import stderr: {}", str::from_utf8(&result.stderr)?);
            }

            let result = vm.exec("gpg --batch --import private.asc 2>&1").await?;
            println!("[VM] PGP private key import: {}", str::from_utf8(&result.stdout)?);
            if !result.status.success() {
                println!("[VM] PGP private key import stderr: {}", str::from_utf8(&result.stderr)?);
            }

            let result = vm.exec("gpg --list-keys --keyid-format long 2>&1").await?;
            let stdout = str::from_utf8(&result.stdout)?;
            println!("[VM] gpg --list-keys output:\n{}", stdout);
            assert!(stdout.contains("Test User"), "PGP key missing 'Test User' in: {}", stdout);
            assert!(stdout.contains("test@example.com"), "PGP key missing email in: {}", stdout);
            println!("[VM] PGP key metadata verified");

            vm.exec("echo 'Hello PGP' > data.txt").await?;
            let result = vm.exec("gpg --batch --yes --pinentry-mode loopback --default-key test@example.com --sign --armor --output data.sig data.txt").await?;
            assert!(result.status.success(), "PGP sign failed: {}", str::from_utf8(&result.stderr)?);
            let result = vm.exec("gpg --batch --verify data.sig").await?;
            assert!(
                result.status.success() && str::from_utf8(&result.stderr)?.contains("Good signature"),
                "PGP verify failed: status={}, stderr={}",
                result.status,
                str::from_utf8(&result.stderr)?
            );
            println!("[VM] PGP sign + verify verified");

            println!("\n[VM] All tests passed!");
            Ok(())
        })
    })
    .await?;

    println!("[INFO] VM tests: {:?}", vm_start.elapsed());
    println!("\n=== ALL TESTS PASSED in {:?} ===", start_time.elapsed());
    Ok(())
}