Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Security Guide

This guide covers HPM’s security features, the threat model, package signing, and operational best practices.

Table of contents

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

PropertyValue
Signature algorithmEd25519 (RFC 8032).
Private key formatPKCS#8 PEM (-----BEGIN PRIVATE KEY-----). Generated by openssl genpkey -algorithm ed25519.
Public key formatSubjectPublicKeyInfo PEM (-----BEGIN PUBLIC KEY-----). Extracted with openssl pkey -pubout.
Signature encodingStandard base64 (RFC 4648, alphabet A–Z a–z 0–9 + /, = padding). Emitted as fileSignature. Verifiers must use the standard alphabet, not base64url.
Key identifierFirst 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 payloadThe 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 keyId as 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 new keyId during the overlap window.
  • A keyId mismatch 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:

CheckEmits
HTTP URLs in [dependencies] (only the url = … form)WARN per offending dependency.
hpm.lock presencePASS or WARN (No lock file found).
hpm.lock stalenessPASS (recent) or WARN (Lock file is N days old) when N > 90.
Package checksum verificationPASS 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:// or https://; hpm audit warns about HTTP URLs in the url = … 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:

PathContents
~/.hpm/config.tomlGlobal 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:

PathContents
<project>/hpm.tomlManifest.
<project>/hpm.lockPinned versions + checksums. Commit this.
<project>/.hpm/packages/{name}.jsonPer-dependency Houdini manifest. Auto-generated.
<project>/.hpm/config.tomlOptional 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

ThreatAttack vectorMitigation
Cache tamperingAttacker modifies a package under ~/.hpm/packages/.SHA-256 verification against hpm.lock before use.
Man-in-the-middleAttacker intercepts a download.TLS + checksum verification.
Lockfile poisoningAttacker rewrites hpm.lock checksums.Detected at install when cached bytes mismatch. Review lockfile diffs in code review.
Dependency driftSame project produces different installs over time.Exact version + checksum pinning; --frozen-lockfile.
Stale dependenciesOld versions with known CVEs.90-day staleness warnings; hpm update surfaces newer versions.
Replay of a vulnerable versionRegistry serves an older artifact than expected.Version is pinned in the lockfile; the registry cannot “silently” downgrade.
Unknown signerUnknown 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:

  1. Open a private security advisory on GitHub.
  2. Include a reproducer, affected versions, and the impact you observed.
  3. Allow a reasonable window for a fix before public disclosure.

See also: Security changelog.

Security changelog

VersionChange
0.7.0Houdini 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.0Package 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.2Generated per-dependency Houdini hpath points at the package root, so Houdini auto-discovers convention subdirs instead of loading only HDAs.
0.3.0Per-platform native packages via [native] + hpm pack --platform.
0.1.0SHA-256 checksum verification, HTTPS warnings, --frozen-lockfile, hpm audit, lock file staleness detection, project-level env overrides.