* add `lgx signature` — dump raw manifest.sig bytes
Symmetric to `lgx manifest --json`: a machine-readable extractor for
the package's signature blob. Until now the only way to get the raw
manifest.sig out of an .lgx was to extract via `tar -O -xzf` — fine in
CI (release.yml does it) but awkward elsewhere.
Out-of-CI tooling that reproduces what `logos-modules-release-action`
records under `sidecar.json#signature` (e.g. a local index.json builder
for a non-GitHub-hosted catalog) needs the raw signature alongside the
manifest. With `lgx manifest --json` + this new `lgx signature`, both
halves of a signed package's envelope are available without shelling
out to `tar` or knowing the archive layout.
Contract:
- Signed package: stdout = manifest.sig bytes verbatim
(byte-identical to the file inside the .lgx), exit 0.
- Unsigned package: stdout empty, exit 0.
- Bad / missing package: stderr message, exit 1.
Callers distinguish "no signature" from "error" via the exit status,
not the stream length — matches the existing convention for
unsigned-package handling elsewhere in the codebase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* address PR #23 review
* Distinguish malformed package from unsigned. `Package::load` accepts
any valid-tar-gzip .lgx, including pathological ones with no
`manifest.json` at all — in which case the previous code exited 0
with empty stdout, indistinguishable from a genuinely unsigned
package. Now: missing manifest.json → exit 1 with a message, so the
"signed-vs-unsigned-vs-error" contract holds for malformed inputs
too.
* Set stdout to binary on Windows before writing the signature bytes
so `\n` doesn't get translated to `\r\n`. POSIX stdout is already
binary; the `#ifdef _WIN32` guard is the whole Windows-only
difference. Without this, the "byte-identical to the file inside
the .lgx" contract silently broke on Windows.
* Add CLI tests in tests/test_cli.cpp covering the three branches of
the documented contract: unsigned → empty + exit 0; signed →
manifest.sig JSON (with `did` + `signature` fields) + exit 0;
missing path → message + non-zero exit. Test infra reuses the
existing CLITest fixture / runLgx helper.
* Clarify the README example that pipes `lgx signature` into `jq` —
it's a signed-package-only pipeline, and the unsigned case would
make jq error on the empty stream. Gate the example accordingly.
Verified locally: `ws test logos-package` PASSes; a hand-crafted
tar+gzip with no manifest.json now produces the explicit
"malformed .lgx — not the same as an unsigned package" error and
exits 1, where previously it silently returned exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add DID-based identity for package signing with comprehensive tests
- Add did:jwk identity layer (base64url, publicKeyToDid, didToPublicKey)
- Replace raw public keys with DID strings in manifest.sig
- Add signer metadata (name, url) and linkedDids placeholder
- JWK private key format (.jwk) replaces PEM (.secret)
- JSON-based keyring with DID lookup
- Make content hashes mandatory (recomputed on every content change)
- Add keygen, keyring, sign commands with DID support
- Add C API (lgx.h) with DID-based signature types
- Add comprehensive test coverage for crypto, keyring, signing, hashes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: replace hashes_valid with package_valid in SignatureInfo
verifySignature() now validates the package (structure + hashes) first
via validatePackage(), removing duplicate hash verification. The
hashes_valid field is replaced by package_valid which reflects the
full package validation result.
- Extract validatePackage() from verify() as non-static instance method
- verify() now delegates to load() + validatePackage()
- verifySignature() calls validatePackage() before checking signature
- Remove duplicate Merkle tree verification from verifySignature()
- Update C API, tests, and downstream consumers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: signPackage validates instead of recomputing hashes
signPackage() now calls validatePackage() to ensure the package is
valid (structure + hashes) before signing. It no longer recomputes
hashes — hashes are already kept up to date by addVariant/removeVariant.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add lgx_keyring_list C API for listing trusted keys
Adds lgx_keyring_list() and lgx_free_keyring_list() to the C API,
enabling downstream consumers to enumerate trusted keys in the keyring.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add custom directory options to keygen, keyring, sign, and verify CLI commands
- keygen: --output-dir / -o to specify key output directory
- keyring: --dir / -d to specify keyring directory
- sign: --keys-dir / -d to specify keys directory
- verify: --keyring-dir to specify keyring directory for trust lookup
All default to the standard ~/.config/logos/ paths when not specified.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update spec, project, and README for directory options and TOFU removal
- Add --output-dir, --keys-dir, --dir, --keyring-dir options to CLI docs
- Add signing/keyring C API functions to project.md
- Add test_crypto.cpp and libsodium to project.md
- Remove --tofu from install-time verification docs
- Update lifecycle example with signing and trust management steps
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review feedback — security, robustness, and clarity
Security:
- Validate key names in keyring to prevent path traversal attacks
- Add missing <cstdlib> include for std::getenv
Robustness:
- Detect malformed manifest.sig (report as invalid, not unsigned)
- Fail validation when crypto::init() fails instead of skipping hashes
- Check init() return value in sign() and verify()
- Handle zero-length input in base64 encode functions
- Check ensureDirectory() path is actually a directory
Clarity:
- Label signer name/URL as self-asserted in verify output
- Update manifest.cpp comment: hashes for integrity, not just signing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make recomputeHashes return Result to fail loudly on crypto errors
Previously recomputeHashes() silently returned without setting hashes
when crypto::init() failed. Now addVariant/removeVariant propagate
the error so packages are never saved without hashes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>