Security Guide
This guide covers HPM’s security features, the threat model, package signing, and operational best practices.
Table of contents
- Integrity and reproducibility
- Package signing
hpm audit- Transport security
- Storage layout
- Best practices
- Threat model
- Reporting vulnerabilities
Integrity and reproducibility
HPM protects against tampering and drift through three layers.
SHA-256 checksums
Every package HPM installs is hashed end-to-end, and the hash is recorded in
hpm.lock:
# hpm.lock (excerpt)
version = 1
[package]
name = "my-studio/my-tools"
version = "1.0.0"
[dependencies."studio/utility-nodes"]
version = "1.2.0"
checksum = "sha256:a3f2b8c9..."
[dependencies."studio/utility-nodes".source]
type = "registry"
registry = "houdinihub"
[metadata]
generated_at = "2026-03-14T10:30:00Z"
hpm_version = "0.7.2"
platform = "linux-x86_64"
On every hpm install, HPM verifies each cached package in ~/.hpm/packages/
against the checksum in hpm.lock before using it. A mismatch aborts the
install with a clear error — corrupted or tampered packages never silently
reach Houdini.
Lock file pinning
hpm.lock pins exact resolved versions of every HPM and Python dependency.
Commit it to version control. In CI, use --frozen-lockfile to fail the
build if the lock would need to change:
hpm install --frozen-lockfile
This catches two failure modes:
- The lock file is missing entirely.
- A dependency’s constraint or a registry’s contents have drifted since the lock was written.
Staleness detection
Lock files include a generated_at timestamp. HPM warns on install and
audit if the lock is older than 90 days — a heuristic for “you probably
haven’t checked for security fixes in a while”. The warning is advisory; it
does not block the install.
Package signing
hpm pack can sign the archive it produces with an Ed25519 key. Consumers
(registries, internal pipelines, end users) can verify that signature
before trusting the package.
Wire format
| Property | Value |
|---|---|
| Signature algorithm | Ed25519 (RFC 8032). |
| Private key format | PKCS#8 PEM (-----BEGIN PRIVATE KEY-----). Generated by openssl genpkey -algorithm ed25519. |
| Public key format | SubjectPublicKeyInfo PEM (-----BEGIN PUBLIC KEY-----). Extracted with openssl pkey -pubout. |
| Signature encoding | Standard base64 (RFC 4648, alphabet A–Z a–z 0–9 + /, = padding). Emitted as fileSignature. Verifiers must use the standard alphabet, not base64url. |
| Key identifier | First 8 bytes of the Ed25519 public key, hex-encoded (16 characters). Emitted as keyId. Lets registries index multiple keys per creator without transmitting the full public key on every artifact. |
| Signed payload | The raw bytes of the produced .zip archive. |
Generating a key pair
openssl genpkey -algorithm ed25519 -out signing.pem
openssl pkey -in signing.pem -pubout -out signing.pub.pem
Keep signing.pem private. Publish signing.pub.pem through your registry
or creator dashboard so consumers know what to verify against.
Signing a package
Three equivalent ways, tried in this order:
# 1. Explicit flag
hpm pack --key ~/.hpm/signing.pem
# 2. Environment variable. Either a path, or inline PEM content
# (detected by a leading "-----BEGIN" marker). The inline form is meant
# for CI systems that inject secrets as plain strings.
export HPM_SIGNING_KEY="$(cat signing.pem)"
hpm pack
# 3. Configured fallback in ~/.hpm/config.toml
# [signing]
# key_path = "/Users/me/.hpm/signing.pem"
hpm pack
The resolution order is flag → env → config. The first that’s set wins.
Output
With --json, hpm pack emits a machine-readable record suitable for CI
pipelines and registry upload tooling:
{
"archive": "dist/my-tools-1.0.0.zip",
"sha256": "a3f2b8c9...",
"signature": "base64-standard-encoded-ed25519-signature",
"key_id": "7a1c3e5f9b2d4860",
"platform": "linux-x86_64"
}
signature and key_id are present only when a signing key was supplied.
platform is present only when the manifest declares [compat].platforms.
Operational guidance
- Store the private PEM in a secret manager (Vault, Infisical, GitHub Actions secrets) rather than on disk in CI runners.
- Treat
keyIdas public — it leaks nothing about the private key, but anchors verification to a specific key version. - Rotate by generating a new key pair, publishing the new
keyId, and re-signing future releases. Consumers should accept both the old and newkeyIdduring the overlap window. - A
keyIdmismatch at verification time means either the wrong public key is configured, or the signer rotated without updating the registry.
hpm audit
hpm audit
Audit runs against the current project and reports:
| Check | Emits |
|---|---|
HTTP URLs in [dependencies] (only the url = … form) | WARN per offending dependency. |
hpm.lock presence | PASS or WARN (No lock file found). |
hpm.lock staleness | PASS (recent) or WARN (Lock file is N days old) when N > 90. |
| Package checksum verification | PASS or WARN (Checksum verification failed: ...). |
Every offender is a warning, not an error — hpm audit never fails the
shell. Wire it into pre-release checklists and CI as an advisory step.
Example output:
HPM Security Audit
========================================
PASS All URLs use HTTPS
PASS Lock file exists (hpm.lock)
PASS Lock file is recent
PASS Package checksums verified
No security issues found.
Transport security
- HPM uses rustls for all HTTPS, so TLS comes from a pure-Rust stack — no OpenSSL, no system-trust-store drift between platforms.
- Registry URLs can be
http://orhttps://;hpm auditwarns about HTTP URLs in theurl = …form of dependencies. If your registry itself is reachable only over HTTP, treat that as a misconfiguration — plain-HTTP registries leak package names and contents, and are trivially tamperable in transit. - Downloads from registries stream through reqwest with the checksum path ending at the SHA-256 verification in
hpm install. MITM tampering doesn’t survive checksum verification.
Storage layout
HPM stores everything under ~/.hpm/ on every supported platform:
| Path | Contents |
|---|---|
~/.hpm/config.toml | Global configuration (including [[registries]], [signing].key_path). |
~/.hpm/packages/ | Canonical CAS, keyed by <slug>@<version>/. Path-installed packages live under the _dev/ subtree, never substituted for a registry hit. |
~/.hpm/fetch/ | ArchiveFetcher staging. Archives are extracted here before being copied into the canonical CAS. |
~/.hpm/venvs/ | Content-addressable Python venvs, keyed by resolved-set hash. |
~/.hpm/cache/ | Download archive cache. |
~/.hpm/registry/ | Per-registry index caches. |
~/.hpm/uv-cache/ | Isolated uv cache — never shared with system uv. |
Per-project:
| Path | Contents |
|---|---|
<project>/hpm.toml | Manifest. |
<project>/hpm.lock | Pinned versions + checksums. Commit this. |
<project>/.hpm/packages/{name}.json | Per-dependency Houdini manifest. Auto-generated. |
<project>/.hpm/config.toml | Optional project-level configuration override. |
The defaults live under $HOME on every platform. To relocate them (e.g., a
shared cache on a fast SSD), override [storage].home_dir (or individual
subdirectories) in ~/.hpm/config.toml. Pick a path that only your user
account can write — HPM does not attempt to sandbox itself against local
privilege escalation on a shared machine.
Best practices
Use HTTPS registries
# Good
[[registries]]
name = "studio"
url = "https://packages.studio.com/v1/registry"
type = "api"
# Bad — plaintext leaks names and lets attackers swap archives in flight
[[registries]]
name = "studio"
url = "http://packages.studio.com/v1/registry"
type = "api"
Commit hpm.lock
Treat hpm.lock like Cargo.lock or package-lock.json — check it in,
review diffs, and merge conflicts carefully.
Use --frozen-lockfile in CI
- name: Install HPM dependencies
run: hpm install --frozen-lockfile
Fails fast if the lock is missing or stale. Catches unintended resolver drift before it reaches production.
Run hpm audit in pre-release checks
Cheap, fast, and flags the easy mistakes (HTTP deps, missing or stale lock, checksum drift). Add it to your release workflow.
Review new dependencies
Before adding a dependency:
- Verify the package is from the creator you expect.
- Check the repository’s recent activity — is it maintained?
- Skim the manifest for scripts, native binaries, and
[runtime]entries that do more than you want.
HPM gives you reproducible, verifiable installs of whatever you asked for. It cannot tell you whether what you asked for is trustworthy.
Rotate signing keys periodically
Ed25519 keys don’t expire, but a key that leaked a year ago is still a leak.
Rotate at studio-appropriate cadence and publish the new keyId.
Threat model
Mitigated
| Threat | Attack vector | Mitigation |
|---|---|---|
| Cache tampering | Attacker modifies a package under ~/.hpm/packages/. | SHA-256 verification against hpm.lock before use. |
| Man-in-the-middle | Attacker intercepts a download. | TLS + checksum verification. |
| Lockfile poisoning | Attacker rewrites hpm.lock checksums. | Detected at install when cached bytes mismatch. Review lockfile diffs in code review. |
| Dependency drift | Same project produces different installs over time. | Exact version + checksum pinning; --frozen-lockfile. |
| Stale dependencies | Old versions with known CVEs. | 90-day staleness warnings; hpm update surfaces newer versions. |
| Replay of a vulnerable version | Registry serves an older artifact than expected. | Version is pinned in the lockfile; the registry cannot “silently” downgrade. |
| Unknown signer | Unknown party uploads an archive claiming to be from a creator. | hpm pack --key + Ed25519 signature + keyId. Consumers verify against the publisher’s known keyId. |
Not addressed
HPM does not protect you against:
- Malicious code in a legitimate package. If the author intends to do harm, HPM will faithfully distribute that harm.
- Compromised upstream sources. If the registry or Git server itself is compromised, HPM trusts what it serves (until a signature check fails, if signatures are in use).
- Zero-day vulnerabilities in dependencies. Use your organization’s security scanning.
- Supply-chain attacks at the package author. Sign your own releases, encourage consumers to verify, and rotate on suspicion.
- Local privilege escalation on shared machines.
~/.hpm/lives in your home directory; anyone with write access to that directory can tamper. Use per-user home directories.
For these threats, layer defenses: code review, dependency scanning
(cargo-audit for Rust deps, pip-audit/similar for Python deps), and
signature verification at your registry.
Reporting vulnerabilities
Do not open a public issue for security problems. Instead:
- Open a private security advisory on GitHub.
- Include a reproducer, affected versions, and the impact you observed.
- Allow a reasonable window for a fix before public disclosure.
See also: Security changelog.
Security changelog
| Version | Change |
|---|---|
| 0.7.0 | Houdini version mapping no longer silently falls back to Python 3.9. Unsupported Houdini majors are now install-time errors, preventing silent ABI mismatches. |
| 0.6.0 | Package signing wire format: PKCS#8 PEM keys, Ed25519 signatures, standard base64 encoding, keyId = first 8 bytes of public key hex. Breaking change from the earlier 32-byte-seed format — regenerate keys with openssl genpkey. HPM_SIGNING_KEY accepts inline PEM. |
| 0.5.2 | Generated per-dependency Houdini hpath points at the package root, so Houdini auto-discovers convention subdirs instead of loading only HDAs. |
| 0.3.0 | Per-platform native packages via [native] + hpm pack --platform. |
| 0.1.0 | SHA-256 checksum verification, HTTPS warnings, --frozen-lockfile, hpm audit, lock file staleness detection, project-level env overrides. |