Security hardening and release readiness
ober
c19c4cea6b30582f6ecc21d1100dc51a1bb6e88a
new file mode 100644 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + JERBOA_VERSION: v0.2.3 + JERBUILD: ${{ github.workspace }}/.jerboa/bin/jerbuild + +jobs: + build-test-audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install system tools + run: | + set -eu + sudo apt-get update + sudo apt-get install -y --no-install-recommends build-essential curl ca-certificates git pkg-config + command -v cargo + cargo --version + + - name: Install Jerboa toolchain + run: sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin + + - name: Test + run: make test + + - name: CLI version smoke + run: make run ARGS='version' + + - name: Install cargo-audit + run: cargo install cargo-audit --locked + + - name: Release evidence + run: make release-evidence new file mode 100644 --- /dev/null +++ b/.github/workflows/security-baseline.yml @@ -0,0 +1,35 @@ +name: Security Baseline + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + baseline: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Required release files + run: | + set -eu + test -f LICENSE + test -f SECURITY.md + test -f .gitignore + find . -maxdepth 1 -iname "README*" -type f | grep -q . + + - name: High-confidence secret scan + run: | + set -eu + pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-(ant-api03|proj|svcacct)-[A-Za-z0-9_-]{30,}|AKIA[0-9A-Z]{16})" + matches="$(git grep -n -I -E "$pattern" -- . ":!*.png" ":!*.jpg" ":!*.jpeg" ":!*.gif" ":!*.so" ":!*.dylib" ":!*.o" ":!*.a" ":!*.boot" ":!*.tar.gz" || true)" + if [ -n "$matches" ]; then + echo "$matches" + echo "High-confidence secret pattern found." + exit 1 + fi --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ # Rust build artifacts pgp-native/target/ -pgp-native/Cargo.lock # Jerboa / Chez compiled output +.jerboa/ +dist/ *.so *.dylib *.wpo --- a/.jerboa/security.json +++ b/.jerboa/security.json @@ -16,7 +16,21 @@ "process": { "shellInterpolation": "deny" }, "network": { "requireTimeouts": false }, "eval": { "stringEval": "deny", "bareRead": "deny", "allowReadEval": false }, - "crypto": { "zeroizeSecrets": true, "strictKeyFilePermissions": true } + "crypto": { "zeroizeSecrets": true, "strictKeyFilePermissions": true }, + "targetEvidence": { + "keyProof": "fail-closed", + "requiredMarkers": [ + "age_encrypt_decrypt_status=target-evidence-recorded", + "signature_roundtrip_status=target-evidence-recorded", + "identity_file_permissions_status=target-evidence-recorded", + "passphrase_non_disclosure_status=target-evidence-recorded", + "decrypt_error_redaction_status=target-evidence-recorded", + "openpgp_disabled_status=confirmed", + "target_platform_smoke_status=target-evidence-recorded", + "production_crypto_review_status=current-run-recorded", + "sensitive_artifact_policy=no-secret-identities-passphrases-plaintext-or-decrypted-payloads-preserved" + ] + } }, "suppressions": [] } new file mode 100644 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. --- a/Makefile +++ b/Makefile @@ -1,30 +1,41 @@ -JERBOA_HOME ?= $(realpath $(CURDIR)/../jerboa) -SCHEME ?= $(JERBOA_HOME)/.chez/bin/scheme +JERBOA_VERSION ?= v0.2.3 +JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin +JERBUILD ?= $(shell if [ -x "$(CURDIR)/../jerboa/dist/jerbuild" ]; then printf '%s\n' "$(CURDIR)/../jerboa/dist/jerbuild"; elif command -v jerbuild >/dev/null 2>&1; then command -v jerbuild; else printf '%s\n' "$(JERBOA_TOOL_DIR)/jerbuild"; fi) +SIBLING_JERBOA := $(realpath $(CURDIR)/../jerboa) +JH = $(shell "$(JERBUILD)" --jerboa-home 2>/dev/null) +JERBOA_HOME ?= $(JH) +JERBOA_SOURCE_HOME ?= $(if $(wildcard $(SIBLING_JERBOA)/lib),$(SIBLING_JERBOA),$(JERBOA_HOME)) +SCHEME ?= $(if $(wildcard $(JERBOA_SOURCE_HOME)/.chez/bin/scheme),$(JERBOA_SOURCE_HOME)/.chez/bin/scheme,$(JERBOA_HOME)/.chez/bin/scheme) BIN_DIR := $(HOME)/.local/bin LIB_DIR := $(HOME)/.local/lib MAN_DIR := $(HOME)/.local/share/man/man1 +DIST_DIR ?= $(CURDIR)/dist +RELEASE_EVIDENCE_DIR ?= $(DIST_DIR)/release-evidence +TARGET_EVIDENCE_DIR ?= $(DIST_DIR)/target-evidence NATIVE_DIR := $(CURDIR)/pgp-native NATIVE_RELEASE := $(NATIVE_DIR)/target/release +NATIVE_STATICLIB := $(NATIVE_RELEASE)/libjpgp_native.a ifeq ($(shell uname -s),Darwin) NATIVE_LIB := $(NATIVE_RELEASE)/libjpgp_native.dylib else NATIVE_LIB := $(NATIVE_RELEASE)/libjpgp_native.so endif -XC_LIBDIRS := $(CURDIR):$(JERBOA_HOME)/lib:$(JERBOA_HOME)/.chez-cross-ta6le/lib:$(JERBOA_HOME)/.chez-cross-ta6fb/lib +XC_LIBDIRS := $(CURDIR):$(JERBOA_SOURCE_HOME)/lib:$(JERBOA_SOURCE_HOME)/.chez-cross-ta6le/lib:$(JERBOA_SOURCE_HOME)/.chez-cross-ta6fb/lib -.PHONY: help run test test-interop build-native binary install install-script clean linux-amd64 freebsd-amd64 linux freebsd jpg-linux-amd64 jpg-freebsd-amd64 +.PHONY: help run test test-interop build-native binary binary-version-smoke install install-script clean linux-amd64 freebsd-amd64 linux freebsd jpg-linux-amd64 jpg-freebsd-amd64 ensure-jerboa-tools audit verify security sbom reproducibility-report target-evidence release-evidence .DEFAULT_GOAL := help help: - @echo "jerboa-pgp — modern PGP replacement" + @echo "jerboa-pgp — modern age-style file crypto" @echo "" @echo "Development:" @echo " make build-native Build pure-Rust crypto backend" @echo " make run ARGS='version' Run jpg under the interpreter" @echo " make test Run smoke tests" - @echo " make test-interop Run bidirectional gpg interop tests" + @echo " make test-interop Report disabled OpenPGP interop status" + @echo " make release-evidence Write test/audit/SBOM/reproducibility evidence" @echo "" @echo "Distribution:" @echo " make binary Build native binary (requires Chez+Jerboa)" @@ -40,27 +51,53 @@ build-native: @echo "" @echo "Built $(NATIVE_LIB)" -run: build-native +ensure-jerboa-tools: + @if ! "$(JERBUILD)" --jerboa-home >/dev/null 2>&1; then \ + echo "=== Installing Jerboa toolchain $(JERBOA_VERSION) into $(JERBOA_TOOL_DIR) ==="; \ + sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \ + fi + +run: build-native ensure-jerboa-tools JERBOA_HOME=$(JERBOA_HOME) JPGP_DIR=$(CURDIR) \ - $(SCHEME) -q --libdirs $(CURDIR):$(JERBOA_HOME)/lib \ - --script pgp/main.ss -- $(ARGS) + $(JERBUILD) exec --libdirs $(CURDIR):$(JERBOA_HOME)/lib \ + pgp/main.ss -- $(ARGS) -test: build-native +test: build-native ensure-jerboa-tools JERBOA_HOME=$(JERBOA_HOME) JPGP_DIR=$(CURDIR) \ - $(SCHEME) -q --libdirs $(CURDIR):$(JERBOA_HOME)/lib \ - --script test/test-all.ss + $(JERBUILD) exec --libdirs $(CURDIR):$(JERBOA_HOME)/lib \ + test/test-all.ss test-interop: build-native JERBOA_HOME=$(JERBOA_HOME) JPGP_REPO=$(CURDIR) SCHEME=$(SCHEME) \ bash test/interop-gpg.sh +audit: + @if cargo audit --version >/dev/null 2>&1; then \ + cargo audit --file pgp-native/Cargo.lock -D warnings; \ + else \ + echo "ERROR: cargo audit is not installed; install cargo-audit to run the native dependency audit gate." >&2; \ + exit 1; \ + fi + +verify: security test audit + +security: + @set -eu; \ + pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|gldt-[A-Za-z0-9_-]{20,}|glrt-[A-Za-z0-9_-]{20,}|AGE-SECRET-KEY-1[A-Za-z0-9_-]{24,}|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16})"; \ + matches="$$(git grep -n -I -E "$$pattern" -- . ":!*.png" ":!*.jpg" ":!*.jpeg" ":!*.gif" ":!*.so" ":!*.dylib" ":!*.o" ":!*.a" ":!*.boot" ":!*.tar.gz" || true)"; \ + if [ -n "$$matches" ]; then \ + echo "$$matches"; \ + echo "High-confidence secret pattern found."; \ + exit 1; \ + fi; \ + echo "secret_scan_status=pass" + BINARY_OUTPUT ?= jpg-bin binary: build-native @echo "==> Building self-contained $(BINARY_OUTPUT) binary" - JPGP_REPO=$(CURDIR) SCHEME=$(SCHEME) JERBOA_HOME=$(JERBOA_HOME) \ - JERBOA_CHEZ_PREFIX=$(JERBOA_HOME)/.chez \ - bash support/build-binary.sh support/binary-entry.ss $(BINARY_OUTPUT) + JPGP_REPO=$(CURDIR) JERBOA_HOME=$(JERBOA_HOME) \ + $(JERBUILD) binary --libdirs "$(CURDIR):$(JERBOA_HOME)/lib" support/binary-entry.ss $(BINARY_OUTPUT) @echo "" @echo "Built ./$(BINARY_OUTPUT). The native lib is still required at runtime:" @echo " $(NATIVE_LIB)" @@ -68,6 +105,43 @@ binary: build-native @echo " /opt/homebrew/lib/libjpgp_native.{dylib,so}" @echo " or set JPGP_NATIVE_LIB=/path/to/libjpgp_native.{dylib,so}" +binary-version-smoke: binary + JPGP_DIR=$(CURDIR) JPGP_NATIVE_LIB="$(NATIVE_LIB)" ./$(BINARY_OUTPUT) version + +sbom: ensure-jerboa-tools + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JERBUILD="$(JERBUILD)" \ + BIN="$(BINARY_OUTPUT)" \ + NATIVE_MANIFEST="pgp-native/Cargo.toml" \ + NATIVE_LOCK="pgp-native/Cargo.lock" \ + NATIVE_STATICLIB="$(NATIVE_STATICLIB)" \ + NATIVE_LIB="$(NATIVE_LIB)" \ + sh scripts/sbom.sh + +reproducibility-report: ensure-jerboa-tools + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + JERBUILD="$(JERBUILD)" \ + MAKE="$(MAKE)" \ + BIN="$(BINARY_OUTPUT)" \ + NATIVE_STATICLIB="$(NATIVE_STATICLIB)" \ + NATIVE_LIB="$(NATIVE_LIB)" \ + sh scripts/reproducibility-report.sh + +target-evidence: + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + TARGET_EVIDENCE_DIR="$(TARGET_EVIDENCE_DIR)" \ + sh scripts/target-evidence.sh + +release-evidence: + @REPO_ROOT="$(CURDIR)" \ + DIST_DIR="$(DIST_DIR)" \ + RELEASE_EVIDENCE_DIR="$(RELEASE_EVIDENCE_DIR)" \ + JERBUILD="$(JERBUILD)" \ + sh scripts/release-evidence.sh + install-script: build-native mkdir -p $(BIN_DIR) $(MAN_DIR) printf '#!/bin/sh\nexec %s -q --libdirs %s:%s/lib --script %s/pgp/main.ss -- "$$@"\n' \ @@ -105,26 +179,26 @@ linux: linux-amd64 linux-amd64: jpg-linux-amd64 jpg-linux-amd64: - @test -d $(JERBOA_HOME)/.chez-cross-ta6le/lib || { \ - echo "ERROR: cross-prefix $(JERBOA_HOME)/.chez-cross-ta6le missing."; \ - echo " Run 'make binary' in $(JERBOA_HOME) first."; exit 1; } + @test -d $(JERBOA_SOURCE_HOME)/.chez-cross-ta6le/lib || { \ + echo "ERROR: cross-prefix $(JERBOA_SOURCE_HOME)/.chez-cross-ta6le missing."; \ + echo " Run 'make binary' in $(JERBOA_SOURCE_HOME) first."; exit 1; } @command -v x86_64-linux-musl-gcc >/dev/null 2>&1 || { \ echo "ERROR: x86_64-linux-musl-gcc not in PATH (install musl-cross)."; exit 1; } - JERBOA_HOME=$(JERBOA_HOME) JPGP_REPO=$(CURDIR) \ - $(SCHEME) -q --libdirs '$(XC_LIBDIRS)' --script build-jpg-cross.ss + JERBOA_HOME=$(JERBOA_SOURCE_HOME) JPGP_REPO=$(CURDIR) \ + $(SCHEME) -q --libdirs '$(XC_LIBDIRS)' --script build-jpg-cross.chez # ── Cross-compile: FreeBSD 14 amd64 (dynamic) ───────────────────────────────── freebsd: freebsd-amd64 freebsd-amd64: jpg-freebsd-amd64 jpg-freebsd-amd64: - @test -d $(JERBOA_HOME)/.chez-cross-ta6fb/lib || { \ - echo "ERROR: cross-prefix $(JERBOA_HOME)/.chez-cross-ta6fb missing."; \ - echo " Run 'make binary' in $(JERBOA_HOME) first."; exit 1; } + @test -d $(JERBOA_SOURCE_HOME)/.chez-cross-ta6fb/lib || { \ + echo "ERROR: cross-prefix $(JERBOA_SOURCE_HOME)/.chez-cross-ta6fb missing."; \ + echo " Run 'make binary' in $(JERBOA_SOURCE_HOME) first."; exit 1; } @command -v x86_64-unknown-freebsd14-clang >/dev/null 2>&1 || { \ echo "ERROR: x86_64-unknown-freebsd14-clang not in PATH (install FreeBSD sysroot + clang wrapper)."; exit 1; } - JERBOA_HOME=$(JERBOA_HOME) JPGP_REPO=$(CURDIR) \ - $(SCHEME) -q --libdirs '$(XC_LIBDIRS)' --script build-jpg-freebsd-cross.ss + JERBOA_HOME=$(JERBOA_SOURCE_HOME) JPGP_REPO=$(CURDIR) \ + $(SCHEME) -q --libdirs '$(XC_LIBDIRS)' --script build-jpg-freebsd-cross.chez clean: cd $(NATIVE_DIR) && cargo clean --- a/PLAN.md +++ b/PLAN.md @@ -4,6 +4,11 @@ This file is the original design sketch, kept for historical record. For the current state of the project see [README.md](README.md) and the documentation under [`docs/`](docs/). +Current production note: OpenPGP/GnuPG interop is disabled. The previous rPGP +path pulled an unfixed RustSec advisory through `rsa`, so production builds +only ship age encryption, age passphrase wrapping, Ed25519 signatures, and +OpenPGP rejection/stub paths. See [`docs/INTEROP.md`](docs/INTEROP.md). + ## Motivation GPG has been around for 20+ years and almost nobody uses it. The data model @@ -22,15 +27,12 @@ who would otherwise like to use it. 2. **One identity file, one passphrase.** No keyring directory, no agent socket, no subkey hierarchy. The identity file is itself passphrase-protected via age scrypt. -3. **Pure Rust, no C.** All crypto comes from audited pure-Rust crates - (`age`, `ed25519-dalek`, `pgp`/rPGP, `zeroize`). +3. **Pure Rust, no C.** All production crypto comes from audited pure-Rust + crates (`age`, `ed25519-dalek`, `zeroize`). 4. **Jerboa for the human side.** CLI, file I/O, recipient parsing, output formatting — all in `(jerboa prelude)` style. -5. **PGP as a thin interop layer.** You can encrypt *to* and decrypt - *from* an OpenPGP key, and produce or verify OpenPGP detached - signatures, but native messages are age. Anything beyond round-trip - (keyservers, web-of-trust, subkey management) is explicitly out of - scope. +5. **Fail closed on unsafe interop.** OpenPGP armor is detected and rejected + until a strict-audit-clean backend exists. Native messages are age. ## Architecture @@ -44,14 +46,12 @@ jerboa-pgp/ │ ├── util.rs # buffer-output helpers │ ├── age_mod.rs # age keygen / encrypt / decrypt │ ├── pass_mod.rs # age scrypt: identity-file wrap/unwrap -│ ├── sig_mod.rs # Ed25519 keygen / sign / verify -│ ├── pgp_mod.rs # rPGP: encrypt-to-PGP-recipient -│ └── pgp_io.rs # rPGP: decrypt, sign, verify +│ └── sig_mod.rs # Ed25519 keygen / sign / verify ├── pgp/ # Jerboa source (.ss) │ ├── util.ss # byte/string helpers, error type │ ├── ffi.ss # foreign-procedure bindings to libjpgp_native │ ├── armor.ss # ASCII-armor detect / classify -│ ├── recipient.ss # parse age vs jpgp1 vs OpenPGP recipients +│ ├── recipient.ss # parse age/jpgp1 recipients; classify OpenPGP to reject │ ├── identity.ss # load/save passphrase-wrapped identity │ ├── prompt.ss # passphrase prompt (no echo) │ ├── cli.ss # subcommand dispatch + arg parsing @@ -66,7 +66,7 @@ jerboa-pgp/ │ └── _jpg ├── test/ │ ├── test-all.ss # smoke tests -│ └── interop-gpg.sh # bidirectional gpg interop test +│ └── interop-gpg.sh # disabled OpenPGP interop status check ├── docs/ # Long-form docs (see docs/README.md) ├── Makefile └── README.md @@ -79,10 +79,10 @@ jpg keygen [--out PATH] Generate identity (prompts passphrase) jpg pubkey [--identity P] [--out P] Print this identity's public key line jpg list [--identity P] Show identity info jpg fingerprint [--identity P] Print SHA256: fingerprint -jpg encrypt [-r R ...] [-s] [-i I] [-o O] Encrypt; auto-routes age/PGP -jpg decrypt [-s] [-i I] [-o O] [--identity P] [--pgp-key SEC.asc] [--pgp-pass P] -jpg sign [-i I] [-o O] [--identity P] [--pgp-key SEC.asc] [--pgp-pass P] -jpg verify SIG [-i I] [--pubkey P] [--pgp-pubkey PUB.asc] +jpg encrypt [-r R ...] [-s] [-i I] [-o O] Encrypt to age/jpgp recipients +jpg decrypt [-s] [-i I] [-o O] [--identity P] +jpg sign [-i I] [-o O] [--identity P] +jpg verify SIG [-i I] [--pubkey P] jpg version ``` @@ -93,15 +93,13 @@ For full details see [`doc/jpg.1`](doc/jpg.1). All items below are implemented and tested: - `keygen` / `pubkey` / `list` / `fingerprint` -- `encrypt` (age, multi-recipient, symmetric) and `encrypt -r FOO.asc` - for OpenPGP recipients -- `decrypt` (age, symmetric) and `decrypt --pgp-key SECRET.asc` for - inbound OpenPGP -- `sign` / `verify` (Ed25519 native) and `--pgp-key` variants that - produce / verify OpenPGP detached signatures +- `encrypt` (age, multi-recipient, symmetric) +- `decrypt` (age, symmetric) +- `sign` / `verify` (Ed25519 native) +- OpenPGP detection with fail-closed CLI rejection and native ABI stubs - Self-contained `jpg` binary built with Chez `compile-program` + WPO + boot-file embedding (~5 MB Mach-O / ELF) -- Bidirectional gpg interop tested end-to-end (`make test-interop`) +- Disabled OpenPGP interop decision recorded by `make test-interop` - mandoc man page, bash + zsh completions ## Out-of-scope (still) @@ -112,9 +110,7 @@ All items below are implemented and tested: SHA-256 fingerprint; there is no third-party trust signing. - **Subkey hierarchy.** Each identity has one age key and one Ed25519 key. No rotation across subkeys, no signing of subkeys. -- **AEAD OpenPGP packets** (the gnupg-proprietary OCB packet, tag 20). - rPGP doesn't parse them. See [docs/INTEROP.md](docs/INTEROP.md) for - how to make gpg emit SEIPDv1 instead. +- **OpenPGP interop.** Disabled until a strict-audit-clean backend exists. - **Agent / passphrase caching.** Each operation prompts. There is no ssh-agent-style daemon. @@ -123,7 +119,7 @@ All items below are implemented and tested: ``` make run ARGS='keygen' # interpreter make test # smoke tests -make test-interop # gpg interop +make test-interop # disabled OpenPGP interop status make binary # native binary `jpg` make install # → ~/.local/bin/jpg + ~/.local/lib/libjpgp_native ``` --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # jerboa-pgp -A modern, friendlier replacement for GPG, written in [Jerboa] with a pure-Rust +A modern, friendlier file crypto tool, written in [Jerboa] with a pure-Rust crypto backend. **No C dependencies.** ``` @@ -12,10 +12,11 @@ jpg verify release.tar.gz.sig -i release.tar.gz ``` Native crypto is [age]-style (X25519 + ChaCha20-Poly1305) for encryption and -Ed25519 for signing. For interop with the few remaining GPG users, `jpg -encrypt -r alice.asc` recognises an OpenPGP public key and encrypts to it -in OpenPGP format using [rPGP] — so the recipient can decrypt with plain -`gpg`. +Ed25519 for signing. + +OpenPGP/GnuPG interop is disabled in production builds. The Rust OpenPGP +backend currently pulls an unfixed `rsa` RustSec advisory, so this repo fails +closed and rejects OpenPGP armor until a clean dependency graph is available. ## Status @@ -23,14 +24,12 @@ v1 implements: - `keygen` / `pubkey` / `list` / `fingerprint` (SSH-style `SHA256:`) - `encrypt` / `decrypt` (age, multi-recipient, symmetric) -- `encrypt -r FOO.asc` — OpenPGP outbound (rPGP, SEIPDv1) -- `decrypt --pgp-key SECRET.asc` — OpenPGP inbound -- `sign` / `verify` (Ed25519 native, `--pgp-key` produces OpenPGP detached sigs) +- `sign` / `verify` (Ed25519 native) - Self-contained `jpg-bin` (Chez `compile-program` + boot-embedded) -- Bidirectional gpg interop tested end-to-end (`make test-interop`) +- Strict native dependency audit with `cargo audit -D warnings` See [`docs/`](docs/) for the architecture, FFI surface, on-disk -formats, gpg interop notes, build internals, and threat model. +formats, disabled interop decision, build internals, and threat model. [`PLAN.md`](PLAN.md) is the original design sketch. ## Why @@ -43,39 +42,40 @@ its *artifacts*. - **One identity file, one passphrase.** No keyring directory, no agent. - **Recipients are public-key strings**, not email addresses. Paste them into Slack like an SSH key. -- **Pure Rust crypto, no C.** `age`, `ed25519-dalek`, `rpgp`, `zeroize`. +- **Pure Rust crypto, no C.** `age`, `ed25519-dalek`, `zeroize`. - **Jerboa for the human side.** CLI parsing, file I/O, formatting. ## Build -Requires Chez Scheme + [Jerboa] checked out at `~/mine/jerboa` (or set -`JERBOA_HOME`). +Requires Rust/Cargo and Jerboa. `make test` and `make run` use `jerbuild`; +if no local Jerboa checkout/tool is available, CI installs the pinned release +toolchain with `support/ensure-jerboa.sh`. ``` make run ARGS='version' # interpreter mode make test # smoke tests -make test-interop # bidirectional gpg interop tests +make audit # cargo-audit for the Rust backend +make test-interop # reports OpenPGP interop disabled/skipped make binary # native `jpg` binary (5MB Mach-O / ELF) +make release-evidence # tests, audit, SBOM, reproducibility make install # → ~/.local/bin/jpg + ~/.local/lib/libjpgp_native ``` -## Interop with gpg +Release evidence is documented in +[`docs/release-evidence.md`](docs/release-evidence.md), with dependency +provenance in [`docs/dependency-provenance.md`](docs/dependency-provenance.md). +The release gate fails closed on RustSec advisories, secret-scan findings, +test failures, or reproducibility mismatches. -`jpg encrypt -r alice.asc -i secret.txt -o secret.asc` produces a SEIPDv1 -(RFC 4880) OpenPGP message that any version of gpg can decrypt. +## Interop with gpg -The reverse direction (`jpg decrypt --pgp-key alice.sec.asc -i msg.asc`) -works when the gpg recipient key does **not** advertise AEAD preferences -in its self-signature. Modern GnuPG 2.5 defaults to its proprietary OCB -packet (tag 20), which rPGP cannot parse. To produce a compatible key in -gpg, drop AEAD from the default preferences (`features=0x05` rather than -the default `0x07`). See `test/interop-gpg.sh` for a working -`~/.gnupg/gpg.conf`. +OpenPGP interop is currently rejected. See [`docs/INTEROP.md`](docs/INTEROP.md) +and [`docs/dependency-provenance.md`](docs/dependency-provenance.md) for the +release decision. ## License -ISC +Apache License 2.0. [Jerboa]: https://git.sr.ht/~lisp/jerboa [age]: https://github.com/FiloSottile/age -[rPGP]: https://github.com/rpgp/rpgp new file mode 100644 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,56 @@ +# Security Policy + +`jerboa-pgp` is an experimental one-shot CLI for age-style encryption and +Ed25519 signatures through a pure-Rust native backend. +Treat it as private/preview until the release gates tracked in +`~/Release-plan.md` and `~/mine/jerboa-production-readiness.md` are complete. + +## Supported Status + +No public production-support commitment exists yet. Security-sensitive releases +must be cut from a clean checkout after: + +- `make test`, `make run ARGS='version'`, `make audit`, and + `make release-evidence` pass. +- The high-confidence secret scan and Jerboa security scanner are clean. +- Rust dependency CVE posture is clean under `cargo audit -D warnings`. +- Release evidence records SBOM/source/native hashes and reproducibility + status for the standalone binary and native staticlib. +- Target key/passphrase evidence is fail-closed: setting + `JPGP_REQUIRE_TARGET_KEY_PROOF=1` without a complete + `JPGP_TARGET_KEY_PROOF_FILE` blocks release evidence. +- Dependency provenance in `docs/dependency-provenance.md` has been reviewed + against the current RustSec database. +- OpenPGP interop remains disabled until its dependency graph is clean. +- History has been reviewed or intentionally reset before first public release. + +## Key And Passphrase Handling + +- Identity files contain X25519 and Ed25519 secret material wrapped with age + scrypt passphrase encryption. +- Identity files are written with best-effort `0600` permissions. +- Passphrases and decrypted identity plaintext live in Scheme process memory + during the one-shot command. This is not acceptable for a daemon. +- Do not print passphrases, secret identities, decrypted identity payloads, or + plaintext decrypted messages in diagnostic paths. +- Production release evidence must prove passphrase and decrypted-payload + non-disclosure on the target platform without preserving secret identities or + plaintext artifacts in logs. + +## FFI And Crypto Posture + +- Native crypto is delegated to Rust crates: `age`, `ed25519-dalek`, + `zeroize`, and RustCrypto dependencies. +- OpenPGP C ABI entry points are compatibility stubs that return + `JPGP_E_UNSUPPORTED`; CLI paths reject OpenPGP before reading user payloads. +- Scheme loads `libjpgp_native` lazily through `(jerboa ffi)` so imports and + static-binary probes do not crash before native functionality is needed. +- The Rust ABI uses borrowed buffers and `(buf, buf_len, out_len*)` output + conventions. No pointer is retained across the FFI boundary. +- Optional cross-build scripts are trusted release tooling, not runtime code. + +## Reporting + +Before public release, report issues privately to the repository owner. After +public release, replace this section with a dedicated advisory contact, +supported versions, and disclosure window. new file mode 100644 --- /dev/null +++ b/build-jpg-cross.chez @@ -0,0 +1,404 @@ +#!chezscheme +;;; build-jpg-cross.chez — Cross-compile jpg from macOS to Linux x86_64 musl +;;; +;;; Produces: jpg-linux-amd64 (static ELF, no shared library deps) +;;; +;;; Differs from sibling cross-builds in that we also build a target-arch +;;; libjpgp_native.a (pure-Rust crypto) and statically link it. + +(import (chezscheme)) ; jerboa-security: suppress direct-chezscheme-import-user-code -- trusted release-build script uses Chez compiler APIs outside runtime user code. + +(define jerboa-home + (or (getenv "JERBOA_HOME") "/Users/user/mine/jerboa")) + +(define jpgp-repo + (or (getenv "JPGP_REPO") (current-directory))) + +(define cross-prefix (format "~a/.chez-cross-ta6le" jerboa-home)) +(define xpatch (format "~a/build/chez/xc-ta6le/s/xpatch" jerboa-home)) +(define cross-cc (or (getenv "CROSS_CC") "x86_64-linux-musl-gcc")) + +(define output "jpg-linux-amd64") +(define entry-script "support/binary-entry.ss") + +(define cargo-features "tls,sqlite,crypto") + +(define jerboa-native-a + (or (getenv "JERBOA_NATIVE_A") + (format "~a/jerboa-native-rs/target/x86_64-unknown-linux-musl/release/libjerboa_native.a" + jerboa-home))) + +(define jpgp-native-a + (format "~a/pgp-native/target/x86_64-unknown-linux-musl/release/libjpgp_native.a" + jpgp-repo)) + +(define cross-csv-dir + (let ([lib (format "~a/lib" cross-prefix)]) + (unless (file-directory? lib) + (error 'build-jpg-cross "cross prefix lib dir missing — run 'make binary' in jerboa first" lib)) + (let* ([entries (directory-list lib)] + [csvs (filter (lambda (e) + (and (>= (string-length e) 3) + (string=? (substring e 0 3) "csv"))) + entries)]) + (when (null? csvs) + (error 'build-jpg-cross "no csv* in cross lib" lib)) + (format "~a/~a/ta6le" lib (car csvs))))) + +(define (require-file p) + (unless (file-exists? p) + (error 'build-jpg-cross "missing file" p))) + +(require-file xpatch) +(require-file (format "~a/libkernel.a" cross-csv-dir)) +(require-file (format "~a/scheme.h" cross-csv-dir)) +(require-file (format "~a/petite.boot" cross-csv-dir)) +(require-file (format "~a/scheme.boot" cross-csv-dir)) +(require-file entry-script) + +(printf "==> build-jpg-cross~n") +(printf " JERBOA_HOME: ~a~n" jerboa-home) +(printf " JPGP_REPO: ~a~n" jpgp-repo) +(printf " cross csv-dir: ~a~n" cross-csv-dir) +(printf " cross-cc: ~a~n" cross-cc) +(printf " output: ~a~n~n" output) + +;; ── Step 0a: Build/rebuild jerboa-native-rs for x86_64-linux-musl ────────── +(define (path-dirname s) + (let loop ([i (- (string-length s) 1)]) + (cond [(< i 0) "."] + [(char=? (string-ref s i) #\/) (substring s 0 i)] + [else (loop (- i 1))]))) + +(define (capture-line cmd) + (call-with-values + (lambda () (open-process-ports cmd (buffer-mode block) (native-transcoder))) + (lambda (to-stdin from-stdout from-stderr pid) + (let ([line (get-line from-stdout)]) + (close-port to-stdin) + (close-port from-stdout) + (close-port from-stderr) + (if (or (eof-object? line) (zero? (string-length line))) #f line))))) + +(define rustup-rustc-path (capture-line "rustup which rustc 2>/dev/null")) +(define rustup-bin-dir (and rustup-rustc-path (path-dirname rustup-rustc-path))) + +(define (try-cargo-build cmd) + (printf " $ ~a~n" cmd) + (zero? (system cmd))) ; jerboa-security: suppress missing-taint-check-at-sink -- trusted release-build script executes printed toolchain commands from release environment. + +(define (rebuild-jerboa-native!) + (let* ([nrs-dir (format "~a/jerboa-native-rs" jerboa-home)] + [cargo-args (format "build --release --no-default-features --features ~a --target x86_64-unknown-linux-musl" + cargo-features)] + [via-rustup + (and rustup-bin-dir + (format "cd '~a' && env PATH='~a':$PATH RUSTC='~a/rustc' '~a/cargo' ~a" + nrs-dir rustup-bin-dir rustup-bin-dir rustup-bin-dir cargo-args))] + [via-path (format "cd '~a' && cargo ~a" nrs-dir cargo-args)] + [ok? (or (and via-rustup (try-cargo-build via-rustup)) + (try-cargo-build via-path))]) + (unless ok? + (error 'build-jpg-cross "failed to build jerboa-native-rs for x86_64-unknown-linux-musl")) + (unless (file-exists? jerboa-native-a) + (error 'build-jpg-cross "cargo succeeded but .a missing" jerboa-native-a)))) + +(cond + [(file-exists? jerboa-native-a) + (printf "==> jerboa-native-rs .a present~n")] + [else + (printf "==> jerboa-native-rs missing — building~n") + (rebuild-jerboa-native!)]) + +;; ── Step 0b: Build/rebuild pgp-native (libjpgp_native.a) ─────────────────── +(define (rebuild-jpgp-native!) + (let* ([nrs-dir (format "~a/pgp-native" jpgp-repo)] + [cargo-args "build --release --target x86_64-unknown-linux-musl"] + [via-rustup + (and rustup-bin-dir + (format "cd '~a' && env PATH='~a':$PATH RUSTC='~a/rustc' '~a/cargo' ~a" + nrs-dir rustup-bin-dir rustup-bin-dir rustup-bin-dir cargo-args))] + [via-path (format "cd '~a' && cargo ~a" nrs-dir cargo-args)] + [ok? (or (and via-rustup (try-cargo-build via-rustup)) + (try-cargo-build via-path))]) + (unless ok? + (error 'build-jpg-cross "failed to build pgp-native for x86_64-unknown-linux-musl")) + (unless (file-exists? jpgp-native-a) + (error 'build-jpg-cross "cargo succeeded but libjpgp_native.a missing" jpgp-native-a)))) + +(cond + [(file-exists? jpgp-native-a) + (printf "==> libjpgp_native.a present~n")] + [else + (printf "==> libjpgp_native.a missing — building~n") + (rebuild-jpgp-native!)]) + +(printf "~n") + +;; ── Step 0.5: Patch load-shared-object in libraries ──────────────────────── +(printf "==> [0.5/6] patching load-shared-object in libraries~n") +(define lso-patch-cmd + (string-append + "perl -i -0777 -pe 's/" + "\\(load-shared-object(?=\\s)" + "(?:[^()\"]++|\"(?:\\\\.|[^\"\\\\])*+\"|" + "(?<bal>\\((?:[^()\"]++|\"(?:\\\\.|[^\"\\\\])*+\"|(?&bal))*+\\))" + ")*+\\)/(void)/g'")) + +(define jerboa-lib-dir (format "~a/lib" jerboa-home)) +(system (format "find '~a' -name '*.sls' -exec ~a {} +" jerboa-lib-dir lso-patch-cmd)) +(system (format "find '~a' -name '*.ss' -exec ~a {} +" jpgp-repo lso-patch-cmd)) + +(system (format "find '~a' -name '*.so' -delete 2>/dev/null" jerboa-lib-dir)) +(system (format "find '~a/pgp' -name '*.so' -delete 2>/dev/null" jpgp-repo)) +(system (format "find '~a/pgp' -name '*.wpo' -delete 2>/dev/null" jpgp-repo)) +(system (format "find '~a/support' -name '*.so' -delete 2>/dev/null" jpgp-repo)) +(system (format "find '~a/support' -name '*.wpo' -delete 2>/dev/null" jpgp-repo)) + +(define (restore-patched-files!) + (printf "~n==> [cleanup] restoring patched .sls/.ss files via git~n") + (system (format "cd '~a' && git ls-files -z -- '*.sls' | xargs -0 git checkout -- 2>/dev/null" jerboa-home)) + (system (format "cd '~a' && git ls-files -z -- '*.ss' | xargs -0 git checkout -- 2>/dev/null" jpgp-repo))) + +;; ── Stage 1: load xpatch (target=ta6le emit mode) ────────────────────────── +(define orig-libdirs (library-directories)) +(printf "==> [1/6] loading xpatch (compiler -> ta6le emit mode)~n") +(load xpatch) +(library-directories + (append + (list (cons jpgp-repo jpgp-repo) + (cons (format "~a/lib" jerboa-home) (format "~a/lib" jerboa-home))) + orig-libdirs)) + +(compile-imported-libraries #t) +(generate-wpo-files #t) + +;; ── Stage 2: compile-program entry-script ────────────────────────────────── +(printf "==> [2/6] compile-program ~a~n" entry-script) +(guard (e [#t (restore-patched-files!) (raise e)]) + (compile-program entry-script)) + +(define entry-wpo + (let ([n (string-length entry-script)]) + (string-append (substring entry-script 0 (- n 3)) ".wpo"))) + +;; ── Stage 3: compile-whole-program → wpo .so ─────────────────────────────── +(define wpo-output (string-append output ".wp.so")) +(printf "==> [3/6] compile-whole-program ~a -> ~a~n" entry-wpo wpo-output) +(guard (e [#t (restore-patched-files!) (raise e)]) + (compile-whole-program entry-wpo wpo-output #t)) + +;; ── Stage 4: embed boot files + program as C arrays ──────────────────────── +(define (embed-as-c-array in-path var-name out-path) + (let* ([bv (call-with-port (open-file-input-port in-path) get-bytevector-all)] + [n (bytevector-length bv)]) + (call-with-port (open-file-output-port out-path + (file-options no-fail) + (buffer-mode block) + (native-transcoder)) + (lambda (out) + (display (format "static const unsigned char ~a[] = {\n" var-name) out) + (let loop ([i 0]) + (when (< i n) + (display (format "0x~2,'0x," (bytevector-u8-ref bv i)) out) + (when (= (mod (+ i 1) 16) 0) (newline out)) + (loop (+ i 1)))) + (when (positive? n) (newline out)) + (display "};\n" out) + (display (format "static const unsigned int ~a_size = sizeof(~a);\n" + var-name var-name) + out)))