Implement hierarchical recipient policies (#5)
This commit is contained in:
57
docs/recipient-policies.md
Normal file
57
docs/recipient-policies.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Recipient policies and selective reencryption
|
||||
|
||||
`RecipientPolicyManager` implements `pass init` inside `crates/storage`. It
|
||||
accepts the storage-owned `InitRequest`, so root initialization and `--path` /
|
||||
`-p` initialization share the same validated relative-path rules as every
|
||||
other repository operation. One empty identity removes the policy at that
|
||||
exact directory; any other identity list is written one item per line with a
|
||||
final newline, matching upstream `pass`.
|
||||
|
||||
## Hierarchy
|
||||
|
||||
An entry inherits the nearest `.gpg-id` in its directory or an ancestor.
|
||||
Nested files override their parent for the complete nested subtree. Exact
|
||||
fingerprints, key IDs, and user IDs are resolved by `KeyStore`; comments,
|
||||
whitespace, duplicates, missing identities, and ambiguous identities use the
|
||||
same rules as encryption elsewhere in storage.
|
||||
|
||||
Changing a policy considers only entries below that directory which are not
|
||||
shielded by a deeper override. Removing an override makes those entries inherit
|
||||
the nearest policy above it. Removal is rejected before mutation if an affected
|
||||
entry would have no policy. Existing public-key session packets are compared to
|
||||
the resolved certificate set, so entries already encrypted for exactly the
|
||||
requested recipients are not needlessly rewritten.
|
||||
|
||||
As upstream does, removing `.gpg-id` does not remove a pre-existing detached
|
||||
`.gpg-id.sig`; without a configured signing-key policy the orphan is ignored.
|
||||
Reinitializing with signing enabled replaces it.
|
||||
|
||||
## Authentication
|
||||
|
||||
When `PASSWORD_STORE_SIGNING_KEY` behavior is configured, `SigningPolicy`
|
||||
contains the key used to create a detached signature and the explicit set of
|
||||
trusted primary fingerprints. A new `.gpg-id` is signed and immediately
|
||||
verified against that set before filesystem mutation. Inherited policies must
|
||||
have a signature which verifies against the same trust set. Missing, malformed,
|
||||
untrusted, and cryptographically invalid signatures are distinct pre-mutation
|
||||
failures.
|
||||
|
||||
## Transaction and Git boundary
|
||||
|
||||
Resolution, signature work, decryption, and replacement encryption all finish
|
||||
in memory before the first repository write. Every affected entry retains its
|
||||
original encrypted bytes. Repository writes use private, synced temporary files
|
||||
and atomic per-file replacement. If any write or the storage commit hook fails,
|
||||
the policy, signature, and every entry are restored in reverse order; new empty
|
||||
policy directories are removed. A rollback failure is surfaced separately from
|
||||
the original operation error.
|
||||
|
||||
`PolicyCommitter` is the integration boundary for the embedded Git engine. It
|
||||
receives the exact changed paths and compatible commit intent after repository
|
||||
bytes have been installed. It is called once for a real change, never for a
|
||||
no-op, and must restore any staging state before returning an error. The policy
|
||||
manager then restores repository bytes. `NoGitCommitter` represents a path not
|
||||
contained in a Git work tree; it is not used for a discovered repository.
|
||||
|
||||
The later embedded-Git implementation owns concrete staging and commits, while
|
||||
this module owns the all-or-nothing storage mutation contract it invokes.
|
||||
Reference in New Issue
Block a user