85.1.44. pepsi-keys¶
manage the end-to-end key store: identities, peer keys and anchors
- Manual section:
1
85.1.44.1.1. Name¶
pepsi-keys - generate, import, publish and retire end-to-end key material.
85.1.44.1.2. Synopsis¶
pepsi-keys [GLOBAL-OPTIONS] identity list [–address ADDRESS] [–json]
pepsi-keys [GLOBAL-OPTIONS] identity show IDENTITY-ID [–json]
pepsi-keys [GLOBAL-OPTIONS] identity generate ADDRESS –protocol PROTOCOL [–name NAME] [–days N] [–no-publish] [–csr] [–shared | –split]
pepsi-keys [GLOBAL-OPTIONS] identity import ADDRESS –protocol PROTOCOL –purpose PURPOSE –public FILE [–private FILE] [–algorithm NAME] [–primary] [–no-publish]
pepsi-keys [GLOBAL-OPTIONS] identity export IDENTITY-ID [–private] [–pem]
pepsi-keys [GLOBAL-OPTIONS] identity csr IDENTITY-ID
pepsi-keys [GLOBAL-OPTIONS] identity revoke IDENTITY-ID [–reason TEXT] [–upload]
pepsi-keys [GLOBAL-OPTIONS] identity forget-private IDENTITY-ID [-y]
pepsi-keys [GLOBAL-OPTIONS] identity set-primary IDENTITY-ID
pepsi-keys [GLOBAL-OPTIONS] identity publish [IDENTITY-ID] [–wkd] [–vks] [–retry [–dry-run]] [-y]
pepsi-keys [GLOBAL-OPTIONS] identity unpublish IDENTITY-ID
pepsi-keys [GLOBAL-OPTIONS] identity delete IDENTITY-ID [-y]
pepsi-keys [GLOBAL-OPTIONS] identity retire
pepsi-keys [GLOBAL-OPTIONS] peer list [–address ADDRESS] [–json]
pepsi-keys [GLOBAL-OPTIONS] peer show ADDRESS [–protocol PROTOCOL] [–json]
pepsi-keys [GLOBAL-OPTIONS] peer unclaimed [–address ADDRESS] [–all-sources] [–json]
pepsi-keys [GLOBAL-OPTIONS] peer import ADDRESS –protocol PROTOCOL –file FILE [–pin]
pepsi-keys [GLOBAL-OPTIONS] peer pin | unpin | remove PEER-KEY-ID
pepsi-keys [GLOBAL-OPTIONS] peer refresh [–address ADDRESS | –expire PEER-KEY-ID] [–expiring-within-days DAYS] [–limit N]
pepsi-keys [GLOBAL-OPTIONS] peer prune [–retain-days DAYS] [–dry-run]
pepsi-keys [GLOBAL-OPTIONS] ca list [–json]
pepsi-keys [GLOBAL-OPTIONS] ca add FILE [–label TEXT]
pepsi-keys [GLOBAL-OPTIONS] ca enable | disable | remove CA-ID
pepsi-keys [GLOBAL-OPTIONS] identity –otp CODE COMMAND …
pepsi-keys [GLOBAL-OPTIONS] otp status [ADDRESS] [–json]
pepsi-keys [GLOBAL-OPTIONS] otp enroll | remove ADDRESS [–otp CODE]
pepsi-keys [GLOBAL-OPTIONS] otp reset ADDRESS
pepsi-keys [GLOBAL-OPTIONS] wrap rotate [–dry-run]
pepsi-keys [GLOBAL-OPTIONS] dns ADDRESS
85.1.44.1.3. Description¶
pepsi-keys is the operator’s tool for Pepsi’s end-to-end cryptography key store — the three tables that hold every key OpenPGP and S/MIME processing needs:
pepsi.crypto_identityThe key pairs of the addresses we serve, private half included and wrapped at rest (
custody = local, an MTA key). Used to sign outbound mail and to decrypt inbound mail. It also holds the users’ own keys, whose private half lives only in their mail client (custody = client, an MUA key; see MTA keys and MUA keys).pepsi.peer_keyRemote correspondents’ public keys and certificates, however they were obtained, each with the source it came from, a validity verdict and a cache deadline. Used to encrypt outbound mail and to verify inbound signatures.
pepsi.ca_trustThe CA certificates an inbound S/MIME chain must reach for its signature to count as trusted.
The tool is not a stage. It manages rows directly, in the shape of
pepsi-whitelist(1) and pepsi-settings(1), connecting to the same
database as the other components through the shared [pepsi-postgres]
section and reading the [pepsi] CRYPTO_* options and the
[pepsi-crypto] section described in pepsi.conf(5).
It is a standalone binary rather than part of the unified pepsi executable
for two reasons: it is the only program in the tree that generates key
material, and a site may install it setuid (see Privileges).
85.1.44.1.3.1. Capability, not shape¶
An address’s material is stored as one row per capability, never as “the
identity”. Each row’s purpose is sign, encrypt or both:
an OpenPGP identity is one
bothrow — a transferable key whose sign-capable primary carries an encryption subkey, so the split is intrinsic and costs nothing;an S/MIME identity is normally two rows, one certificate with
digitalSignatureand one withkeyEncipherment/keyAgreement;with
[pepsi] CRYPTO_SMIME_SHARED_KEY = yesit is instead onebothcertificate carrying both key usages (RSA only).
That option decides only what newly created identities look like. Both
shapes are supported permanently and may coexist for one address — an old shared
certificate still valid alongside a freshly issued split pair — so every lookup
matches a capability (sign or both; encrypt or both) rather than
counting rows. Listings and identity show print the purpose for exactly
this reason.
85.1.44.1.3.2. Retirement is asymmetric¶
Pepsi delivers plaintext to the mailbox, so a private decryption key is needed only for mail still in flight and for whatever ciphertext an operator has archived — never to read one’s own mailbox. A private signing key, once retired, can do nothing legitimate at all: nothing ever re-signs old mail. So at expiry or revocation:
a
signrow’s private half is destroyed, and only the public half is kept so old signatures can still be verified;an
encryptrow’s private half is retained;a
bothrow follows the decryption rule — destroying it would take the ability to decrypt with it — which is the price of shared-certificate mode: revoking a compromised signing key there also ends the ability to receive new encrypted mail.
private_purged_at records when material was dropped, so “deliberately
destroyed” stays distinguishable from “never held”; both identity list and
identity show report it.
85.1.44.1.3.3. MTA keys and MUA keys¶
An address may hold keys of two kinds. An MTA key (custody = local) is
one whose private half Pepsi holds: pepsi-stage-encrypt(1) signs with it,
pepsi-stage-decrypt(1) opens mail with it, and it is advertised in
Autocrypt: headers. It is created by identity generate, by an import with
–private, or automatically on a user’s first submission under [stage-encrypt]
ENABLE_PEP. An MUA key (custody = client) is the user’s own key from
their mail client: public-only for ever, never primary. Pepsi encrypts to it and
passes mail encrypted to it through unopened, but can never sign or decrypt with
it. It is created by an import without –private, by the web console or
API, by the e-mail register command, or automatically from the
Autocrypt: header of the user’s own submitted mail.
The public face of an address – what the Web Key Directory serves, what local mail to the user is encrypted to – is the newest active MUA key when there is one, else the primary active MTA key. The MTA key is not retired when an MUA key appears; it keeps decrypting mail from correspondents who still use it. identity show prints the custody and whether the identity is the public face.
85.1.44.1.3.4. Key-server uploads are requested per identity¶
Each identity records whether its upload to the key server was asked for
(crypto_identity.vks_wanted). Automatically generated keys and automatically
registered MUA keys start with it off: Autocrypt and the deployment’s own Web
Key Directory are how they are found, and an upload cannot be withdrawn. Keys
made by hand (identity generate, identity import, and the web and e-mail
generate) take [pepsi-keys] VKS_PUBLISH as their default, and
–no-publish keeps it off. An explicit request – identity publish –vks,
the web console’s Upload to the key server, the e-mail publish command –
sets it whatever VKS_PUBLISH says. identity publish –retry uploads
exactly the rows where it is set, so “not uploaded” is recorded state, and
identity show reports it.
85.1.44.1.3.5. Ownership¶
An identity is keyed on the lower-cased address. When
pepsi-common’s locality rules ([pepsi-crypto] LOCAL_DOMAINS,
RECIPIENT_DELIMITER and TARGETS, defaulting to [pepsi-ingress]
ACCEPTED_DOMAINS and the /etc/login.defs uid range) resolve that address to
a local account, the passwd login is recorded in crypto_identity.login;
otherwise the column is NULL — a role address, a hosted domain, an
Exchange-fronted deployment.
That column is the access rule. An unprivileged caller may see and change only
identities whose login is their own, resolved from the real uid’s passwd
entry — not $USER, not an argument, not the effective uid. An identity with
no owning account belongs to nobody in particular and is therefore
operator-only: treating “unowned” as “everybody’s” would make every role address
writable by every local user.
root, and the pepsi-crypto, pepsi and pepsi-owner accounts, may
manage every identity. Peer keys and trust anchors have no per-user
namespace — they are somebody else’s key, or deployment-wide policy — so every
peer and ca subcommand, list included, is operator-only, as are
wrap rotate and identity retire.
85.1.44.1.3.6. Privileges¶
Reaching a private key takes two things, and they are guarded separately:
the database role
pepsi-crypto, the only one granted thecrypto_identity.private_wrappedcolumn (apart from the schema ownerpepsi-owner, which reads it by ownership but cannot read the key-encryption key). Every ordinary service role — including thepepsiaccount every ordinary stage worker runs as — holds a column-level grant that omits it, so a compromise elsewhere in the pipeline yields the public half of every identity and nothing more (see pepsi-setup(1));the key-encryption key, which lives only in the filesystem, in
secrets.d/pepsi-crypto.secret.
The database alone therefore yields nothing but ciphertext, and the key-encryption key alone yields nothing at all.
pepsi-keys assumes the pepsi-crypto identity only for the duration of
each database call. Run as root it becomes that account for the call
(root has no database role of its own) and returns afterwards; run as
pepsi-crypto or pepsi-owner it simply connects as itself; run from a
setuid install it raises its effective ids to the binary’s owner. PostgreSQL
peer authentication keys off the effective uid, not the gid, so a setgid bit
would grant the group that guards the secret fragment but not the database
identity — a program that must be this role has to be this account.
As shipped the binary carries no setuid bit (mode 0755): one binary able
to open every private key in a deployment is a far larger prize than
pepsi-whitelist(1)’s single table, so the default is that only the operator
runs it. A site that wants ordinary users to manage their own identities makes it
setuid pepsi-crypto.
Which authority is in force differs between the two installs. The per-identity
ownership rule above applies only when the binary actually carries the setuid
bit: a caller is treated as privileged when it is root, when its login is one
of the privileged accounts, or when the binary is not setuid at all. On the
shipped 0755 install every caller is therefore privileged as far as this
program is concerned, and only PostgreSQL stands between a user and another
user’s identities — the caller authenticates as themselves and the database
grants decide. That is the intended design, but it means granting somebody the
pepsi-crypto database role hands them every identity in the deployment,
whatever this program’s ownership rule would otherwise have said. When it is
setuid, the same three precautions pepsi-whitelist(1) takes apply: the
effective ids are dropped to the invoking user before anything else happens, the
environment variables that steer configuration discovery (HOME,
XDG_CONFIG_HOME), ${DATADIR} expansion (PATH) and libpq (PG*) are
removed, and -c/–config is refused for unprivileged callers — the
configuration names the database and the key-encryption key.
85.1.44.1.4. Commands¶
85.1.44.1.4.1. Identity commands¶
- identity list [–address ADDRESS] [–json]
List identities as a table: id, address, protocol, purpose, status, whether the row is the primary one for its capability, whether it is published, the state of its private half (
yes/no/purged, orclientfor the user’s own MUA key, whose private half Pepsi never held) and its expiry. An unprivileged caller sees only their own. –address restricts the listing to one address; –json emits the rows as JSON instead.- identity show IDENTITY-ID [–json]
Print one identity in full, including its fingerprint, algorithm, custody (
localfor an MTA key,clientfor the user’s own MUA key), whether it is the address’s public face (the key correspondents are given),wrap_key_id, timestamps, any revocation reason, the Web Key Directory local-part hash the address is served under, and its key-server state: one ofverified atTIME,uploaded atTIME, awaiting confirmation,upload requested, ornot uploaded (not requested).- identity generate ADDRESS –protocol
openpgp|smime[OPTIONS] Create key material for ADDRESS and store it, private half wrapped. The new material becomes the primary for its capability; nothing existing is rewritten or replaced, so the previous material stays usable for decrypting what was already sent to it.
OpenPGP produces one
bothrow using[pepsi] CRYPTO_OPENPGP_ALGORITHM(ed25519, the default, orrsaatCRYPTO_GENERATE_RSA_BITS, which must be 2048, 3072 or 4096). S/MIME produces the signing/encryption pair usingCRYPTO_SMIME_ALGORITHM(rsa,p256orp384), each with a self-signed certificate for immediate use.- –name NAME
Display name for the OpenPGP User ID (
Name <address>) or the certificate subject. Without it the User ID is the bare address.- –days N
Lifetime of the generated material. Defaults to
[pepsi-crypto] IDENTITY_VALIDITY_DAYS(730, two years).- –no-publish
Do not serve the public half over the Web Key Directory or upload it. By default a new identity is published — a key nobody can fetch cannot be encrypted to — and its key-server upload is requested when
[pepsi-keys] VKS_PUBLISHis on.
Generation on request is always allowed, even for an address that already has the user’s own MUA key or a revoked key; only automatic creation refuses an address with any identity.
- –csr
Also print a PKCS#10 certificate signing request over each generated key, so a real CA can issue a replacement certificate for the same key without re-keying. S/MIME only; OpenPGP has no such object.
- –shared
Force one S/MIME certificate carrying both key usages, whatever
CRYPTO_SMIME_SHARED_KEYsays. Refused with an elliptic-curveCRYPTO_SMIME_ALGORITHM: one EC key doing both ECDSA and ECDH is cross-algorithm key reuse that several S/MIME clients reject.- –split
Force the separate signing and encryption certificates. Mutually exclusive with –shared; with neither, the configured default applies.
- identity import ADDRESS –protocol P –purpose PURPOSE –public FILE [OPTIONS]
Store material generated elsewhere. PURPOSE is
sign,encryptorbothand must describe what the material can actually do — it is what every later lookup matches on. The public half is an OpenPGP transferable key or an X.509 certificate (DER or PEM, detected);-reads standard input. The fingerprint is derived from the material itself: the OpenPGP fingerprint, or the SHA-256 of the certificate DER.- –private FILE
The private half — an OpenPGP transferable secret key, or a PKCS#8 key — which is wrapped before it is stored, as an MTA key (
custody = local). Without it only the public half is kept. For OpenPGP the row is then recorded as the user’s own MUA key (custody = client), which is never primary (–primary is ignored for it): Pepsi can publish and encrypt to it but never sign or decrypt with it. It pre-empts automatic key creation for the address and becomes the address’s public face; mail encrypted to it is passed to the mail client unopened. A public-only S/MIME import keepscustody = local– typically a CA-issued certificate over a key already in the store (see identity csr).- –algorithm NAME
Algorithm name to record. Informational; defaults to
imported.- –primary
Make this the material used for new messages of its capability.
- –no-publish
Do not publish the public half. Otherwise the key-server upload is requested when
[pepsi-keys] VKS_PUBLISHis on.
- identity export IDENTITY-ID [–private] [–pem]
Write the material to standard output in its own binary form — OpenPGP packets or X.509 DER — so it can be piped straight into another tool.
- –private
Export the private half instead of the public one, unwrapping it first. Fails when the row holds none, naming which of “never held” and “purged” applies.
- –pem
Wrap X.509 material in a PEM block. OpenPGP material has no PEM form and is refused with a pointer to
gpg --enarmor.
- identity csr IDENTITY-ID
Print a PKCS#10 certificate signing request over an identity’s existing private key, with the key usage its
purposeimplies. This is the path that matters once an identity is in service: the self-signed certificate gets the organisation going, and the same key — already published, already used to receive encrypted mail — is later presented to a real CA without re-keying. S/MIME identities only, and only while the private half is still held.- identity revoke IDENTITY-ID [–reason TEXT] [–upload]
Withdraw an identity. The row becomes
revokedand stops being primary; the public half is kept so signatures made before the revocation can still be checked. Asignrow’s private half is destroyed here and now; anencryptorbothrow keeps its private half, and the command says so. TEXT is recorded with the row.–upload additionally builds a revocation certificate — signed by the key it retires, with TEXT as its reason — and publishes it to the configured key server. That is the only remedy for a key already uploaded: a key server can be told a key is revoked, never told to forget it.
The upload happens before the local revocation, because revoking a
signidentity destroys the private key the revocation certificate is signed with. If the upload fails, nothing is revoked, so the key-server problem can be fixed and the command re-run. Revoking without –upload and then wanting a published revocation is not recoverable.- identity forget-private IDENTITY-ID [-y, –yes]
Destroy an identity’s private key permanently, leaving the public half. This is the only way a decryption key leaves the store, and it is irreversible: anything still queued in
pepsi.workqueue, anything in an encrypted archive and any message redelivered later from a backup becomes unreadable. Mail already delivered is unaffected. The command prints exactly that and asks for confirmation; –yes skips the question, and with no terminal and no –yes the answer is no, so an unattended run cannot destroy key material because nobody was there to object.- identity set-primary IDENTITY-ID
Make this the material used for new messages of its (address, protocol, purpose). Only an
activeidentity may be made primary. The previous primary for the same triple is demoted.- identity publish IDENTITY-ID [–wkd] [–vks] [-y, –yes]
Serve the identity’s public half over the Web Key Directory. This is the reversible channel — our own server, our own domain — and it is what –wkd names; it is also the default, so the flag exists only to make the channel explicit. The material stays usable either way.
–vks additionally records the upload request (
vks_wanted), uploads the key to the configured key server and asks it to mail the confirmation link to the address. Because the request is recorded before the attempt, a failed upload is retried by identity publish –retry rather than forgotten. That is irreversible: a key server can later be told the key is revoked, but never told to forget it, and the address becomes a permanent public record. The command prints exactly what cannot be taken back and asks for confirmation; –yes skips the question, and with no terminal and no –yes the answer is no. OpenPGP only — there is no key server for S/MIME, whose publication channel is theSMIMEArecord dns prints.- identity publish –retry [–dry-run]
Work through every published, active OpenPGP identity whose upload was requested (
vks_wanted) and that is not verified on the key server yet, instead of one. This is the form to run from cron (hourly is ample). It is not gated by[pepsi-keys] VKS_PUBLISH: that option only decides whether keys made by hand start out requested, and a user’s own request is honoured whatever it says. An automatically created key is never on the list unless its owner asked.It exists because publication is a two-step protocol: an upload returns a token, only the token authorises the confirmation mail, and only a followed link makes the server serve the key by address. A key uploaded but never confirmed is stored yet unfindable. Each round first asks the key server whether the address is already served — so a confirmation followed by pepsi-stage-vks-confirm(1), by a human, or on an earlier run whose database write did not land all end the loop — and otherwise uploads and requests verification. Identities are spaced by
VKS_RETRY_INTERVAL, at mostVKS_BATCHper run, and abandoned afterVKS_MAX_ATTEMPTSwith the reason left on the row for identity show. –dry-run lists what would be published and changes nothing. Operator-only.- identity unpublish IDENTITY-ID
Stop serving the identity over the Web Key Directory, and forget its key-server bookkeeping so the retry job leaves it alone and a later re-publication starts from a clean slate.
It does not remove anything from a key server, because nothing can. When the identity had been uploaded the command says so and points at identity revoke –upload, which is the only way to tell the world the key is dead.
- identity delete IDENTITY-ID [-y, –yes]
Remove the row outright, public half included. Confirmed like forget-private. Prefer revoke, which keeps the public half so old signatures remain checkable; a deleted identity leaves nothing behind at all.
- identity retire
Run the retirement sweep over every identity: everything past its
expires_atbecomesexpired, private signing material is destroyed and stamped, and decryption material (including a sharedbothrow’s) is left alone. Intended for a periodic timer as much as for the command line; identity revoke applies the same rule inline for one identity. Operator-only.
85.1.44.1.4.2. Peer commands¶
- peer list [–address ADDRESS] [–json]
List cached peer keys: id, address, protocol, source, validity, whether the lookup that produced it was DNSSEC-validated, whether it is pinned, and the fingerprint.
- peer show ADDRESS [–protocol PROTOCOL] [–json]
Show the keys that would actually be used for ADDRESS, after the matching rule: an exact address match always wins, and a
*@domainrow is consulted only when there is no exact one (it is flagged as a domain-wide fallback in the output). PROTOCOL defaults toopenpgp.- peer unclaimed [–address ADDRESS] [–all-sources] [–json]
List the addresses this deployment serves that hold no identity of ours, yet have a harvested or gossiped peer key — the keys a stranger can plant by sending us a message with a forged
From:. Keys are harvested on theFrom:header, which the sender writes, and inbound authentication is fail-open, so on a deployment that does not enforce DMARC on its own domain such a row can name one of our own users. Where we hold an identity the row is inert (our own key pre-empts it); where we do not, it decides what mail to that user is encrypted with. That is deliberate — it is also the only way a user who runs GnuPG in their own client gets their real key here — so this is a worklist, not an alarm: confirm each key with its owner, then import it as an identity (identity import without –private), after which it is inert, or remove it (peer remove).Served means the domain is one of
[pepsi-ingress] ACCEPTED_DOMAINSor[pepsi-crypto] LOCAL_DOMAINS; no passwd account is required. No identity means no active identity able to encrypt, the same rule the encryption stage’s pre-emption applies, so an address whose only identity expired, was revoked or can only sign is listed; theIDScolumn counts its identities in any state. Harvested is sourceinbound(the sender’s ownAutocrypt:header or attached key) orgossip(a third party’sAutocrypt-Gossip:); –all-sources includes discovered, manual and API keys as well, which is the setGET /api/v1/peers/unclaimedreports. A*@domainrow is never listed. Read-only and operator-only. The exit status is 0 whether or not anything was found; with –json an empty report is[], which is what a periodic check should test for.- peer import ADDRESS –protocol P –file FILE [–pin]
Store a correspondent’s key by hand (
-reads standard input). The row is recorded with sourcemanual, which outranks every discovery mechanism, and is the only source permitted to carry a*@domainaddress — a database constraint, not a convention, so no discovery answer for one address can ever be generalised to a whole domain.A key that conflicts with one already stored replaces it only if it outranks everything there; a pinned row is never displaced, and the command says which of stored, refreshed, replaced and kept happened. (The single exception to “only if it outranks” is the Autocrypt tier’s newest-wins rule, which never involves a
manualkey in either direction — see pepsi-keydisc(1).)- –pin
Pin the imported key, so discovery never replaces it.
- peer pin PEER-KEY-ID, peer unpin PEER-KEY-ID
Set or clear the pin on one stored key.
- peer refresh [–address ADDRESS] [–expire PEER-KEY-ID] [–expiring-within-days DAYS] [–limit N]
Re-validate cached peer keys. It only ever refreshes keys that already exist: it never discovers a key for an address that has none. Pepsi looks an address up when it has mail for it, which is why discovery is driven by the pipeline (pepsi-keydisc(1)) rather than by this command.
Three shapes, selected by the flags:
- –address ADDRESS
Run the whole discovery fan-out for one address in this process — every method in
[pepsi-keydiscovery] SOURCESat once, with the rank-grace stop rule — and store what it finds with the right source and DNSSEC flag. One line per method is printed: the protocol and fingerprint (marked(dnssec)for an AD-validated DANE answer),no key, or the error. This also settles the address’s discovery request, so it releases any message parked on it. An address excluded byALLOW_DOMAINS/DENY_DOMAINSis refused rather than queried.- –expire PEER-KEY-ID
Clear one row’s cache deadline so the next message for that address re-fetches. No network I/O at all, and mutually exclusive with –address.
- (neither flag)
Sweep: every key that is due — its
refresh_afterhas passed, or its own expiry falls within –expiring-within-days (default7) — is put through the same fan-out, up to –limit keys (default100). Pinned and hand-entered (manual/api) keys are skipped, and so are*@domainrows: an operator put those there, and no discovery answer outranks that, so fetching would be pure cost. The number re-validated is printed. This is the natural body of a periodic timer.
- peer prune [–retain-days DAYS] [–dry-run]
Delete expired peer keys and aged-out discovery requests — two tables, one job. A peer key that expired more than DAYS (default
7) ago is removed unless it is pinned. The expiry is the one the key itself states — an OpenPGP primary key’s current self-signature, an X.509notAfter— recorded whenever a key is learnt, whether by discovery, harvesting or gossip (a key imported by hand records none, and a key that states none is never pruned); on thepepsi.key_requestside both a settled row whose negative entry has aged out past the same window and a pending row abandoned that long past its deadline have nothing left to say and go too. –dry-run reports how many keys carry an expiry and are unpinned, and removes nothing.- peer remove PEER-KEY-ID
Delete one stored key. This is also how an operator lets through a correspondent’s newer Autocrypt key that
ACCEPT_ROTATION = expiredheld (akey.peer.rotate.heldaudit row; see pepsi-stage-autocrypt-learn(1)): with the old row gone, the next message carrying the new key stores it as a first key.
85.1.44.1.4.3. Trust-anchor commands¶
- ca list [–json]
List the trust anchors with their id, whether they are enabled, their label and their subject.
- ca add FILE [–label TEXT]
Add a CA certificate (DER or PEM;
-reads standard input) as a trust anchor. A certificate withoutbasicConstraints CA:TRUEis refused: no chain could ever end at it. Idempotent on the certificate’s SHA-256 — adding the same anchor twice only updates the label.An anchor is accepted here on its
basicConstraintsalone, but path building applies its constraints like any other CA’s (RFC 5937): a name-constrained anchor vouches only for names inside itsnameConstraints, a CA that saidpathlen:0will not have a sub-CA accepted below it, and arequireExplicitPolicyorinhibitAnyPolicyit carries is honoured. A chain that breaks one fails withpolicy-rejected. So does one through a CA carrying a critical extension Pepsi cannot process at all (RFC 5280 §4.2) — such a CA is never used as an issuer.- ca enable CA-ID, ca disable CA-ID
Hand an anchor to the verifier, or stop doing so while keeping the row for the record.
- ca remove CA-ID
Delete an anchor.
Pepsi ships no default anchor set, deliberately. A signature that chains to a CA nobody chose is reported untrusted rather than bad, so an empty store degrades to “we cannot vouch for this” — and an operator who adds an anchor is making a decision rather than inheriting one.
85.1.44.1.4.4. Second-factor commands¶
An address may have a second factor: a TOTP secret (RFC 6238, SHA-1, six
digits, 30-second steps) its owner keeps in an authenticator app. Once it
exists, an unprivileged caller (a setuid install) must present a current
code for every change to that address’s identities – identity generate,
import, revoke, forget-private, set-primary, publish,
unpublish, delete – and for identity export --private and
identity csr, as the option –otp CODE of the identity group
(pepsi-keys identity --otp 123456 revoke 7). Ownership is checked before the
code, so a user cannot count failures against another user’s enrolment. The
operator (root, pepsi-crypto, a non-setuid install) is never asked. The
same second factor guards the e-mail key commands and the web console; see
pepsi-stage-encrypt(1) and the key-management chapter of the manual.
A code is accepted once (a replay within its own 30 seconds is refused), one step of clock skew either way is tolerated, a wrong or replayed code counts as a failure and a right one resets the count, and ten failures in a row lock the second factor until the operator resets it.
- otp status [ADDRESS] [–json]
Show an address’s enrolment: when it was enrolled and last used, and the consecutive failures (or
LOCKED). The operator may omit ADDRESS to list every enrolment. Never shows a secret.- otp enroll ADDRESS [–otp CODE]
Create a fresh secret for ADDRESS and print it once, as base32 and as an
otpauth://URI (qrencode -t ansiutf8turns that into a QR code to scan). Replaces an existing enrolment, starting again at zero failures; the owner needs a current code of the old one for that, the operator does not – which is how the operator hands a locked-out user a new secret.- otp remove ADDRESS [–otp CODE]
Remove the second factor; the owner needs a current code.
- otp reset ADDRESS
Operator only: remove the second factor whatever its state, unlocking the address. Its owner enrols again (by e-mail, or with
otp enroll).
The secret is stored wrapped under the key-encryption key, like a private key,
in a table only the pepsi-crypto role may read; wrap rotate re-wraps it
too.
85.1.44.1.4.5. Key-encryption-key commands¶
- wrap rotate [–dry-run]
Re-wrap every stored private key, and every second-factor secret, under the current
[pepsi-crypto] KEY_WRAP_KEY_ID. Rotation is incremental by construction, because each row records the key that sealed it: configure the new secret asKEY_WRAP_SECRETand keep the old one beside it asKEY_WRAP_SECRET_<OLD-ID>, pointKEY_WRAP_KEY_IDat the new id, and run this. Between the configuration change and the sweep the deployment keeps working — new material is sealed under the new key, old material still opens under the retired one — and only afterwards may the retired secret be removed.Rows whose key-encryption key has no configured secret are reported and left untouched rather than failing the run; so are rows re-wrapped concurrently (each update is guarded on the old key id, so a newer wrap is never written over).
- –dry-run
List what would be re-wrapped, and change nothing.
85.1.44.1.4.6. DNS publication¶
- dns ADDRESS
Print, in zone-file format, the DNS records that publish every published, active identity of ADDRESS: an
OPENPGPKEYrecord (RFC 7929) for OpenPGP material and anSMIMEArecord (RFC 8162, in the TLSA3 0 0— domain-issued certificate, full certificate, exact match — form) for S/MIME. Both are published under a hashed owner name, the hex of the first 28 octets of the SHA-256 of the local part, so the zone does not enumerate the addresses it serves.Publish these only in a DNSSEC-signed zone, which the output repeats as a comment: an unsigned key record is a key from whoever can answer for the zone, which is no improvement on having no key at all.
The DNS owner-name hash is case-sensitive (RFC 7929 §3), unlike the Web Key Directory hash of the same local part, which lower-cases. The two are computed by different functions on purpose.
pepsi-setup(1) prints the same records for every published identity as part of its DNS output, capped at a readable number; this command is how to get one specific address in a large deployment.
85.1.44.1.5. Key-server configuration¶
Web Key Directory publication needs no configuration: it follows each identity’s
published flag and is served by pepsi-httpd(1). Key-server publication
does, because it cannot be undone, and its options live in [pepsi-keys]:
VKS_PUBLISH(defaultno)Whether identities created or imported by hand start with their key-server upload requested, so that identity publish –retry uploads them and keeps retrying until the address is verified. An upload can be revoked but never withdrawn, so the default stays an operator’s decision. It does not gate the retry job, and it never applies to automatically created or registered keys; see Key-server uploads are requested per identity. With it off, identity publish –vks (or a user’s own request through the web console or by e-mail) remains available.
VKS_SERVER(default: the first[pepsi-keydiscovery] VKS_SERVERSentry)Where uploads go;
https://only. One server, not a list: publishing the same key to several multiplies an irreversible act, and each then has to be kept up to date with revocations.VKS_MAX_ATTEMPTS(default 5),VKS_RETRY_INTERVAL(default6 h),VKS_BATCH(default 50)How hard, how often and how many at a time identity publish –retry tries. Note that durations reject calendar units:
6 hparses,1 ddoes not.
85.1.44.1.6. Global Options¶
These global options precede the subcommand (a trailing flag is rejected).
- -c FILE, –config FILE
Read the configuration from FILE instead of searching the default locations. Rejected for unprivileged callers when this program is installed setuid: the configuration names the database connection, every
${…}-expanded path and the key-encryption key itself.- -L LOGLEVEL, –log LOGLEVEL
Set the logging verbosity. LOGLEVEL is one of
error,warn,info,debugortrace(default:info).- -v, –verbose
Show log messages from all sources, including third-party libraries.
- -h, –help
Print a usage summary and exit.
- -V, –version
Print the version and exit.
85.1.44.1.7. Exit Status¶
- 0
Successful completion. A destructive command that was declined at its confirmation prompt also exits 0, having changed nothing.
- 1
An error occurred: a malformed configuration file, an inadmissible option combination, an address that resolves to another user’s account, key material that cannot be parsed, a key-encryption key that is missing or does not open a row, or a failed database connection or query. The reason is written to the log.
85.1.44.1.8. Files¶
When –config is not given, the first existing file from the following list is used:
$XDG_CONFIG_HOME/pepsi.conf$HOME/.config/pepsi.conf/etc/pepsi/pepsi.conf/etc/pepsi.conf
secrets.d/pepsi-crypto.secretThe key-encryption key, beside the configuration file and pulled into the
[pepsi-crypto]section with an@inline-secret@directive. It is kept out of the world-readable configuration because it is the one secret that opens every stored private key; pepsi-setup(1) gives it mode0640and hands it to thepepsi-cryptoaccount on every run. Back it up separately — see the Key management chapter of the manual for what its loss costs.
No other file is read or written: all key material lives in the database.
85.1.44.1.9. Examples¶
Find the served addresses whose encryption is decided by a key somebody else supplied, and make a confirmed one the user’s own (an MUA key: no –private):
pepsi-keys -c /etc/pepsi/pepsi.conf peer unclaimed
pepsi-keys -c /etc/pepsi/pepsi.conf peer show bob@example.org
gpg --export bob@example.org > bob.pgp # the key bob confirmed to you
pepsi-keys -c /etc/pepsi/pepsi.conf identity import bob@example.org \
--protocol openpgp --purpose both --public bob.pgp
Generate an OpenPGP identity and check that GnuPG accepts the public half:
pepsi-keys -c /etc/pepsi/pepsi.conf identity generate alice@example.org \
--protocol openpgp --name 'Alice Example'
pepsi-keys -c /etc/pepsi/pepsi.conf identity list --address alice@example.org
pepsi-keys -c /etc/pepsi/pepsi.conf identity export 1 | gpg --import
Generate the S/MIME pair, printing a certificate request over each key for a real CA:
pepsi-keys -c /etc/pepsi/pepsi.conf identity generate alice@example.org \
--protocol smime --csr
Install the certificate that CA issued, over the key that is already in the store:
pepsi-keys -c /etc/pepsi/pepsi.conf identity import alice@example.org \
--protocol smime --purpose sign --public /tmp/alice-sign.pem --primary
Publish the address in DNS (signed zone only):
pepsi-keys -c /etc/pepsi/pepsi.conf dns alice@example.org >> example.org.zone
Withdraw a compromised signing key, then confirm the private half is gone:
pepsi-keys -c /etc/pepsi/pepsi.conf identity revoke 1 --reason 'laptop stolen'
pepsi-keys -c /etc/pepsi/pepsi.conf identity show 1
Trust a correspondent’s key by hand and pin it against later discovery:
pepsi-keys -c /etc/pepsi/pepsi.conf peer import bob@example.com \
--protocol openpgp --file bob.pgp --pin
Look one correspondent up with every enabled method and store the result:
pepsi-keys -c /etc/pepsi/pepsi.conf peer refresh --address bob@example.com
Keep the cache honest from a periodic timer: re-validate what is due, then drop what has aged out:
pepsi-keys -c /etc/pepsi/pepsi.conf peer refresh
pepsi-keys -c /etc/pepsi/pepsi.conf peer prune
Add an S/MIME trust anchor and see what is trusted:
pepsi-keys -c /etc/pepsi/pepsi.conf ca add /etc/ssl/certs/our-ca.pem --label 'Our CA'
pepsi-keys -c /etc/pepsi/pepsi.conf ca list
Rotate the key-encryption key, looking first:
pepsi-keys -c /etc/pepsi/pepsi.conf wrap rotate --dry-run
pepsi-keys -c /etc/pepsi/pepsi.conf wrap rotate
Expire everything that is past its validity (from a periodic timer):
pepsi-keys -c /etc/pepsi/pepsi.conf identity retire
Publish one key to the key server, then check it arrived:
pepsi-keys -c /etc/pepsi/pepsi.conf identity publish 1 --vks
pepsi-keys -c /etc/pepsi/pepsi.conf identity show 1
Drive every pending key-server verification to completion (from cron):
pepsi-keys -c /etc/pepsi/pepsi.conf identity publish --retry
Stop publishing a key, and tell the world the uploaded one is dead:
pepsi-keys -c /etc/pepsi/pepsi.conf identity unpublish 1
pepsi-keys -c /etc/pepsi/pepsi.conf identity revoke 1 --upload \
--reason 'address closed'
85.1.44.1.10. See Also¶
pepsi-keydisc(1), pepsi-httpd(1), pepsi-stage-vks-confirm(1), pepsi-setup(1), pepsi-config(1), pepsi-whitelist(1), pepsi-settings(1), pepsi.conf(5), pepsi.state(7)
85.1.44.1.11. Bugs¶
Report bugs to the Pepsi issue tracker.