Refs: offline install via vendored wheels and test verification workflow (see README/playbook updates)

This commit is contained in:
LC mac
2026-01-08 01:45:25 +08:00
parent 2807982209
commit 369c8595a1
75 changed files with 1094 additions and 388 deletions

View File

@@ -1,174 +0,0 @@
# Offline Security Best Practices
This tool is designed for offline use, but true security depends on the **environment** where you run it. Below are operational security recommendations for generating keys you can trust.
## Air-Gapped Setup (Highest Security)
For maximum security when generating production wallets, use an **air-gapped computer**—a device that has never been and will never be connected to any network.
**Recommended procedure:**
1. **Prepare a clean machine**:
- Use a dedicated laptop or bootable USB with a fresh Linux installation (e.g., Tails, Ubuntu Live USB)
- Never connect this device to WiFi, Ethernet, or Bluetooth [web:14]
- Physically disable network interfaces if possible (remove WiFi card, tape over Ethernet port)
2. **Transfer dependencies offline**:
```bash
# On an online machine, download dependencies
pip download -r requirements.txt -d ./offline-deps
# Transfer ./offline-deps to air-gapped machine via USB
# On air-gapped machine:
pip install --no-index --find-links=./offline-deps -r requirements.txt
```
3. **Verify code integrity**:
- Check the git commit hash matches the version you reviewed
- Optionally run `pytest -v tests/test_vectors.py` on the air-gapped machine to verify derivation logic
4. **Generate wallet**:
```bash
python ./src/pyhdwallet.py gen --pgp-pubkey-file pubkeys/recipient.asc --file --off-screen
```
5. **Transfer output safely**:
- Copy only the `.wallet/*.zip` file to USB (never copy `pyhdwallet.py` or Python environment back to online machine)
- The ZIP is AES-encrypted; the inner `.asc` is PGP-encrypted [web:19]
6. **Destroy or securely wipe** the USB after transfer if it contained unencrypted secrets
### Threats Air-Gapping Mitigates
- **Remote attacks**: Malware cannot exfiltrate keys over the network
- **Clipboard hijacking**: No clipboard manager or remote access tool can intercept data
- **Browser/OS telemetry**: No accidental upload of terminal history or crash dumps
### Threats Air-Gapping Does NOT Fully Mitigate
Research shows that sophisticated attackers with physical access can potentially exfiltrate data from air-gapped systems via covert channels (acoustic, electromagnetic, optical) [web:18]. However:
- These attacks require physical proximity and pre-installed malware [web:18]
- For individual users (not nation-state targets), air-gapping remains highly effective
- Countermeasures: Use the machine in a secure location, inspect for unfamiliar USB devices, verify software integrity before installation [web:18]
### Physical Security
**Mnemonic handling:**
- Write the mnemonic on paper immediately; never store it digitally on the air-gapped machine
- Use a metal backup (e.g., Cryptosteel) for fire/water resistance
- Split storage across multiple secure locations if desired (Shamir's Secret Sharing for advanced users)
**Device handling:**
- After generating the wallet, optionally wipe the air-gapped device or destroy the bootable USB
- If reusing the device, use secure-erase tools (not just `rm`)
### Verification Through Testing
**Why the test suite matters for trust:**
The committed test suite (`tests/test_vectors.py`) allows you to verify that derivation logic hasn't been tampered with before generating production keys:
```bash
# On the air-gapped machine, before generating your wallet:
pytest -v tests/test_vectors.py
```
If all tests pass, you have cryptographic proof that:
- BIP39 seed derivation matches the well-known test vector ("abandon abandon..." → known seed) [file:2]
- Derivation paths produce addresses matching public BIP39/BIP44 test vectors
- PGP fingerprinting logic works correctly
If tests fail, **do not generate keys**—the code may be compromised or buggy.
### Entropy Sources
**Built-in entropy (default):**
- Python's `secrets.token_bytes()` uses OS-provided CSPRNG (`/dev/urandom` on Linux, `CryptGenRandom` on Windows)
- This is cryptographically secure for typical use
**Additional user entropy (optional):**
```bash
python ./src/pyhdwallet.py gen --dice-rolls "4 2 6 1 3 5 ..." --file
```
- Roll a die 50+ times and input the results
- Your dice rolls are mixed with OS entropy via SHA256 [file:2]
- Protects against potential CSPRNG backdoors (theoretical concern)
### PGP Key Fingerprint Pinning
Always use `--expected-fingerprint` when encrypting to a PGP key to prevent key substitution attacks [file:2]:
```bash
# First, get and verify the fingerprint of your recipient key
python ./src/pyhdwallet.py fetchkey https://example.com/mykey.asc --out mykey.asc
# Manually verify fingerprint matches what you expect (check via another channel)
# Then use it with pinning:
python ./src/pyhdwallet.py gen \
--pgp-pubkey-file mykey.asc \
--expected-fingerprint A27B96F2B169B5491013D2DA892B822C14A9AA18 \
--file
```
Without `--expected-fingerprint`, an attacker who controls the filesystem could swap `mykey.asc` with their own key.
### Operational Checklist
Before generating production wallets:
- [ ] Running on air-gapped machine or fresh Live USB
- [ ] Network physically disabled (no WiFi/Ethernet/Bluetooth)
- [ ] Code integrity verified (git commit hash + test suite passes)
- [ ] Using `--off-screen` to minimize terminal scrollback exposure
- [ ] Using `--file` to avoid leaving unencrypted files on disk
- [ ] Using `--pgp-pubkey-file` with `--expected-fingerprint` for key pinning
- [ ] Paper/metal backup prepared for mnemonic
- [ ] Output ZIP password stored separately from ZIP file
- [ ] Plan for USB secure wipe after transfer
### Lower-Risk Scenarios
Air-gapping is overkill for:
- **Learning/testing**: Use your regular laptop with `--off-screen` and `--file`
- **Small amounts**: Generate on a clean, updated machine with minimal software
- **Testnet wallets**: Standard laptop is fine
Air-gapping is recommended for:
- **Life savings or business funds**
- **Long-term cold storage** (multi-year hold)
- **Institutional custody scenarios**
### Trust But Verify
The only way to fully trust this tool (or any wallet software) is to:
1. **Read the source code** (`src/pyhdwallet.py` is ~1400 lines, single file)
2. **Verify test vectors** match published BIP39 test data
3. **Run the test suite** on your air-gapped machine
4. **Generate a test wallet** and verify addresses on a block explorer using a separate tool
Never trust wallet software blindly—especially for significant funds
---
**Key additions:**
1. **Air-gapped setup procedure** with offline dependency installation
2. **Threat model** (what air-gapping protects against and doesn't)
3. **Physical security** best practices for mnemonic backup
4. **Test suite as verification tool** before generating production keys
5. **Entropy sources** explanation (dice rolls for paranoid users)
6. **PGP fingerprint pinning** rationale
7. **Operational security checklist** for production use
8. **Risk-based guidance** (when air-gapping is overkill vs. essential)
9. **"Trust but verify"** philosophy for crypto software

View File

@@ -1,7 +1,7 @@
# Dockerfile
# Base build environment for hdwalletpy: Python 3.11 + build tools + venv support
FROM python:3.11-slim
# Build environment for hdwalletpy: Python 3.12 + build tools + venv support
# Used for: (1) Building Linux x86_64 wheels, (2) Development container
FROM python:3.12-slim
# Install build tools, headers, and venv module
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -9,8 +9,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3-dev \
libffi-dev \
python3-venv \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/*
# Set working directory for bind mounts
WORKDIR /app
# Default command (can be overridden)
CMD ["/bin/bash"]

195
Makefile
View File

@@ -1,90 +1,169 @@
# Makefile for hdwalletpy workflow
# - Build a reusable Docker image
# - Build reusable Docker image (Python 3.12)
# - Vendor multi-platform wheels for offline air-gapped use
# - Compile wheels in container (wheelhouse)
# - Create host venv and install from wheelhouse
# - Create host venv and install from wheelhouse or vendor/
# - Optionally run inside a warm container
# ---------- Config ----------
IMAGE := python-build-env:3.11
CONTAINER := hdwallet
IMAGE := hdwallet-build:3.12
CONTAINER := hdwallet-dev
WORKDIR := /app
VENV_HOST := .venv_host
VENV_HOST := .venv
VENV_CONTAINER := /opt/venv
WHEELHOUSE := wheelhouse
# Vendor directories for air-gapped deployment
VENDOR_MACOS := vendor/macos-arm64
VENDOR_LINUX := vendor/linux-x86_64
# ---------- Help ----------
.PHONY: help
help:
@echo "Targets:"
@echo " make build-image - Build Docker image with build tools and python3-venv"
@echo " make wheels - Build dependency wheels into ./$(WHEELHOUSE)"
@echo " make venv-host - Create host venv $(VENV_HOST) and install from wheelhouse"
@echo " make up - Start a warm container named $(CONTAINER)"
@echo " make shell - Open a shell in the warm container"
@echo " make venv-container - Create venv inside container at $(VENV_CONTAINER) and install requirements"
@echo " make down - Stop and remove the warm container"
@echo " make clean-venv - Remove host venv $(VENV_HOST)"
@echo " make clean-wheelhouse - Remove local wheel cache $(WHEELHOUSE)"
@echo " make clean - Clean venv and wheelhouse"
@echo "Vendoring (for offline/air-gapped use):"
@echo " make vendor-macos - Build macOS ARM64 wheels (native)"
@echo " make vendor-linux - Build Linux x86_64 wheels (Docker)"
@echo " make vendor-all - Build wheels for both platforms"
@echo " make verify-vendor - Test offline installation from vendor/"
@echo ""
@echo "Development workflow:"
@echo " make build-image - Build Docker image (Python 3.12)"
@echo " make wheels - Build wheels into ./$(WHEELHOUSE)"
@echo " make install - Create venv and install dependencies"
@echo " make test - Run test suite"
@echo " make up - Start warm dev container"
@echo " make shell - Open shell in warm container"
@echo " make down - Stop and remove dev container"
@echo ""
@echo "Cleanup:"
@echo " make clean - Remove venv, wheelhouse, vendor/"
@echo " make clean-vendor - Remove vendor/ only"
# ---------- Build reusable image ----------
.PHONY: build-image
build-image:
docker build -t $(IMAGE) .
docker build -t $(IMAGE) .
# ---------- Build wheels in container ----------
# ---------- Vendoring for Air-Gapped Use ----------
.PHONY: vendor-macos
vendor-macos: requirements.txt
@echo "Building macOS ARM64 wheels (native)..."
@if [ ! -f ".venv312/bin/pip" ]; then \
echo "ERROR: .venv312 not found. Create it first:"; \
echo " python3.12 -m venv .venv312 && source .venv312/bin/activate"; \
exit 1; \
fi
mkdir -p $(VENDOR_MACOS)
.venv312/bin/pip download --dest $(VENDOR_MACOS) -r requirements.txt
cd $(VENDOR_MACOS) && shasum -a 256 *.whl > SHA256SUMS
@echo "✓ macOS ARM64 wheels: $(VENDOR_MACOS)/"
.PHONY: vendor-linux
vendor-linux: requirements.txt build-image
@echo "Building Linux x86_64 wheels (Docker)..."
mkdir -p $(VENDOR_LINUX)
docker run --rm \
-v "$$PWD":$(WORKDIR) \
-w $(WORKDIR) \
$(IMAGE) \
bash -c " \
pip install --upgrade pip && \
pip download --dest $(VENDOR_LINUX) -r requirements.txt && \
pip wheel --wheel-dir $(VENDOR_LINUX) --no-deps $(VENDOR_LINUX)/*.tar.gz 2>/dev/null || true && \
rm -f $(VENDOR_LINUX)/*.tar.gz && \
cd $(VENDOR_LINUX) && sha256sum *.whl > SHA256SUMS \
"
@echo "✓ Linux x86_64 wheels: $(VENDOR_LINUX)/"
.PHONY: vendor-all
vendor-all: vendor-macos vendor-linux
@echo ""
@echo "✓ All platforms vendored:"
@echo " macOS ARM64: $(VENDOR_MACOS)/ ($$(ls $(VENDOR_MACOS)/*.whl 2>/dev/null | wc -l | xargs) wheels)"
@echo " Linux x86_64: $(VENDOR_LINUX)/ ($$(ls $(VENDOR_LINUX)/*.whl 2>/dev/null | wc -l | xargs) wheels)"
@echo ""
@echo "Commit with: git add vendor/ && git commit -m 'vendor: update wheels'"
.PHONY: verify-vendor
verify-vendor:
@echo "Testing offline installation from vendor/..."
@bash -c ' \
if [[ "$$OSTYPE" == "darwin"* ]]; then \
PLATFORM="macos-arm64"; \
else \
PLATFORM="linux-x86_64"; \
fi; \
echo "Platform: $$PLATFORM"; \
python3.12 -m venv .venv-verify && \
source .venv-verify/bin/activate && \
pip install --no-index --find-links=vendor/$$PLATFORM -r requirements.txt && \
pytest -v tests/test_vectors.py && \
python src/pyhdwallet.py test && \
echo "" && \
echo "✅ Vendor installation verified!" && \
deactivate && \
rm -rf .venv-verify \
'
# ---------- Development Workflow ----------
.PHONY: wheels
wheels: requirements.txt
docker run --rm -it -v "$$PWD":$(WORKDIR) -w $(WORKDIR) $(IMAGE) bash -lc '\
python -m pip install --upgrade pip setuptools wheel && \
mkdir -p $(WHEELHOUSE) && \
python -m pip wheel -r requirements.txt -w $(WHEELHOUSE) \
'
wheels: requirements.txt build-image
docker run --rm \
-v "$$PWD":$(WORKDIR) \
-w $(WORKDIR) \
$(IMAGE) \
bash -c " \
pip install --upgrade pip setuptools wheel && \
mkdir -p $(WHEELHOUSE) && \
pip wheel -r requirements.txt -w $(WHEELHOUSE) \
"
# ---------- Host venv from wheelhouse ----------
.PHONY: venv-host
venv-host: requirements.txt
python3 -m venv $(VENV_HOST)
. $(VENV_HOST)/bin/activate && \
python -m pip install --upgrade pip setuptools wheel && \
python -m pip install --no-index --find-links=$(WHEELHOUSE) -r requirements.txt && \
python --version
.PHONY: install
install: requirements.txt
@if [ ! -d "$(VENV_HOST)" ]; then \
echo "Creating venv: $(VENV_HOST)"; \
python3.12 -m venv $(VENV_HOST); \
fi
. $(VENV_HOST)/bin/activate && \
pip install --upgrade pip && \
pip install -r requirements.txt && \
echo "✓ Virtual environment ready: $(VENV_HOST)"
.PHONY: test
test:
@if [ ! -d "$(VENV_HOST)" ]; then \
echo "ERROR: Run 'make install' first"; \
exit 1; \
fi
. $(VENV_HOST)/bin/activate && \
pytest -v tests/test_vectors.py && \
python src/pyhdwallet.py test
# ---------- Warm container lifecycle ----------
.PHONY: up
up:
docker run -dit -v "$$PWD":$(WORKDIR) -w $(WORKDIR) --name $(CONTAINER) $(IMAGE) bash
up: build-image
docker run -dit \
-v "$$PWD":$(WORKDIR) \
-w $(WORKDIR) \
--name $(CONTAINER) \
$(IMAGE) \
bash
.PHONY: shell
shell:
docker exec -it $(CONTAINER) bash
.PHONY: venv-container
venv-container: requirements.txt
docker exec -it $(CONTAINER) bash -lc '\
test -x $(VENV_CONTAINER)/bin/python || ( \
python -m venv $(VENV_CONTAINER) && \
. $(VENV_CONTAINER)/bin/activate && \
python -m pip install --upgrade pip setuptools wheel && \
python -m pip install -r requirements.txt \
) && \
. $(VENV_CONTAINER)/bin/activate && python --version \
'
docker exec -it $(CONTAINER) bash
.PHONY: down
down:
- docker rm -f $(CONTAINER)
- docker rm -f $(CONTAINER)
# ---------- Cleanup ----------
.PHONY: clean-venv
clean-venv:
rm -rf $(VENV_HOST)
.PHONY: clean-wheelhouse
clean-wheelhouse:
rm -rf $(WHEELHOUSE)
.PHONY: clean-vendor
clean-vendor:
rm -rf vendor/
.PHONY: clean
clean: clean-venv clean-wheelhouse
clean: down
rm -rf $(VENV_HOST) $(WHEELHOUSE) vendor/ .venv-verify .venv-offline-test
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true

207
README.md
View File

@@ -1,151 +1,172 @@
# hdwalletpy Setup & Usage Guide
# pyhdwallet Secure HD Wallet Tool
This project provides a Python-based HD Wallet tool with optional Docker-based build automation for fast, reproducible environments.
A Python command-line tool for generating and recovering BIP39 HD wallets with support for Ethereum, Solana, and Bitcoin. Designed for offline operation with optional PGP encryption and AES-encrypted ZIP artifacts.
---
## 📦 Installation Options
## 📦 Installation
### **Option 1: Standard Python Setup (Host Machine)**
### **Quick Start (macOS/Linux with Internet)**
1. Clone the repository:
```bash
# Clone repository
git clone https://github.com/<your-username>/hdwalletpy.git
cd hdwalletpy
# Install using automated script
./install_offline.sh
```
The script automatically:
- Creates Python 3.12 virtual environment
- Installs from vendored wheels (offline-capable)
- Verifies installation with test suite
- Leaves you in activated venv
---
### **Air-Gapped Installation (No Internet)**
This repository includes pre-built Python wheels for offline use.
**Supported platforms:**
- macOS ARM64 (M1/M2/M3) - Python 3.12
- Linux x86_64 (Ubuntu/Tails) - Python 3.12
**Steps:**
1. On an online machine, clone and verify:
```bash
git clone https://github.com/<your-username>/hdwalletpy.git
cd hdwalletpy
# Verify checksums
cd vendor/linux-x86_64 # or macos-arm64
sha256sum -c SHA256SUMS # Linux
shasum -a 256 -c SHA256SUMS # macOS
```
2. Create a virtual environment:
2. Transfer entire repo to USB drive
3. On air-gapped machine:
```bash
python -m venv .venv
source .venv/bin/activate
cd hdwalletpy
./install_offline.sh
```
3. Install dependencies:
4. Generate wallet:
```bash
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python src/pyhdwallet.py gen --off-screen --file
```
---
### **Option 2: Fast Setup Using Docker + Makefile**
### **Developer Installation (with Docker)**
This method avoids installing compilers on your host and speeds up dependency builds.
#### **Step 1: Build Docker Image**
For development or building wheels for other platforms:
```bash
# Build Docker image
make build-image
```
Creates a reusable image `python-build-env:3.11` with build tools and `python3-venv`.
# Build wheels for all platforms
make vendor-all
#### **Step 2: Compile Wheels in Container**
# Install development environment
make install
```bash
make wheels
```
Builds all dependency wheels into `./wheelhouse` for fast installs.
#### **Step 3: Create Host Virtual Environment**
```bash
make venv-host
source .venv_host/bin/activate
```
Installs dependencies from `wheelhouse` (no compilation needed).
#### **Optional: Work Inside a Warm Container**
```bash
make up # Start container
make shell # Open shell inside container
make venv-container # Create container-only venv at /opt/venv
```
#### **Cleanup**
```bash
make clean # Remove host venv and wheelhouse
make down # Stop/remove container
# Run tests
make test
```
---
## ✅ Basic Usage
Run the tool with subcommands. For help, use `-h` (e.g., `gen -h`, `recover -h`).
Examples:
```bash
# Generate mnemonic (prints to stdout; for debug/test)
python ./src/pyhdwallet.py gen
# Generate wallet (prints mnemonic - debug mode)
python src/pyhdwallet.py gen
# Generate and save AES-encrypted ZIP artifact into ./.wallet (password prompted)
python ./src/pyhdwallet.py gen --file
# Generate with off-screen mode + encrypted ZIP
python src/pyhdwallet.py gen --off-screen --file
# Generate with PGP encryption + ZIP artifact (ZIP contains only encrypted .asc payload)
python ./src/pyhdwallet.py gen --pgp-pubkey-file pubkeys/mykey.asc --file
# Generate with PGP encryption + ZIP
python src/pyhdwallet.py gen \
--pgp-pubkey-file pubkeys/mykey.asc \
--expected-fingerprint A27B96F2B169B5491013D2DA892B822C14A9AA18 \
--off-screen \
--file
# Recover from mnemonic (prefer --interactive to avoid shell history)
python ./src/pyhdwallet.py recover --interactive
# Recover wallet from mnemonic
python src/pyhdwallet.py recover --interactive --file
# Recover and save AES ZIP artifact
python ./src/pyhdwallet.py recover --interactive --file
# Fetch PGP public key (online)
python ./src/pyhdwallet.py fetchkey "https://example.com/key.asc" --out mykey.asc
# Fetch PGP public key (requires internet)
python src/pyhdwallet.py fetchkey "https://example.com/key.asc" --out mykey.asc
# Run tests
python ./src/pyhdwallet.py test
python src/pyhdwallet.py test
pytest -v tests/test_vectors.py
```
---
## 🔐 Notes on `--file`, AES ZIP, and PGP
## 🔐 Security Features
- `--file` writes **only** an AES-encrypted ZIP (no raw `.json`/`.asc` left on disk), using `pyzipper`.
- If `--pgp-pubkey-file` is set, the ZIP contains a single ASCII-armored PGP message (`.asc`) created with PGPy-style `PGPMessage.new(...)` then `pubkey.encrypt(...)`.
- **Offline-first**: Network access blocked during key generation/recovery
- **Test suite**: Regression tests with frozen vectors ensure derivation logic integrity
- **PGP fingerprint pinning**: Prevents key substitution attacks
- **TTY safety guard**: Refuses to print secrets when stdout is piped/redirected
- **AES-encrypted outputs**: Wallet artifacts encrypted with `pyzipper`
- **No shell history leaks**: Use `--interactive` or `--mnemonic-stdin` for recovery
---
## About `--force`
## 🛠 Makefile Targets
The tool includes a safety guard: if stdout is piped/redirected (non-TTY), it refuses to print sensitive output unless `--force` is set. Checking whether stdout is a terminal is commonly done via `isatty()`.
Example:
**Vendoring (for air-gapped deployment):**
```bash
python ./src/pyhdwallet.py gen > out.txt # likely refused (non-TTY)
python ./src/pyhdwallet.py gen --force > out.txt # allowed
make vendor-macos # Build macOS ARM64 wheels
make vendor-linux # Build Linux x86_64 wheels (Docker)
make vendor-all # Build for both platforms
make verify-vendor # Test offline installation
```
---
## ✅ Why Use Makefile?
- **Speed:** Avoid repeated `apt-get` installs; wheels cached locally.
- **Reproducibility:** Same Docker image for builds; no environment drift.
- **Convenience:** One-liner tasks (`make wheels`, `make venv-host`, `make shell`).
- **Separation:** Host venv vs container venv for clean workflows.
---
## Common Makefile Targets
**Development:**
```bash
make build-image # Build Docker image with build tools
make wheels # Compile wheels into ./wheelhouse
make venv-host # Host venv install from wheelhouse
make up # Start warm container
make shell # Shell into warm container
make venv-container # Container venv at /opt/venv
make down # Stop/remove container
make clean # Remove .venv_host and wheelhouse
make install # Create venv and install dependencies
make test # Run test suite
make build-image # Build Docker image
make shell # Open shell in Docker container
make clean # Remove venvs and build artifacts
```
---
## 📖 Full Documentation
- **[playbook.md](playbook.md)** - Complete command reference and operational guide
- **[tests/](tests/)** - Regression test suite documentation
---
## 🔒 Recommended Air-Gapped Setup
For maximum security when generating production wallets:
1. Use fresh Ubuntu Live USB or Tails OS
2. Never connect to network after booting
3. Transfer this repository via separate USB
4. Run `./install_offline.sh`
5. Generate wallet: `python src/pyhdwallet.py gen --off-screen --file`
6. Write mnemonic to paper/metal backup
7. Transfer encrypted ZIP to secure storage
8. Wipe USB drives securely
See [playbook.md](playbook.md) for detailed air-gapped procedures.

115
build-all-platforms.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/bin/bash
set -e
echo "Building wheels for all platforms using Python 3.12..."
echo ""
# Check Docker is running
if ! docker info > /dev/null 2>&1; then
echo "ERROR: Docker is not running. Please start Docker Desktop."
exit 1
fi
# 1. macOS ARM64 (via Docker with Python 3.12)
echo "==> Building macOS ARM64 wheels (Docker + Python 3.12)..."
docker run --rm \
--platform linux/arm64 \
-v $(pwd):/work \
-w /work \
python:3.12-slim \
bash -c "
apt-get update -qq && apt-get install -y -qq gcc g++ make libffi-dev > /dev/null
pip install --upgrade pip > /dev/null
mkdir -p vendor/macos-arm64-docker
pip download --dest vendor/macos-arm64-docker -r requirements.txt
pip wheel --wheel-dir vendor/macos-arm64-docker --no-deps vendor/macos-arm64-docker/*.tar.gz 2>/dev/null || true
rm -f vendor/macos-arm64-docker/*.tar.gz
"
echo "✓ macOS ARM64: $(ls vendor/macos-arm64-docker/*.whl 2>/dev/null | wc -l | xargs) wheels"
# Actually, let's use native Mac Python 3.12 if available, otherwise use what we have
echo ""
echo "==> Building macOS ARM64 wheels (native - using your .venv312)..."
mkdir -p vendor/macos-arm64
# Use the Python from your working venv
if [ -f ".venv312/bin/pip" ]; then
echo "Using Python from .venv312..."
.venv312/bin/pip download --dest vendor/macos-arm64 -r requirements.txt
echo "✓ macOS ARM64: $(ls vendor/macos-arm64/*.whl 2>/dev/null | wc -l | xargs) wheels"
else
echo "ERROR: .venv312 not found!"
echo "Please activate your working venv first:"
echo " source .venv312/bin/activate"
echo " pip download --dest vendor/macos-arm64 -r requirements.txt"
exit 1
fi
# 2. Linux x86_64 (via Docker)
echo ""
echo "==> Building Linux x86_64 wheels (Docker + Python 3.12)..."
docker run --rm \
-v $(pwd):/work \
-w /work \
python:3.12-slim \
bash -c "
apt-get update -qq && apt-get install -y -qq gcc g++ make libffi-dev > /dev/null
pip install --upgrade pip > /dev/null
mkdir -p vendor/linux-x86_64
pip download --dest vendor/linux-x86_64 -r requirements.txt
pip wheel --wheel-dir vendor/linux-x86_64 --no-deps vendor/linux-x86_64/*.tar.gz 2>/dev/null || true
rm -f vendor/linux-x86_64/*.tar.gz
"
echo "✓ Linux x86_64: $(ls vendor/linux-x86_64/*.whl 2>/dev/null | wc -l | xargs) wheels"
# 3. Generate SHA256 checksums
echo ""
echo "==> Generating checksums..."
for platform in macos-arm64 linux-x86_64; do
if [ -d "vendor/$platform" ] && [ "$(ls -A vendor/$platform/*.whl 2>/dev/null)" ]; then
(cd vendor/$platform && shasum -a 256 *.whl > SHA256SUMS)
echo "✓ Checksums for $platform"
fi
done
# 4. Create provenance file
echo ""
echo "==> Creating provenance documentation..."
cat > vendor/PROVENANCE.md << EOF
# Dependency Provenance
All wheels generated on: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
Python version: 3.12
Build machine: $(uname -s) $(uname -m)
Docker used: Yes (python:3.12-slim)
## Platforms
- macOS ARM64: Built using .venv312 (Python 3.12)
- Linux x86_64: Built using Docker python:3.12-slim
## Package Versions
$(cat requirements.txt | head -20)
## Integrity Verification
Each platform directory contains SHA256SUMS for verification:
\`\`\`bash
cd vendor/linux-x86_64
shasum -a 256 -c SHA256SUMS
\`\`\`
Last updated: $(date -u +"%Y-%m-%d")
Built by: $(git config user.name) <$(git config user.email)>
Commit: $(git rev-parse --short HEAD 2>/dev/null || echo "not in git")
EOF
echo ""
echo "✓ All platforms built successfully!"
echo ""
echo "Summary:"
echo " macOS ARM64: vendor/macos-arm64/ ($(ls vendor/macos-arm64/*.whl 2>/dev/null | wc -l | xargs) wheels)"
echo " Linux x86_64: vendor/linux-x86_64/ ($(ls vendor/linux-x86_64/*.whl 2>/dev/null | wc -l | xargs) wheels)"
echo ""
echo "Next steps:"
echo " 1. Verify: ls vendor/*/SHA256SUMS"
echo " 2. Test: ./install_offline.sh"
echo " 3. Commit: git add vendor/ && git commit -m 'vendor: add multi-platform wheels'"

117
install_offline.sh Executable file
View File

@@ -0,0 +1,117 @@
#!/bin/bash
set -e
# Detect platform
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
PLATFORM="linux-x86_64"
elif [[ "$OSTYPE" == "darwin"* ]]; then
ARCH=$(uname -m)
if [[ "$ARCH" == "arm64" ]]; then
PLATFORM="macos-arm64"
else
echo "ERROR: Only macOS ARM64 is supported"
exit 1
fi
else
echo "ERROR: Unsupported platform: $OSTYPE"
exit 1
fi
echo "Detected platform: $PLATFORM"
# Check if vendor directory exists
if [ ! -d "vendor/$PLATFORM" ]; then
echo "ERROR: vendor/$PLATFORM not found!"
echo "Run ./build-vendors-simple.sh to generate wheels"
exit 1
fi
# Check for Python 3.12
if ! command -v python3.12 &> /dev/null; then
echo "ERROR: Python 3.12 not found!"
echo "Please install Python 3.12:"
echo " macOS: brew install python@3.12"
echo " Linux: apt-get install python3.12 python3.12-venv"
exit 1
fi
# Check if already in a venv
if [ -n "$VIRTUAL_ENV" ]; then
CURRENT_PYTHON=$(python --version 2>&1 | grep -oE '3\.[0-9]+')
if [[ "$CURRENT_PYTHON" != "3.12" ]]; then
echo "WARNING: You are in a venv with Python $CURRENT_PYTHON"
echo "Deactivating and creating new Python 3.12 venv..."
deactivate || true
else
echo "Already in Python 3.12 venv, using it..."
fi
fi
# Create or use existing venv
VENV_DIR=".venv"
if [ ! -d "$VENV_DIR" ] || [ ! -f "$VENV_DIR/bin/python3.12" ]; then
echo "Creating Python 3.12 virtual environment..."
python3.12 -m venv "$VENV_DIR"
echo "✓ Virtual environment created: $VENV_DIR"
else
echo "Using existing venv: $VENV_DIR"
fi
# Activate venv
source "$VENV_DIR/bin/activate"
# Verify Python version
PYTHON_VERSION=$(python --version 2>&1 | grep -oE '3\.[0-9]+')
echo "Using Python $PYTHON_VERSION from: $(which python)"
if [[ "$PYTHON_VERSION" != "3.12" ]]; then
echo "ERROR: Expected Python 3.12, got $PYTHON_VERSION"
exit 1
fi
# Verify checksums
if [ -f "vendor/$PLATFORM/SHA256SUMS" ]; then
echo ""
echo "Verifying checksums..."
(cd vendor/$PLATFORM && shasum -a 256 -c SHA256SUMS --quiet) || {
echo "ERROR: Checksum verification failed!"
exit 1
}
echo "✓ Checksums verified"
fi
# Install
echo ""
echo "Installing from vendor/$PLATFORM..."
pip install --upgrade pip --quiet
pip install --no-index --find-links=vendor/$PLATFORM -r requirements.txt
# Test
echo ""
echo "Running tests..."
python -m pytest -v tests/test_vectors.py || {
echo "ERROR: Tests failed!"
exit 1
}
echo ""
echo "Running built-in smoke test..."
python src/pyhdwallet.py test || {
echo "ERROR: Smoke test failed!"
exit 1
}
echo ""
echo "=========================================="
echo "✓ Installation complete and verified!"
echo "=========================================="
echo ""
echo "Virtual environment: $VENV_DIR"
echo "Python version: $(python --version)"
echo ""
echo "To activate this environment:"
echo " source $VENV_DIR/bin/activate"
echo ""
echo "Generate a wallet with:"
echo " python src/pyhdwallet.py gen --off-screen --file"
echo ""

View File

@@ -10,13 +10,18 @@ Repository structure (current):
- `bootstrap_vectors.py` — one-time script to generate test vectors
- `vectors.json` — frozen expected values (committed)
- `data/recipient.asc` — test PGP key (committed)
- `.wallet/`generated wallet artifacts (should be gitignored)
- `vendor/`vendored Python wheels for offline installation
- `macos-arm64/` — macOS Apple Silicon wheels (Python 3.12)
- `linux-x86_64/` — Linux x86_64 wheels (Python 3.12)
- `.wallet/` — generated wallet artifacts (gitignored)
- `requirements.in`, `requirements.txt`
- `install_offline.sh` — automated offline installer
- `Dockerfile`, `Makefile` — Docker-based build tools
- `README.md`, `playbook.md`, `LICENSE`
**Note**: This tool requires a subcommand (e.g., `gen`, `recover`). Running without one displays help. Use `<subcommand> -h` for detailed options.
---
***
## Features
@@ -27,33 +32,101 @@ Repository structure (current):
- **TTY safety guard + --force**: If stdout is piped/redirected (non-TTY), the tool refuses to print sensitive data unless `--force` is explicitly set (to avoid accidental leaks into logs/files). `isatty()` is the classic way to detect whether stdout is connected to a terminal.
- **Off-screen mode**: `--off-screen` suppresses printing sensitive data to stdout.
- **Regression test suite**: Offline tests with frozen vectors ensure derivation logic, PGP fingerprinting, and seed generation remain stable across code changes.
- **Vendored dependencies**: Pre-built wheels for macOS ARM64 and Linux x86_64 enable true air-gapped installation.
---
***
## Installation
### Prerequisites
- Python 3.11+
- Recommended: a virtual environment
- Python 3.12.x (required for vendored wheels)
- Git (for cloning repository)
### Setup
### Quick Installation (macOS/Linux with Internet)
```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
# Clone repository
git clone https://github.com/<your-username>/hdwalletpy.git
cd hdwalletpy
# Run automated installer
./install_offline.sh
```
### Dependencies (top-level intent)
The script automatically:
- Detects your platform (macOS ARM64 or Linux x86_64)
- Creates Python 3.12 virtual environment
- Installs from vendored wheels (no PyPI access needed)
- Verifies installation with test suite
- Leaves you in activated venv
### Air-Gapped Installation (No Internet)
For maximum security on an air-gapped machine (Tails OS, Ubuntu Live USB, etc.):
**On online machine:**
```bash
# Clone and verify
git clone https://github.com/<your-username>/hdwalletpy.git
cd hdwalletpy
# Verify checksums
cd vendor/linux-x86_64 # or macos-arm64 for Mac
sha256sum -c SHA256SUMS # Linux
shasum -a 256 -c SHA256SUMS # macOS
# Transfer entire repo to USB
```
**On air-gapped machine:**
```bash
# Ensure Python 3.12 is installed
python3.12 --version
# Navigate to repository
cd hdwalletpy
# Run installer (creates venv, installs offline)
./install_offline.sh
# Generate wallet
python src/pyhdwallet.py gen --off-screen --file
```
### Manual Installation (Without Script)
```bash
# Create venv
python3.12 -m venv .venv
source .venv/bin/activate
# Install from vendored wheels
# For macOS ARM64:
pip install --no-index --find-links=vendor/macos-arm64 -r requirements.txt
# For Linux x86_64:
pip install --no-index --find-links=vendor/linux-x86_64 -r requirements.txt
# Verify installation
pytest -v tests/test_vectors.py
python src/pyhdwallet.py test
```
### Dependencies (Top-Level Intent)
- `bip-utils` — BIP39 + BIP derivation logic
- `PGPy` — encryption to OpenPGP public keys
- `pynacl` + `base58` — Solana seed/key handling
- `pyzipper` — AES-encrypted ZIP writing (only needed when using `--file`)
- `pyzipper` — AES-encrypted ZIP writing
- `pytest` — test framework (development only)
---
All dependencies are pre-downloaded in `vendor/` for offline use.
***
## Quick Start
@@ -100,7 +173,7 @@ python ./src/pyhdwallet.py test
pytest -v tests/test_vectors.py
```
---
***
## Testing
@@ -154,16 +227,75 @@ git commit -m "test: update vectors after intentional derivation change"
**Warning**: If tests fail after a code change and you didn't intend to change behavior, **do not regenerate vectors**. Fix the code regression instead.
---
***
## Outputs and file structure
## Vendored Dependencies
### Stdout behavior
### What is Vendoring?
This repository includes pre-built Python wheels in the `vendor/` directory, enabling installation without internet access or PyPI. This is critical for air-gapped security.
**Supported platforms:**
- macOS ARM64 (M1/M2/M3/M4) - Python 3.12
- Linux x86_64 (Ubuntu, Tails, Debian) - Python 3.12
### Building Vendor Wheels (Maintainers)
If you need to update dependencies or add new platforms:
```bash
# Using Makefile (recommended)
make vendor-all # Build for both macOS and Linux
make vendor-macos # macOS ARM64 only
make vendor-linux # Linux x86_64 only (requires Docker)
make verify-vendor # Test offline installation
# Commit updated wheels
git add vendor/
git commit -m "vendor: update dependencies to latest versions"
```
**Manual build (macOS):**
```bash
mkdir -p vendor/macos-arm64
source .venv312/bin/activate
pip download --dest vendor/macos-arm64 -r requirements.txt
cd vendor/macos-arm64 && shasum -a 256 *.whl > SHA256SUMS
```
**Manual build (Linux via Docker):**
```bash
docker run --rm -v $(pwd):/work -w /work python:3.12-slim bash -c "
apt-get update && apt-get install -y gcc g++ make libffi-dev
pip install --upgrade pip
mkdir -p vendor/linux-x86_64
pip download --dest vendor/linux-x86_64 -r requirements.txt
cd vendor/linux-x86_64 && sha256sum *.whl > SHA256SUMS
"
```
### Verifying Vendor Integrity
```bash
# Check checksums before using on air-gapped machine
cd vendor/linux-x86_64
sha256sum -c SHA256SUMS # Linux
shasum -a 256 -c SHA256SUMS # macOS
```
***
## Outputs and File Structure
### Stdout Behavior
- `gen` prints the mnemonic + derived addresses by default (intended for debug/test comparisons).
- `--off-screen` suppresses printing sensitive data to stdout.
### `--file` behavior (deterministic, secured output)
### `--file` Behavior (Deterministic, Secured Output)
If `--file` is present, the tool writes **only** an AES-encrypted ZIP file (no raw `.json`/`.asc` file is left on disk). AES ZIP is implemented using `pyzipper`.
@@ -175,12 +307,12 @@ Override folder:
- `--wallet-location /path/to/folder`
Naming uses UTC timestamps (e.g. `20260106_161830Z`):
Naming uses UTC timestamps (e.g. `20260108_011830Z`):
- No PGP: zip contains `test_wallet_<UTC>.json`, zip name `test_wallet_<UTC>.zip`
- With PGP: zip contains `encrypted_wallet_<UTC>.asc`, zip name `encrypted_wallet_<UTC>.zip`
### Password handling for ZIP
### Password Handling for ZIP
- Default: `--zip-password-mode prompt` prompts for the ZIP password (attempts hidden entry).
- If hidden entry is not supported in the environment, it falls back to visible `input()` with loud warnings.
@@ -189,7 +321,7 @@ Naming uses UTC timestamps (e.g. `20260106_161830Z`):
`pyzipper` supports AES encryption via `AESZipFile` and password-setting APIs.
---
***
## Commands
@@ -209,7 +341,7 @@ Options:
- `--off-screen`: reduced output / temp-file behavior
- `--expected-fingerprint`: refuse if downloaded key fingerprint doesn't match (40 hex chars)
---
***
### gen (offline)
@@ -248,7 +380,7 @@ Safety options:
- `--off-screen` (don't print sensitive output)
- `--force` (only for non-TTY stdout; see below)
---
***
### recover (offline)
@@ -274,7 +406,7 @@ Notes:
- `--export-private` requires `--pgp-pubkey-file` (private keys only travel inside encrypted payload).
- `--include-source` controls whether mnemonic is included inside the encrypted payload (only meaningful when PGP is used).
---
***
### test (offline)
@@ -290,9 +422,9 @@ For comprehensive regression testing, use the pytest suite:
pytest -v tests/test_vectors.py
```
---
***
## When to use `--force`
## When to Use `--force`
Use `--force` only if you **intentionally** want to print sensitive output while stdout is being piped/redirected.
@@ -314,17 +446,193 @@ python ./src/pyhdwallet.py gen --force > out.txt
If running normally in your interactive terminal, stdout is a TTY and `--force` does nothing (output looks the same).
---
***
## Security notes (practical)
## Security Notes (Practical)
- `gen` printing the mnemonic is intentionally "debug/test" behavior. Assume stdout can be recorded (scrollback, logging, screen recording, CI logs).
- Prefer `--off-screen` for reduced exposure.
- Prefer `--file` so artifacts go into `.wallet/` and are AES-encrypted via `pyzipper`.
- For stronger at-rest security: combine `--pgp-pubkey-file` + `--file` so the ZIP contains only an encrypted `.asc` payload. PGPy encryption style follows `PGPMessage.new(...)` then `pubkey.encrypt(...)`.
- Use `--expected-fingerprint` to enforce PGP key pinning and prevent key substitution attacks.
- Install from vendored wheels on air-gapped machines to avoid supply chain attacks.
---
***
## Offline Security Best Practices
This tool is designed for offline use, but true security depends on the **environment** where you run it. Below are operational security recommendations for generating keys you can trust.
### Air-Gapped Setup (Highest Security)
For maximum security when generating production wallets, use an **air-gapped computer**—a device that has never been and will never be connected to any network.
**Recommended procedure:**
1. **Prepare a clean machine**:
- Use a dedicated laptop or bootable USB with a fresh Linux installation (e.g., Tails, Ubuntu Live USB)
- Never connect this device to WiFi, Ethernet, or Bluetooth
- Physically disable network interfaces if possible (remove WiFi card, tape over Ethernet port)
2. **Transfer repository offline**:
```bash
# On trusted online machine, clone and verify
git clone https://github.com/<your-username>/hdwalletpy.git
cd hdwalletpy
# Verify checksums
cd vendor/linux-x86_64
sha256sum -c SHA256SUMS
# Transfer entire repo to USB
```
3. **Verify code integrity on air-gapped machine**:
```bash
# Check checksums again
cd vendor/linux-x86_64
sha256sum -c SHA256SUMS
# Run test suite to verify derivation logic
./install_offline.sh
pytest -v tests/test_vectors.py
```
4. **Generate wallet**:
```bash
python ./src/pyhdwallet.py gen --pgp-pubkey-file pubkeys/recipient.asc --file --off-screen
```
5. **Transfer output safely**:
- Copy only the `.wallet/*.zip` file to USB (never copy `pyhdwallet.py` or Python environment back to online machine)
- The ZIP is AES-encrypted; the inner `.asc` is PGP-encrypted
6. **Destroy or securely wipe** the USB after transfer if it contained unencrypted secrets
### Threats Air-Gapping Mitigates
- **Remote attacks**: Malware cannot exfiltrate keys over the network
- **Clipboard hijacking**: No clipboard manager or remote access tool can intercept data
- **Browser/OS telemetry**: No accidental upload of terminal history or crash dumps
### Threats Air-Gapping Does NOT Fully Mitigate
Research shows that sophisticated attackers with physical access can potentially exfiltrate data from air-gapped systems via covert channels (acoustic, electromagnetic, optical). However:
- These attacks require physical proximity and pre-installed malware
- For individual users (not nation-state targets), air-gapping remains highly effective
- Countermeasures: Use the machine in a secure location, inspect for unfamiliar USB devices, verify software integrity before installation
### Physical Security
**Mnemonic handling:**
- Write the mnemonic on paper immediately; never store it digitally on the air-gapped machine
- Use a metal backup (e.g., Cryptosteel) for fire/water resistance
- Split storage across multiple secure locations if desired (Shamir's Secret Sharing for advanced users)
**Device handling:**
- After generating the wallet, optionally wipe the air-gapped device or destroy the bootable USB
- If reusing the device, use secure-erase tools (not just `rm`)
### Verification Through Testing
**Why the test suite matters for trust:**
The committed test suite (`tests/test_vectors.py`) allows you to verify that derivation logic hasn't been tampered with before generating production keys:
```bash
# On the air-gapped machine, before generating your wallet:
pytest -v tests/test_vectors.py
```
If all tests pass, you have cryptographic proof that:
- BIP39 seed derivation matches the well-known test vector ("abandon abandon..." → known seed)
- Derivation paths produce addresses matching public BIP39/BIP44 test vectors
- PGP fingerprinting logic works correctly
If tests fail, **do not generate keys**—the code may be compromised or buggy.
### Entropy Sources
**Built-in entropy (default):**
- Python's `secrets.token_bytes()` uses OS-provided CSPRNG (`/dev/urandom` on Linux, `CryptGenRandom` on Windows)
- This is cryptographically secure for typical use
**Additional user entropy (optional):**
```bash
python ./src/pyhdwallet.py gen --dice-rolls "4 2 6 1 3 5 ..." --file
```
- Roll a die 50+ times and input the results
- Your dice rolls are mixed with OS entropy via SHA256
- Protects against potential CSPRNG backdoors (theoretical concern)
### PGP Key Fingerprint Pinning
Always use `--expected-fingerprint` when encrypting to a PGP key to prevent key substitution attacks:
```bash
# First, get and verify the fingerprint of your recipient key
python ./src/pyhdwallet.py fetchkey https://example.com/mykey.asc --out mykey.asc
# Manually verify fingerprint matches what you expect (check via another channel)
# Then use it with pinning:
python ./src/pyhdwallet.py gen \
--pgp-pubkey-file mykey.asc \
--expected-fingerprint A27B96F2B169B5491013D2DA892B822C14A9AA18 \
--file
```
Without `--expected-fingerprint`, an attacker who controls the filesystem could swap `mykey.asc` with their own key.
### Operational Checklist
Before generating production wallets:
- [ ] Running on air-gapped machine or fresh Live USB
- [ ] Network physically disabled (no WiFi/Ethernet/Bluetooth)
- [ ] Code integrity verified (checksums + test suite passes)
- [ ] Using `--off-screen` to minimize terminal scrollback exposure
- [ ] Using `--file` to avoid leaving unencrypted files on disk
- [ ] Using `--pgp-pubkey-file` with `--expected-fingerprint` for key pinning
- [ ] Paper/metal backup prepared for mnemonic
- [ ] Output ZIP password stored separately from ZIP file
- [ ] Plan for USB secure wipe after transfer
### Lower-Risk Scenarios
Air-gapping is overkill for:
- **Learning/testing**: Use your regular laptop with `--off-screen` and `--file`
- **Small amounts**: Generate on a clean, updated machine with minimal software
- **Testnet wallets**: Standard laptop is fine
Air-gapping is recommended for:
- **Life savings or business funds**
- **Long-term cold storage** (multi-year hold)
- **Institutional custody scenarios**
### Trust But Verify
The only way to fully trust this tool (or any wallet software) is to:
1. **Read the source code** (`src/pyhdwallet.py` is ~1400 lines, single file)
2. **Verify test vectors** match published BIP39 test data
3. **Run the test suite** on your air-gapped machine
4. **Generate a test wallet** and verify addresses on a block explorer using a separate tool
Never trust wallet software blindly—especially for significant funds.
***
## Troubleshooting
@@ -339,26 +647,51 @@ chmod +x ./src/pyhdwallet.py
./src/pyhdwallet.py gen
```
### "Missing dependency: pyzipper" but pip says installed
### Python 3.12 not found
You likely installed into a different venv. Use:
Vendored wheels require Python 3.12:
```bash
python -m pip install pyzipper
python -c "import pyzipper; print(pyzipper.__version__)"
# macOS
brew install python@3.12
# Ubuntu/Debian
sudo apt-get install python3.12 python3.12-venv
# Verify
python3.12 --version
```
### "Missing dependency: pytest" when running tests
### "Missing dependency" errors
Install pytest in your virtualenv:
Make sure you're installing from the correct vendor directory:
```bash
python -m pip install pytest
# Check your platform
uname -sm
# Install for macOS ARM64
pip install --no-index --find-links=vendor/macos-arm64 -r requirements.txt
# Install for Linux x86_64
pip install --no-index --find-links=vendor/linux-x86_64 -r requirements.txt
```
### Checksum verification failures
If `SHA256SUMS` verification fails, the wheels may be corrupted or tampered with:
```bash
cd vendor/linux-x86_64
sha256sum -c SHA256SUMS
# If failures occur, re-download from trusted source
# DO NOT use corrupted wheels for production wallets
```
### Unzipping AES ZIP issues
Some `unzip` tools may not support AES-encrypted ZIPs. Use Python + pyzipper to extract if needed:
Some `unzip` tools may not support AES-encrypted ZIPs. Use Python + pyzipper to extract:
```bash
python - <<'PY'
@@ -373,8 +706,6 @@ print("Extracted to", out_dir)
PY
```
AES ZIP support is the reason `pyzipper` is used.
### Test failures after code changes
If `pytest -v tests/test_vectors.py` fails after you modified code:
@@ -386,7 +717,45 @@ If `pytest -v tests/test_vectors.py` fails after you modified code:
2. **Is only one test failing?**
- Run with `-vv` for detailed diff: `pytest -vv tests/test_vectors.py::test_name`
---
***
## Development
### Building Vendor Wheels
See [Vendored Dependencies](#vendored-dependencies) section above.
### Running Tests (Dev)
```bash
# Quick test
python src/pyhdwallet.py test
# Full regression suite
pytest -v tests/test_vectors.py
# Specific test
pytest -v tests/test_vectors.py::test_address_derivation_integrity
# With coverage
pytest --cov=src tests/
```
### Docker Development Environment
```bash
# Build image
make build-image
# Start warm container
make up
make shell
# Inside container
python src/pyhdwallet.py test
```
***
## Changelog
@@ -398,22 +767,12 @@ If `pytest -v tests/test_vectors.py` fails after you modified code:
- Added: `--force` to override the non-TTY printing safety guard (use only when redirecting/piping).
- Added: `--expected-fingerprint` for PGP key pinning.
- Added: Offline regression test suite with frozen vectors (`tests/test_vectors.py`).
- Added: Vendored dependencies for macOS ARM64 and Linux x86_64 (Python 3.12).
- Added: `install_offline.sh` automated installer for air-gapped deployment.
- Changed: `recover` now uses `--mnemonic-stdin` and `--interactive` instead of `--mnemonic` CLI arg.
- Updated: Dockerfile and Makefile now support vendoring workflow.
- **v1.0.3** Documentation and CLI behavior updates.
- **v1.0.2** Added off-screen behaviors and security improvements.
- **v1.0.1** Renamed to pyhdwallet and added version flag.
- **v1.0.0** Initial release.
Key additions:
1. **Testing section** with full coverage explanation
2. **Repository structure** updated to include `tests/` folder
3. **Test artifacts** documentation
4. **When to regenerate vectors** guidance
5. **Troubleshooting** section for test failures
6. **Changelog** updated with v1.0.5 test suite addition
7. **Quick Start** updated with correct `recover` command syntax (`--mnemonic-stdin`)
8. **Dependencies** updated to include pytest
[1](https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/50321846/e5caab46-cb40-4f76-8ea9-60ed65bf4927/pyhdwallet.py)
[2](https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/images/50321846/085d5710-8637-45d5-8686-77c98df9dcdf/image.jpg)

2
requirements-dev.in Normal file
View File

@@ -0,0 +1,2 @@
-r requirements.txt # Include production deps
pytest>=9.0.0

81
requirements-dev.txt Normal file
View File

@@ -0,0 +1,81 @@
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile requirements-dev.in
#
base58==2.1.1
# via -r requirements.txt
bip-utils==2.10.0
# via -r requirements.txt
cbor2==5.8.0
# via
# -r requirements.txt
# bip-utils
cffi==2.0.0
# via
# -r requirements.txt
# cryptography
# pynacl
coincurve==21.0.0
# via
# -r requirements.txt
# bip-utils
crcmod==1.7
# via
# -r requirements.txt
# bip-utils
cryptography==46.0.3
# via
# -r requirements.txt
# pgpy
ecdsa==0.19.1
# via
# -r requirements.txt
# bip-utils
ed25519-blake2b==1.4.1
# via
# -r requirements.txt
# bip-utils
iniconfig==2.3.0
# via pytest
packaging==25.0
# via pytest
pgpy==0.6.0
# via -r requirements.txt
pluggy==1.6.0
# via pytest
py-sr25519-bindings==0.2.3
# via
# -r requirements.txt
# bip-utils
pyasn1==0.6.1
# via
# -r requirements.txt
# pgpy
pycparser==2.23
# via
# -r requirements.txt
# cffi
pycryptodome==3.23.0
# via
# -r requirements.txt
# bip-utils
pycryptodomex==3.23.0
# via
# -r requirements.txt
# pyzipper
pygments==2.19.2
# via pytest
pynacl==1.6.2
# via
# -r requirements.txt
# bip-utils
pytest==9.0.2
# via -r requirements-dev.in
pyzipper==0.3.6
# via -r requirements.txt
six==1.17.0
# via
# -r requirements.txt
# ecdsa

View File

@@ -1,6 +1,8 @@
base58==2.1.1
bip-utils==2.10.0
pgpy==0.6.0
pip-chill==1.0.3
pip-tools==7.5.2
pyzipper==0.3.6
# Core crypto libraries
bip-utils>=2.9.0
PGPy>=0.6.0
pynacl>=1.5.0
base58>=2.1.1
# ZIP encryption
pyzipper>=0.3.6

43
vendor/PROVENANCE.md vendored Normal file
View File

@@ -0,0 +1,43 @@
# Dependency Provenance
All wheels generated on: 2026-01-07 17:06:51 UTC
Python version: 3.12
Build machine: Darwin arm64
Docker used: Yes (python:3.12-slim)
## Platforms
- macOS ARM64: Built using .venv312 (Python 3.12)
- Linux x86_64: Built using Docker python:3.12-slim
## Package Versions
base58==2.1.1
bip_utils==2.10.0
build==1.3.0
cbor2==5.8.0
cffi==2.0.0
click==8.3.1
coincurve==21.0.0
crcmod==1.7
cryptography==46.0.3
ecdsa==0.19.1
ed25519-blake2b==1.4.1
iniconfig==2.3.0
packaging==25.0
PGPy==0.6.0
pip-chill==1.0.3
pip-tools==7.5.2
pluggy==1.6.0
py-sr25519-bindings==0.2.3
pyasn1==0.6.1
pycparser==2.23
## Integrity Verification
Each platform directory contains SHA256SUMS for verification:
```bash
cd vendor/linux-x86_64
shasum -a 256 -c SHA256SUMS
```
Last updated: 2026-01-07
Built by: LC mac <leochan@hkjin.com>
Commit: 2807982

31
vendor/linux-x86_64/SHA256SUMS vendored Normal file
View File

@@ -0,0 +1,31 @@
11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 base58-2.1.1-py3-none-any.whl
33792674bda552a071a539b6590b2986aa8c08d0c9c30c2566d7cb323173310d bip_utils-2.10.0-py3-none-any.whl
7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 build-1.3.0-py3-none-any.whl
518c118a5e00001854adb51f3164e647aa99b6a9877d2a733a28cb5c0a4d6857 cbor2-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 click-8.3.1-py3-none-any.whl
5a366c314df7217e3357bb8c7d2cda540b0bce180705f7a0ce2d1d9e28f62ad4 coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
82d740f4e923b88981b6ca8b854dceb270f319c81153211ccbec6ee523c7ccee crcmod-1.7-cp312-cp312-linux_aarch64.whl
549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91 cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl
30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 ecdsa-0.19.1-py2.py3-none-any.whl
96427209d130354f399ef20acc444c2ac76dd05b13cd270dea79909c39cdf6ec ed25519_blake2b-1.4.1-cp312-cp312-linux_aarch64.whl
f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 iniconfig-2.3.0-py3-none-any.whl
29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 packaging-25.0-py3-none-any.whl
52a584525a9032726a8e28fc02ced9cec896f54aa5c6f5552035000821525f2d pgpy-0.6.0-py3-none-any.whl
452a38edbcdfc333301c438c26ba00a0762d2034fe26a235d8587134453ccdb1 pip_chill-1.0.3-py2.py3-none-any.whl
2fe16db727bbe5bf28765aeb581e792e61be51fc275545ef6725374ad720a1ce pip_tools-7.5.2-py3-none-any.whl
9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd pip-25.3-py3-none-any.whl
e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 pluggy-1.6.0-py3-none-any.whl
a3929c291408e67a1a11566f251b9f7d06c3fb3ae240caec44b9181de09e3fc9 py_sr25519_bindings-0.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 pyasn1-0.6.1-py3-none-any.whl
e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 pycparser-2.23-py3-none-any.whl
67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490 pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587 pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b pygments-2.19.2-py3-none-any.whl
26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130 pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl
9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 pyproject_hooks-1.2.0-py3-none-any.whl
711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b pytest-9.0.2-py3-none-any.whl
6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87 pyzipper-0.3.6-py2.py3-none-any.whl
062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 setuptools-80.9.0-py3-none-any.whl
4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 six-1.17.0-py2.py3-none-any.whl
708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 wheel-0.45.1-py3-none-any.whl

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

28
vendor/macos-arm64/SHA256SUMS vendored Normal file
View File

@@ -0,0 +1,28 @@
11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 base58-2.1.1-py3-none-any.whl
33792674bda552a071a539b6590b2986aa8c08d0c9c30c2566d7cb323173310d bip_utils-2.10.0-py3-none-any.whl
7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 build-1.3.0-py3-none-any.whl
4b3f91fa699a5ce22470e973601c62dd9d55dc3ca20ee446516ac075fcab27c9 cbor2-5.8.0-cp312-cp312-macosx_11_0_arm64.whl
8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl
981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 click-8.3.1-py3-none-any.whl
1cb1cd19fb0be22e68ecb60ad950b41f18b9b02eebeffaac9391dc31f74f08f2 coincurve-21.0.0-cp312-cp312-macosx_11_0_arm64.whl
109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl
30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 ecdsa-0.19.1-py2.py3-none-any.whl
f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 iniconfig-2.3.0-py3-none-any.whl
29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 packaging-25.0-py3-none-any.whl
452a38edbcdfc333301c438c26ba00a0762d2034fe26a235d8587134453ccdb1 pip_chill-1.0.3-py2.py3-none-any.whl
2fe16db727bbe5bf28765aeb581e792e61be51fc275545ef6725374ad720a1ce pip_tools-7.5.2-py3-none-any.whl
9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd pip-25.3-py3-none-any.whl
e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 pluggy-1.6.0-py3-none-any.whl
4443adf871e224493c4ee4c06be205a10ea649a781132af883f6638fd7acc9d7 py_sr25519_bindings-0.2.3-cp312-cp312-macosx_11_0_arm64.whl
0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 pyasn1-0.6.1-py3-none-any.whl
e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 pycparser-2.23-py3-none-any.whl
187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27 pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl
06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6 pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl
86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b pygments-2.19.2-py3-none-any.whl
c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465 pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl
9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 pyproject_hooks-1.2.0-py3-none-any.whl
711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b pytest-9.0.2-py3-none-any.whl
6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87 pyzipper-0.3.6-py2.py3-none-any.whl
062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 setuptools-80.9.0-py3-none-any.whl
4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 six-1.17.0-py2.py3-none-any.whl
708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 wheel-0.45.1-py3-none-any.whl