408eacf7e2
Provide a password-store CLI that keeps pass's on-disk layout (<name>.gpg binary OpenPGP under $PASSWORD_STORE_DIR) but routes all encryption and decryption through a vault-plugin-secrets-gpg engine mount, so the GPG private key never lives on the client. - Implement init/show/ls/insert/generate/edit/rm/mv/cp/find/grep/git with pass-compatible flags and implicit `passv <name>`=show dispatch. - Resolve the recipient from a Vault ref: .gpg-id holds <mount>/<key> for passv-native stores, or a sibling .vault-id lets a store keep real GPG fingerprints in .gpg-id for dual gpg+Vault use. - mv/cp re-encrypt across differing recipients; same-recipient moves copy the ciphertext verbatim. - Ship the sibling build/packaging/CI: nfpm RPM to /usr/bin/passv (artifactapi rpm-internal on v* tag), Woodpecker PR/release pipelines, unit tests plus a real-Vault e2e that also proves dual gpg+Vault decryption. - README covers store creation, migrating a GPG pass store, and dual-mode.
130 lines
5.5 KiB
Bash
Executable File
130 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# End-to-end test for passv against a real Vault dev server running the
|
|
# vault-plugin-secrets-gpg engine. Exercises the full pass workflow: init,
|
|
# insert, show, generate, edit, mv/cp (with cross-gpg-id re-encryption), find,
|
|
# grep, rm — proving the store round-trips through Vault.
|
|
#
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
PLUGIN_BIN="${PLUGIN_BIN:-${ROOT_DIR}/../vault-plugin-secrets-gpg/dist/vault-plugin-secrets-gpg}"
|
|
|
|
red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
|
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
|
blue() { printf '\033[34m==> %s\033[0m\n' "$*"; }
|
|
fail() { red "FAIL: $*"; exit 1; }
|
|
|
|
command -v vault >/dev/null || fail "vault binary not found"
|
|
[ -x "${PLUGIN_BIN}" ] || fail "plugin binary not found at ${PLUGIN_BIN} (build it: make -C ../vault-plugin-secrets-gpg build)"
|
|
|
|
WORK="$(mktemp -d)"
|
|
PLUGIN_DIR="${WORK}/plugins"
|
|
export PASSWORD_STORE_DIR="${WORK}/store"
|
|
mkdir -p "${PLUGIN_DIR}" "${PASSWORD_STORE_DIR}"
|
|
cp "${PLUGIN_BIN}" "${PLUGIN_DIR}/vault-plugin-secrets-gpg"
|
|
|
|
export VAULT_ADDR="http://127.0.0.1:8281"
|
|
export VAULT_TOKEN="root"
|
|
|
|
cleanup() {
|
|
[ -n "${VAULT_PID:-}" ] && kill "${VAULT_PID}" 2>/dev/null || true
|
|
rm -rf "${WORK}"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
blue "Starting Vault dev server"
|
|
vault server -dev -dev-root-token-id=root -dev-listen-address=127.0.0.1:8281 \
|
|
-dev-plugin-dir="${PLUGIN_DIR}" >"${WORK}/vault.log" 2>&1 &
|
|
VAULT_PID=$!
|
|
|
|
for i in $(seq 1 30); do
|
|
vault status >/dev/null 2>&1 && break
|
|
sleep 0.5
|
|
[ "$i" = 30 ] && fail "vault did not become ready"
|
|
done
|
|
|
|
blue "Enabling gpg secrets engine + creating keys"
|
|
vault secrets enable -path=gpg vault-plugin-secrets-gpg >/dev/null
|
|
vault write -field=public_key gpg/keys/personal algorithm=rsa-2048 identity='Personal <me@unkin.net>' >/dev/null
|
|
vault write -field=public_key gpg/keys/work algorithm=ed25519 identity='Work <work@unkin.net>' >/dev/null
|
|
green "engine ready with keys gpg/personal, gpg/work"
|
|
|
|
PASSV="${ROOT_DIR}/dist/passv"
|
|
blue "Building passv"
|
|
make -C "${ROOT_DIR}" build >/dev/null
|
|
p() { "${PASSV}" "$@"; }
|
|
|
|
blue "init + insert + show"
|
|
p init gpg/personal
|
|
printf 's3cr3t\n' | p insert --echo email/gmail
|
|
printf 'hunter2\n' | p insert --echo email/work
|
|
[ "$(p show email/gmail)" = "s3cr3t" ] || fail "show email/gmail mismatch"
|
|
[ "$(p show email/work)" = "hunter2" ] || fail "show email/work mismatch"
|
|
# Files on disk must be real binary OpenPGP (magic byte 0x85/0x84... gpg packet).
|
|
head -c1 "${PASSWORD_STORE_DIR}/email/gmail.gpg" | od -An -tx1 | grep -qiE '8[45c]|c[1-9a-f]' \
|
|
|| fail "email/gmail.gpg is not a binary OpenPGP message"
|
|
green "insert/show round-trip via Vault OK"
|
|
|
|
blue "generate (+ in-place)"
|
|
gen="$(p generate -n banking/pin 12 | tail -1)"
|
|
[ "${#gen}" = 12 ] || fail "generated length wrong: '${gen}'"
|
|
[ "$(p show banking/pin)" = "${gen}" ] || fail "generated password not stored"
|
|
green "generate OK (${gen})"
|
|
|
|
blue "grep + find"
|
|
p grep s3cr3t | grep -q 'email/gmail' || fail "grep did not find secret"
|
|
p find gmail | grep -q 'email/gmail' || fail "find did not match"
|
|
green "grep/find OK"
|
|
|
|
blue "mv within same key (verbatim ciphertext move)"
|
|
p mv email/gmail email/personal-gmail
|
|
[ "$(p show email/personal-gmail)" = "s3cr3t" ] || fail "mv lost content"
|
|
p show email/gmail 2>/dev/null && fail "source still present after mv"
|
|
green "mv OK"
|
|
|
|
blue "cp across keys (re-encrypt personal -> work)"
|
|
p init -p projects gpg/work
|
|
p cp email/work projects/shared-login
|
|
[ "$(p show projects/shared-login)" = "hunter2" ] || fail "cross-key cp lost content"
|
|
# The copy must now be decryptable by the WORK key specifically.
|
|
ct="$(base64 -w0 "${PASSWORD_STORE_DIR}/projects/shared-login.gpg")"
|
|
[ "$(vault write -field=plaintext gpg/decrypt/work ciphertext="${ct}" | base64 -d)" = "hunter2" ] \
|
|
|| fail "copy was not re-encrypted to the work key"
|
|
green "cross-key cp re-encrypted correctly"
|
|
|
|
blue "rm"
|
|
p rm -f banking/pin
|
|
p show banking/pin 2>/dev/null && fail "entry present after rm"
|
|
green "rm OK"
|
|
|
|
blue "tree listing"
|
|
p ls | grep -q 'projects' || fail "tree missing projects dir"
|
|
|
|
if command -v gpg >/dev/null; then
|
|
blue "dual-mode: one entry readable by BOTH local gpg and Vault"
|
|
export GNUPGHOME="${WORK}/gnupg"
|
|
mkdir -p "${GNUPGHOME}"; chmod 700 "${GNUPGHOME}"
|
|
gpg --batch --passphrase '' --quick-generate-key 'Local Test <local@unkin.net>' default default never >/dev/null 2>&1
|
|
LOCAL_FPR="$(gpg --list-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')"
|
|
# Import the Vault key's public half so gpg can encrypt to it too.
|
|
vault read -field=public_key gpg/keys/personal | gpg --batch --import >/dev/null 2>&1
|
|
VAULT_FPR="$(vault read -field=fingerprint gpg/keys/personal)"
|
|
|
|
mkdir -p "${PASSWORD_STORE_DIR}/shared"
|
|
printf '%s\n%s\n' "${LOCAL_FPR}" "${VAULT_FPR}" > "${PASSWORD_STORE_DIR}/shared/.gpg-id" # for plain pass/gpg
|
|
echo 'gpg/personal' > "${PASSWORD_STORE_DIR}/shared/.vault-id" # for passv
|
|
# Encrypt to BOTH recipients, the way `pass` would in a multi-id store.
|
|
printf 'dualsecret' | gpg --batch --yes --trust-model always \
|
|
-r "${LOCAL_FPR}" -r "${VAULT_FPR}" --encrypt \
|
|
--output "${PASSWORD_STORE_DIR}/shared/db.gpg"
|
|
|
|
[ "$(gpg --batch --decrypt "${PASSWORD_STORE_DIR}/shared/db.gpg" 2>/dev/null)" = "dualsecret" ] \
|
|
|| fail "local gpg could not decrypt the dual-recipient entry"
|
|
[ "$(p show shared/db)" = "dualsecret" ] \
|
|
|| fail "passv (Vault) could not decrypt the dual-recipient entry"
|
|
green "dual-mode OK: same file opened by local gpg AND Vault"
|
|
fi
|
|
|
|
green "ALL PASSV END-TO-END CHECKS PASSED"
|