fix(security): remove private key from log output#52
Conversation
GA-006: Replace logger.warning() that included full private JWK with a stderr hint directing users to 'capiscio identity export' or the identity directory. Key material is never logged.
|
✅ Documentation validation passed!
|
|
✅ All checks passed! Ready for review. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests. |
There was a problem hiding this comment.
Pull request overview
Removes private key material from CapiscIO.connect()’s user-facing “capture hint” output to prevent leaking private JWKs (including d) via logging sinks.
Changes:
- Replaces the previous logger-based hint that included the full private JWK with a stderr hint that only shows
kidand guidance for exporting/copying the key. - Adds an explicit SECURITY note in the hint helper to discourage logging key material.
| @@ -66,11 +72,18 @@ def _log_agent_key_capture_hint(agent_id: str, private_jwk: dict) -> None: | |||
| " serverless, CI) the identity will be lost on restart unless\n" | |||
| " you persist the private key.\n" | |||
| "\n" | |||
| " Add to your secrets manager / .env:\n" | |||
| f" Key ID: {kid}\n" | |||
| "\n" | |||
| " To export the private key for your secrets manager:\n" | |||
| "\n" | |||
| " capiscio identity export --format env\n" | |||
| "\n" | |||
| " Or copy the key file from:\n" | |||
| "\n" | |||
| " CAPISCIO_AGENT_PRIVATE_KEY_JWK='" + compact_json + "'\n" | |||
| " ~/.capiscio/identities/<agent>/private.jwk\n" | |||
| "\n" | |||
| " The DID will be recovered automatically from the JWK on startup.\n" | |||
| " The DID will be recovered automatically from the JWK on startup.\n", | |||
| file=sys.stderr, | |||
| ) | |||
There was a problem hiding this comment.
Security regression prevention: there’s no unit test asserting that the capture hint output never includes private key material (e.g., the JWK d value) and doesn’t emit the full JWK. Since this is a P0 leakage fix, consider adding a test that captures stderr/logging and verifies only non-sensitive identifiers (e.g., kid) appear.
| " Or copy the key file from:\n" | ||
| "\n" | ||
| " CAPISCIO_AGENT_PRIVATE_KEY_JWK='" + compact_json + "'\n" | ||
| " ~/.capiscio/identities/<agent>/private.jwk\n" | ||
| "\n" |
There was a problem hiding this comment.
The hint message points users to ~/.capiscio/identities/<agent>/private.jwk, but this module’s default storage location is DEFAULT_KEYS_DIR / agent_id (i.e., ~/.capiscio/keys/<agent_id>/private.jwk) or a user-provided keys_dir. This will send users to a non-existent/wrong path—consider printing the actual on-disk path (or at least the correct default path) and using the existing agent_id parameter to fill in the directory name.
| @@ -66,11 +72,18 @@ def _log_agent_key_capture_hint(agent_id: str, private_jwk: dict) -> None: | |||
| " serverless, CI) the identity will be lost on restart unless\n" | |||
| " you persist the private key.\n" | |||
| "\n" | |||
| " Add to your secrets manager / .env:\n" | |||
| f" Key ID: {kid}\n" | |||
| "\n" | |||
| " To export the private key for your secrets manager:\n" | |||
| "\n" | |||
| " capiscio identity export --format env\n" | |||
| "\n" | |||
| " Or copy the key file from:\n" | |||
| "\n" | |||
| " CAPISCIO_AGENT_PRIVATE_KEY_JWK='" + compact_json + "'\n" | |||
| " ~/.capiscio/identities/<agent>/private.jwk\n" | |||
| "\n" | |||
| " The DID will be recovered automatically from the JWK on startup.\n" | |||
| " The DID will be recovered automatically from the JWK on startup.\n", | |||
| file=sys.stderr, | |||
| ) | |||
There was a problem hiding this comment.
This helper now prints unconditionally to sys.stderr during CapiscIO.connect() identity initialization. Unlike the rest of this module (which uses logger.*), this output can’t be suppressed/redirected via logging configuration and may pollute stderr in library use (e.g., ASGI apps, tests, serverless logs). Consider switching back to logger.warning/info with sanitized content (no key material) or gating the hint behind an explicit opt-in/verbosity flag.
| def _log_agent_key_capture_hint(agent_id: str, private_jwk: dict) -> None: | ||
| """Log a one-time hint telling the user how to persist key material.""" | ||
| compact_json = json.dumps(private_jwk, separators=(",", ":")) | ||
| logger.warning( | ||
| """Log a one-time hint telling the user how to persist key material. | ||
|
|
||
| SECURITY: Never log private key material. Instead, direct users to | ||
| export the key using the CLI or check the identity directory. | ||
| """ | ||
| # Derive the key ID (kid) from the public portion for identification only | ||
| kid = private_jwk.get("kid", "unknown") | ||
| print( |
There was a problem hiding this comment.
Naming/docs mismatch: _log_agent_key_capture_hint (and its docstring) still says it "Log[s]" a hint, but the implementation uses print(). Also the inline comment says "Derive" the kid from the public portion, but the code just reads private_jwk.get("kid"). Consider renaming/updating the docstring/comment to reflect what the function actually does, and/or explicitly label kid as the DID in this SDK (since elsewhere kid is treated as the DID).
| # Derive the key ID (kid) from the public portion for identification only | ||
| kid = private_jwk.get("kid", "unknown") | ||
| print( |
There was a problem hiding this comment.
The inline comment says the kid is derived “from the public portion”, but the code reads it directly from private_jwk. This is functionally fine, but the comment is inaccurate; either derive kid from _public_jwk_from_private(private_jwk) or reword the comment to match the implementation.
- Rename _log_agent_key_capture_hint → _print_agent_key_capture_hint
to match actual behavior (print to stderr, not logger)
- Fix key path in hint: ~/.capiscio/keys/{agent_id}/private.jwk
(was incorrectly showing ~/.capiscio/identities/<agent>/...)
- Add TestPrintAgentKeyCaptureHint regression tests:
- Verify no private key material (d, x) in output
- Verify correct key file path in output
|
✅ Documentation validation passed!
|
|
✅ All checks passed! Ready for review. |
|
✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests. |
GA-006: Remove private key from logger
Priority: P0 — private key leakage
Problem
_log_agent_key_capture_hint()inconnect.pywas logging the full private JWK (including thedparameter) vialogger.warning(). If logging is configured to write to a file, stdout capture, or any log aggregation service, the agent's private key would be exposed.Fix
logger.warning()call that includedjson.dumps(private_jwk)with aprint()to stderrcapiscio identity export --format env(CLI)~/.capiscio/identities/<agent>/private.jwk(file path)Verification
grep -rn "compact_json\|private.*log\|jwk.*warning" capiscio_sdk/returns no source code matches (only proto binary/cache files)