aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
156 lines (115 loc) • 6.61 kB
Markdown
---
id: crypto-flag-verification
severity: HIGH
applies_to: [all-agents, applied-cryptographer]
tags: [cryptography, cli-tools, openssl, defaults]
---
# Cryptographic CLI Flag Verification
**Enforcement Level**: HIGH
**Scope**: Any code or script invoking low-level crypto CLI tools
**Framework**: security-engineering
## Rule
When invoking low-level cryptographic CLI tools (`openssl`, `gpg`, `age`, `7z`, `dd` with crypto pipes, etc.), the invocation MUST specify all KDF, mode, and iteration parameters explicitly. Tool defaults are frequently insecure or deprecated, and silent fallbacks are common. Specifically:
- `openssl enc` MUST include `-pbkdf2 -iter <N>` (N ≥ 600,000 for SHA-256) AND an AEAD mode (`-aes-256-gcm`, `-chacha20-poly1305` if available). Without `-pbkdf2`, `openssl enc` defaults to `EVP_BytesToKey` — a single MD5 iteration. Without an AEAD mode, the output is unauthenticated (also see `no-unauthenticated-encryption`).
- `gpg --symmetric` MUST include `--s2k-mode 3 --s2k-count <high>` AND `--s2k-cipher-algo AES256 --s2k-digest-algo SHA512`. Default S2K count varies by version and is often insufficient.
- `7z` archive encryption is acceptable for casual use; for security-sensitive archives, use `age` or a libsodium-based program instead.
- `zip --encrypt` (legacy ZipCrypto) is BROKEN. Use 7z or age.
## Why
CLI crypto tools are designed for backwards compatibility and breadth, not for safe defaults. `openssl enc` in particular has multiple footguns:
- Default key derivation (`-pass`) without `-pbkdf2` is `EVP_BytesToKey`, which uses a single MD5 iteration over `password || salt`. Brute-force speed: ~10^9 attempts/sec on a single GPU. A 10-character password is recovered in seconds.
- Default mode is CBC (unauthenticated). `-aes-256-cbc` is unauthenticated, allowing tampering and padding-oracle attacks.
- Salt is auto-generated by default (good) but the file format binds salt and ciphertext loosely (`Salted__` magic + 8 bytes), which has been deprecated in OpenSSL 3.0+.
The combination of weak default KDF + unauthenticated mode is what review finding H6 caught: `openssl enc -aes-256-cbc -pass fd:0` was nominally claimed to use "PBKDF2 100k iterations", but the actual command line lacked `-pbkdf2 -iter`, making the protection ~five orders of magnitude weaker than claimed.
This rule exists because the cost of confirming a flag is grep, and the cost of missing a flag is total compromise.
Source: review finding H6 (2026-05-03 gap analysis).
## How to apply
### Detection
Grep patterns to flag for review:
```bash
# openssl enc without -pbkdf2 — BLOCK
grep -rn "openssl enc" --include="*.sh" --include="*.py" --include="Makefile" \
| grep -v "\-pbkdf2"
# openssl enc with CBC — BLOCK (also no-unauthenticated-encryption)
grep -rn "openssl enc.*-aes.*-cbc"
# openssl enc reading from stdin without explicit flags
grep -rn "openssl enc" | grep -v "\-iter"
# gpg --symmetric without explicit S2K
grep -rn "gpg --symmetric" --include="*.sh" \
| grep -v "\-\-s2k-mode 3"
# zip --encrypt — broken construction
grep -rn "zip.*\-\-encrypt"
grep -rn "zip.*-e " | grep -v "/\*"
```
### Remediation
#### `openssl enc` — required form
```bash
# Correct invocation: AEAD mode + explicit KDF + iteration count
openssl enc -aes-256-gcm \
-pbkdf2 -iter 600000 \
-in plaintext \
-out ciphertext \
-pass fd:0
```
But for new code, **don't use `openssl enc`**. Use a small Python program around libsodium instead:
```python
#!/usr/bin/env python3
"""Encrypt stdin to stdout using XChaCha20-Poly1305.
Reads passphrase from fd 3 (caller responsibility to pipe it cleanly).
"""
import os, sys
import nacl.secret, nacl.pwhash, nacl.utils
passphrase = os.read(3, 4096).rstrip(b"\n")
salt = nacl.utils.random(nacl.pwhash.argon2id.SALTBYTES)
key = nacl.pwhash.argon2id.kdf(
nacl.secret.SecretBox.KEY_SIZE,
passphrase, salt,
opslimit=nacl.pwhash.argon2id.OPSLIMIT_INTERACTIVE,
memlimit=nacl.pwhash.argon2id.MEMLIMIT_INTERACTIVE,
)
box = nacl.secret.SecretBox(key)
plaintext = sys.stdin.buffer.read()
ct = box.encrypt(plaintext)
# wire format: salt (16) || ct (24-byte nonce + plaintext + 16-byte tag)
sys.stdout.buffer.write(salt + ct)
```
The whole program is ~20 lines, has one dependency (`pynacl`, audited C lib underneath), uses Argon2id for the password, and produces an AEAD ciphertext with all parameters explicit.
#### `gpg --symmetric` — required form
```bash
gpg --symmetric \
--s2k-mode 3 \
--s2k-count 65011712 \
--s2k-cipher-algo AES256 \
--s2k-digest-algo SHA512 \
--compress-algo none \
--batch --passphrase-fd 0 \
-o ciphertext.gpg \
plaintext
```
`s2k-count` must be a power of two between 1024 and 65011712 (per RFC 4880). Use the maximum unless latency is unacceptable.
#### `age` — generally safe defaults
```bash
# Encrypt to a recipient (X25519 or SSH key)
age -r age1xyz... -o ct.age plaintext
# Encrypt with passphrase (prompts interactively; uses scrypt internally)
age -p -o ct.age plaintext
```
`age` has good defaults; verify the binary signature before use, and pin a specific version in production scripts.
## Verification checklist for any crypto CLI invocation
Before approving any code that calls a crypto CLI:
- [ ] Mode is AEAD (or paired with a separate MAC per `no-unauthenticated-encryption`)
- [ ] KDF is explicit (`-pbkdf2 -iter N`, `--s2k-mode 3 --s2k-count N`)
- [ ] Iteration / cost parameter is current per OWASP guidance
- [ ] Password/key is read from fd or env, never `-k password` or `-pass pass:...` (visible in `ps`)
- [ ] Output format embeds enough metadata for clean decryption (KDF params, salt, mode)
- [ ] No silent fallbacks (e.g., `openssl enc` without `-aes-*` flag has version-dependent default)
- [ ] Decryption verifies authentication BEFORE any structural processing (padding, parsing)
## Linked rules
- `no-unauthenticated-encryption` — most common pairing: missing flags AND missing authentication
- `no-adhoc-kdf` — `openssl enc` without `-pbkdf2` falls back to an ad-hoc-equivalent KDF (single MD5)
- `no-key-reuse-across-purposes` — when a CLI tool internally derives multiple keys, verify they're domain-separated
## References
- OpenSSL `enc(1)` man page — read the FILE FORMATS and OPTIONS sections
- RFC 4880 §3.7 — OpenPGP S2K (string-to-key) specification
- OWASP Cryptographic Storage Cheat Sheet — symmetric encryption guidance
- "PSA: Don't use `openssl enc`" — recurring blog topic (search "openssl enc considered harmful")
- Filippo Valsorda's `age` specification — `age-encryption.org/v1`