feat: add age provider#174
Conversation
Signed-off-by: Anish Pallati <i@anish.land> Assisted-by: Claude Opus 4.8
Signed-off-by: Anish Pallati <i@anish.land> Assisted-by: Claude Opus 4.8
domenkozar
left a comment
There was a problem hiding this comment.
Review findings are attached inline: one P1 correctness issue and six P2 compatibility, auditability, and documentation issues.
| /// Parses the configured identity file from credential, env, or path | ||
| fn identity_file(&self) -> Result<IdentityFile<NoCallbacks>> { | ||
| if let Some(material) = credential_or_env(&self.credentials, IDENTITY, AGE_IDENTITY_ENV) { | ||
| return IdentityFile::from_buffer(std::io::Cursor::new(material.into_bytes())) |
There was a problem hiding this comment.
P1 — Parse SSH private keys before age identity files
When ?recipients-file= contains the documented ssh-ed25519 or ssh-rsa recipient and the matching OpenSSH private key is supplied through identity, this path rejects it because age::IdentityFile parses only X25519 and plugin identities. A team-mode write can therefore succeed, but subsequent SecretSpec reads using that SSH recipient fail. Detect age::ssh::Identity first and include it among the decryption identities.
| if let Ok(r) = s.parse::<age::ssh::Recipient>() { | ||
| return Ok(Box::new(r)); | ||
| } | ||
| if let Ok(r) = s.parse::<age::plugin::Recipient>() { |
There was a problem hiding this comment.
P2 — Parse native tagged recipients before plugin recipients
When a roster contains a valid age 0.12 native age1tag... or age1tagpq... recipient, this fallback misclassifies it as a plugin recipient and requires age-plugin-tag or age-plugin-tagpq. The age crate exposes native tag::Recipient and tagpq::Recipient, and its recipients-file parser accepts them, so parse those before the plugin fallback.
| } | ||
|
|
||
| fn uri(&self) -> String { | ||
| format!("age:{}", self.config.path.display()) |
There was a problem hiding this comment.
P2 — Preserve non-secret age options in the reported URI
Provider::uri() feeds audit logs, fallback warnings, and resolution reports and is documented to preserve non-secret configuration, but this drops recipients-file and armor=false. A write that changes the reader roster is therefore recorded identically to a default write and cannot be reproduced from the report. Append the encoded non-secret query options while continuing to exclude identity material.
| ## Quick start | ||
|
|
||
| ```bash | ||
| $ secretspec set DATABASE_URL --provider "age://secrets.age?identity=~/.config/age/keys.txt" |
There was a problem hiding this comment.
P2 — Replace the literal tilde in the quick start
Because the URI is quoted, the shell does not expand ~, and the provider instead rebases it to <project>/~/.config/age/keys.txt. Copying either quick-start command therefore fails even when the key exists under the user's home directory. Use $HOME or an absolute path and update the repeated URI example as well.
| uuid.workspace = true | ||
| data-encoding.workspace = true | ||
| detect-coding-agent.workspace = true | ||
| age = { workspace = true, optional = true } |
There was a problem hiding this comment.
P2 — Add the age provider to the unreleased changelog
This Rust feature adds a default provider but leaves /CHANGELOG.md unchanged. The repository instructions require a user-facing entry under the existing Unreleased section for every Rust change; otherwise the 0.17 release notes will omit the new provider.
| { icon: 'googlecloud', slug: 'gcsm', label: 'Google Cloud Secret Manager' }, | ||
| { icon: 'microsoftazure', slug: 'akv', label: 'Azure Key Vault' }, | ||
| { slug: 'infisical', label: 'Infisical' }, | ||
| { slug: 'age', label: 'age' }, |
There was a problem hiding this comment.
P2 — Mark age as upcoming across the landing page
The documentation publishes from main while the released CLI is 0.16, but this marquee renders plain age without the required (0.17+), making an unavailable provider look current. The new-provider checklist also requires adding age to the landing-page secretspec config init snippets, which remain unchanged. Label this entry and add age ... (0.17+) there too.
|
|
||
| ### Plugins and post-quantum keys | ||
|
|
||
| Identity and recipient parsing both go through age's plugin protocol, so keys handled by an `age-plugin-*` binary work when that binary is on `PATH`. |
There was a problem hiding this comment.
P2 — Qualify support for callback-dependent age plugins
This statement is false for plugins that issue confirm, request-public, or request-secret: recipient and identity plugins are constructed with NoCallbacks, which returns None and suppresses messages. Hardware-, PIN-, or passphrase-dependent plugins can therefore fail despite being on PATH. Either provide callbacks or document that only non-interactive plugins are supported.
|
Implemented all seven findings from the inline review in commit Highlights:
Validation: pre-commit Clippy and rustfmt passed; |
Closes #4
KEY=valueentry, and project and profile do not namespace it, the same as the dotenv provider?recipients-file=is given, and to the committed roster file when one isidentitycredential, thenAGE_IDENTITY, then a?identity=fileIdentityFileandRecipientPluginV1withNoCallbacks), so everyage-plugin-*works automaticallyI verified the post-quantum path on my personal system since it needs
age-plugin-pqonPATH, the remaining behavior is covered in the unit tests