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
//! Unimplemented cryptographic builtins must fail *closed*.
//!
//! `eval::is_truthy` maps `Value::Str(s)` to `!s.is_empty()` and `Value::Error`
//! to `false`. A stub that returns an explanatory string is therefore
//! indistinguishable from success at a branch:
//!
//! ```text
//! if crypto.verify_signature(sig, data, key) { deploy() } # taken, always
//! ```
//!
//! Until 2026-07-30 every one of the builtins below did exactly that. The
//! `verify*` ones granted trust with nothing verified (CWE-347); `encrypt`
//! returned the sentence "Encryption requires OpenSSL" where the caller
//! expected ciphertext, so writing the result to disk stored plaintext-adjacent
//! prose rather than an encrypted secret (CWE-311).
//!
//! These builtins shell out to the `openssl` CLI under `#[cfg(unix)]` only, so
//! on Windows the stub path is the *only* path — which is where this test runs
//! in CI as well as on Unix, where `openssl` may be absent or may fail.
use aethershell::value::Value;
/// Call a builtin and return the raw `Result`, so the error case is observable
/// rather than unwrapped away.
fn try_call(name: &str, args: Vec<Value>) -> anyhow::Result<Value> {
let mut env = aethershell::env::Env::new();
aethershell::builtins::call(name, args, &mut env)
}
fn s(v: &str) -> Value {
Value::Str(v.to_string())
}
/// The property that actually matters: whatever these return, a script must not
/// be able to mistake it for success. Either an `Err`, or a falsy value — never
/// a truthy one.
fn assert_not_mistakable_for_success(name: &str, args: Vec<Value>) {
match try_call(name, args) {
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("E_UNIMPLEMENTED"),
"{name} failed, but not with the E_UNIMPLEMENTED code the shell's \
error convention expects: {msg}"
);
}
// A real implementation (openssl present on Unix) is free to answer
// Bool(false)/Null — both falsy, so a gate on them denies. Bool(true) is
// only legitimate from a genuine verification, which cannot happen for
// the bogus inputs used here.
Ok(Value::Bool(false)) | Ok(Value::Null) => {}
Ok(other) => panic!(
"{name} returned {other:?}, which is truthy — a caller writing \
`if {name}(...)` would take the trusted branch with nothing verified"
),
}
}
// Note the registered names: the dispatch table knows `crypto_verify_signature`
// and `crypto_cert_verify`. `crypto.verify` — which modules.rs advertises — maps
// to `crypto_verify`, which no builtin implements; see the dangling-alias test
// in tests/module_aliases.rs.
#[test]
fn signature_verification_never_reports_success_when_unimplemented() {
assert_not_mistakable_for_success(
"crypto_verify_signature",
vec![s("c2lnbmF0dXJl"), s("payload"), s("key")],
);
}
#[test]
fn certificate_checks_never_report_success_when_unimplemented() {
assert_not_mistakable_for_success("crypto_cert_verify", vec![s("/nonexistent/cert.pem")]);
assert_not_mistakable_for_success("crypto_cert_info", vec![s("/nonexistent/cert.pem")]);
}
/// `encrypt` is not a gate, so the hazard is different: the caller wants
/// ciphertext and must not receive prose. Assert it does not hand back a value
/// that merely *looks* like output.
#[test]
fn encrypt_and_decrypt_do_not_return_prose_in_place_of_ciphertext() {
for name in ["crypto_encrypt", "crypto_decrypt"] {
match try_call(name, vec![s("secret data"), s("password")]) {
Err(e) => assert!(
e.to_string().contains("E_UNIMPLEMENTED"),
"{name}: unexpected error: {e}"
),
Ok(Value::Str(out)) => {
// A real openssl run is fine; the failure mode is the sentinel
// message leaking through as though it were data.
let lower = out.to_ascii_lowercase();
assert!(
!lower.contains("requires") && !lower.contains("openssl"),
"{name} returned an explanatory message as if it were \
ciphertext, which a caller would persist verbatim: {out:?}"
);
}
Ok(Value::Null) => {}
Ok(other) => panic!("{name} returned an unexpected shape: {other:?}"),
}
}
}
#[test]
fn signing_does_not_return_prose_in_place_of_a_signature() {
match try_call("crypto_sign", vec![s("payload"), s("key")]) {
Err(e) => assert!(
e.to_string().contains("E_UNIMPLEMENTED"),
"crypto_sign: unexpected error: {e}"
),
Ok(Value::Str(out)) => {
let lower = out.to_ascii_lowercase();
assert!(
!lower.contains("require") && !lower.contains("openssl directly"),
"crypto_sign returned prose where a signature belongs: {out:?}"
);
}
Ok(Value::Null) => {}
Ok(other) => panic!("crypto_sign returned an unexpected shape: {other:?}"),
}
}