OpenPGP Prompting and Caching
Daniel Kahn Gillmor
American Civil Liberties Union
125 Broad St.
New York, NY
10004
USA
dkg@fifthhorseman.net
08 August 2024
int
openpgp
Internet-Draft
Some OpenPGP secret keys and messages are locked with a passphrase. An OpenPGP-using application might want to prompt the user for the passphrase, or might want to permit the user to only enter the passphrase once while keeping the material unlocked for future use. This document describes a simple interface that can be used for prompting the user and for caching the results of such a prompt. It is designed to interoperate well with the Stateless OpenPGP Interface, and to facilitate its use both in test suite operations and in common patterns of interactive operation.

About This Document
The latest revision of this draft can be found at https://dkg.gitlab.io/openpgp-prompting-caching/. Status information for this document may be found at https://datatracker.ietf.org/doc/draft-dkg-openpgp-prompting-caching/.
Discussion of this document takes place on the OpenPGP Working Group mailing list (mailto:openpgp@ietf.org), which is archived at https://mailarchive.ietf.org/arch/browse/openpgp/. Subscribe at https://www.ietf.org/mailman/listinfo/openpgp/.
Source for this draft and an issue tracker can be found at https://gitlab.com/dkg/openpgp-prompting-caching/.
Introduction
Some OpenPGP secret keys and messages are locked with a passphrase.
An application that uses OpenPGP might want to prompt the user for the passphrase, or might want to permit the user to only enter the passphrase once while keeping the material unlocked for future use.
This document describes a simple interface that can be used for prompting the user and for caching the results of such a prompt across an interactive session. The OpenPGP Prompting and Caching interface described here can be referred to as OPAC. A specific command-line utility offering this interface is referred to as opac.
It is intended to interoperate well with the Stateless OpenPGP Interface (see [I-D.dkg-openpgp-stateless-cli]), and to facilitate the use of OpenPGP both in test suite operations and in common patterns of interactive operation.
Requirements Language
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
Terminology
This document tries to use certain terms with specificity:
- "Certificate" refers to an OpenPGP Certificate, or Transferable Public Key, as defined in [RFC9580]
- "Secret Key" refers to an OpenPGP Transferable Secret Key, as defined in [RFC9580].
- "S2K" refers to the OpenPGP String-to-Key process for deriving high-quality cryptographic symmetric keys from a human-memorable string of text, also defined in [RFC9580].
- "Session" refers to an interactive user session at a computer. Different human-computer interfaces may have different conceptions of a session, and this document is largely agnostic about those details. The salient features of a "session", for the purposes of this document, are that it has a defined time limit (often bounded by operations like "logging in" and "logging out"), a way of requesting and gathering feedback from the user ("prompting"), and is intentionally under some overarching administrative authority or control by the system operator ("the logged in user").
- "Fails with XXX" means that the invoked command terminates and yields a specific value associated with XXX (see Table 1) as its return code.
Theory of Operation
OPAC is a command-line interface to facilitate the use of OpenPGP, that binds together three things:
- The user's interactive session.
- An ephemeral cache of OpenPGP-related data that can "unlock" OpenPGP objects in the filesystem.
- A session-specific way to notify the user or elicit feedback from them ("prompting").
The ephemeral cache may be backed by a variety of different mechanisms. For example, it could use memory of an ephemeral session-bound process, or it could store data in a kernel-level keyring. Critically, the cache lasts no more than the duration of the user's session, and the material in the cache is never deliberately placed in non-ephemeral storage.
Since the cached material is associated with objects in the filesystem, OPAC tends to assume that all processes in the session will have the same view of the filesystem as each other, or at the very least that any process invoking OPAC will have the same view as any session-bound process that backs the OPAC cache.
There are unusual computing scenarios or configurations where OPAC will not work correctly, but it is intended to work with the overwhelming majority of interactive computing environments. See Section 7 for more details about OPAC's applicability.
Specification
opac uses a command-line interface, with a set of standardized subcommands.
version: Version information

opac version [--extended|--opac]

- Standard Input: ignored
- Standard Output: version information
This subcommand emits version information as UTF-8-encoded text.
With no arguments, the version string emitted should contain the name of the opac implementation, followed by a single space, followed by the version number. An opac implementation should use a version number that respects an established standard that is easily comparable and parsable, like [SEMVER].
If --extended is supplied, the implementation may emit multiple lines of version information. The first line MUST match the information produced by a simple invocation, but the rest of the text has no defined structure.
If --opac is supplied, the implementations should produce a single line with the implemented [SEMVER] opac interface that this implementation specifies. For this draft, that version is 0.1.
Example:

$ opac version
ExampleOpac 0.3
$ opac version --extended
ExampleOpac 0.3
LibDBus 0.11.4
See https://pgp.example/opac/ for more information
$ opac version --opac
0.1
$

get-password: Request a Password

opac get-password [--message MESSAGE] [--ignore-cache]
                  [--prompt-timeout TIMEOUT]
                  FILENAME

- Standard Input: ignored
- Standard Output: PASSWORD
When successful, this invocation emits the human-readable password the user expects to be associated with the OpenPGP material found in FILENAME on standard output, encoded as UTF-8 text. If any failure happens, it emits nothing on standard output.
If --ignore-cache is supplied, or if FILENAME is the special value -, opac MUST NOT look in its cache.
If an associated password is found in the cache, opac MAY ask the user to confirm its use before emitting it.
If no associated password is found in the cache, opac asks the user for the password associated with FILENAME.
The optional argument MESSAGE MUST be well-formed according to Section 6.7. If MESSAGE is not well-formed, opac get-password fails with MALFORMED_MESSAGE.
If the user declines to offer a password, or declines to permit use of the password from its cache, opac get-password fails with USER_DECLINED.
When the user's provided password is emitted, opac get-password MUST NOT emit any other data to standard output before terminating successfully. In particular, it must not pad the output with LINE FEED (U+000A) or other similar characters.
If the user fails to respond to a prompt after sufficient time has elapsed, opac get-password fails with PROMPT_TIMED_OUT. If --prompt-timeout is supplied, the specified timeout for the prompt is used. If it is not supplied, the prompt timeout is taken from the configuration (see Section 4.3.2).
opac get-password MUST NOT insert anything into the cache, or update the expiration date of anything in the cache.
cache-password: Cache a Password

opac cache-password [--on-reuse REUSE_DIRECTIVE]
                    [--duration CACHE_DURATION]
                    FILENAME

- Standard Input: PASSWORD
- Standard Output: nothing
This invocation reads the supplied password on standard input, and injects it in the cache associated with the FILENAME OpenPGP object in the filesystem.
Standard input MUST contain only a single UTF-8 encoded password, with no other information. However, it MAY include trailing whitespace characters, which opac strips before storing.
If opac cache-password does cache the password in association with FILENAME, it succeeds.
opac cache-password MAY decline to cache the supplied password for any reason. If it declines to cache, it SHOULD emit the reason for declining to stderr and fail with CACHE_DECLINED or some other more specific non-zero return code.
If FILENAME is - in this invocation, it MUST NOT insert anything in the cache, and fails with INVALID_FILENAME.
When associating the password with FILENAME in the cache, it chooses the duration from the cache by looking at the --duration argument and in its configuration (see Section 4.3.1). It chooses the shorter of those two durations to be the default cache.
When a cached password is requested for use, opac follows the associated REUSE_DIRECTIVE, either from the command line or from the configuration (see Section 4.3.3). If the REUSE_DIRECTIVE appears in both the configuration and the command line, opac cache-password uses the stricter of the two choices.
When inserting a password in the cache associated with FILENAME, if any other password is in the cache associated with FILENAME, the old value is removed from the cache.
cache-internal: Cache an Intermediate Value From a Calculation

opac cache-internal [--on-reuse REUSE_DIRECTIVE]
                    [--duration CACHE_DURATION]
                    FILENAME INTERNAL_LABEL

- Standard Input: Internal Value (octet stream)
- Standard Output: nothing
This invocation reads the supplied password on standard input, and injects it in the cache associated with the FILENAME OpenPGP object in the filesystem and the supplied INTERNAL_LABEL.
If the INTERNAL_LABEL is malformed (see Section 6.5), it MUST NOT insert anything in the cache, and fails with MALFORMED_INTERNAL_LABEL. If FILENAME is -, it MUST NOT insert anything in the cache, and fails with INVALID_FILENAME.
Standard input contains an arbitrary stream of octets, but it MUST be at least 1 octet long. If the input is entirely empty, opac cache-internal fails with MALFORMED_INTERNAL_VALUE
If opac cache-internal does cache the internal value in association with FILENAME and INTERNAL_LABEL, it succeeds.
opac cache-internal MAY decline to cache the supplied internal value for any reason. If it declines to cache, it SHOULD emit the reason for declining to stderr and fail with CACHE_DECLINED or some other more specific non-zero return code.
--on-reuse and --duration apply in the same way as opac cache-password (see Section 2.3).
When inserting an internal value in the cache associated with FILENAME and INTERNAL_LABEL, if any other password is in the cache associated with both FILENAME and INTERNAL_LABEL, the old value is removed from the cache. Note that multiple internal values can be held in the cache associated with a given FILENAME as long as they each have a distinct INTERNAL_LABEL.
get-internal: Retrieve an Intermediate Value From the Cache

opac get-internal [--message MESSAGE]
                  [--prompt-timeout PROMPT_TIMEOUT]
                  FILENAME INTERNAL_LABEL

- Standard Input: ignored
- Standard Output: Internal Value (octet stream)
This invocation retrieves a previously cached internal value associated with the FILENAME OpenPGP object in the filesystem and the supplied INTERNAL_LABEL.
If an associated internal value is found in the cache, opac get-internal MAY ask the user to confirm its use before emitting it.
If no associated internal value is found in the cache, opac get-internal fails with NO_ASSOCIATED_VALUE.
The optional argument MESSAGE MUST be well-formed according to Section 6.7. If MESSAGE is not well-formed, opac get-internal fails with MALFORMED_MESSAGE.
If the user declines to permit use of the internal value from its cache, opac get-internal fails with USER_DECLINED.
When returning a value from the cache, opac get-internal MUST emit exactly the same data to standard output that was provided on standard input from the corresponding call to opac cache-internal (see Section 2.4).
If the user fails to respond to a prompt after sufficient time has elapsed, opac get-internal fails with PROMPT_TIMED_OUT. If --prompt-timeout is supplied, the specified timeout for the prompt is used. If it is not supplied, the prompt timeout is taken from the configuration (see Section 4.3.2).
opac get-internal MUST NOT insert anything into the cache, or update the expiration date of anything in the cache.
drop-cache: Dropping Cached Data

opac drop-cache [--all-subsessions|FILENAME]

- Standard Input: ignored
- Standard Output: nothing
If FILENAME is supplied, this invocation empties the cache of all data (passwords and internal values) associated with FILENAME.
If FILENAME is not supplied, it empties the entire cache.
If any passwords or internal values were removed from the cache, and they were all successfully removed, the process succeeds.
If opac drop-cache failed to remove any item from the cache that it was instructed to remove, it fails with COULD_NOT_REMOVE_FROM_CACHE. In this case, it SHOULD emit an explanation to standard error.
If OPAC_SUBSESSION is set, it only removes items from the subsession-specific cache. If OPAC_SUBSESSION is not set, it only removes items from the main cache.
If --all-subsessions is present, and either OPAC_SUBSESSION is set of FILENAME is also present, then opac drop-cache fails with INCOMPATIBLE_OPTIONS. Otherwise, if --all-subsessions is present, it removes all items from the main cache and from every subsession cache as well.
notify: Notify the User of Additional Steps

opac notify FILENAME MESSAGE

- Standard Input: ignored
- Standard Output: NOTIFICATION_HANDLE
This invocation causes opac to emit a non-interactive notification to the user.
This is intended specifically for the case where the user needs to take an action for the OpenPGP operation to succeed that the application cannot trigger on the user's behalf. For example, if the implementation needs the user to press a button on a USB hardware token to permit the use of secret key material, it might use opac notify so that the user knows that the application is awaiting action from them.
If MESSAGE is malformed (see Section 6.7), it fails with MESSAGE_MALFORMED, and emits nothing on standard output.
If it was unable to produce a notification, it fails with COULD_NOT_NOTIFY, and emits nothing on standard output.
If it successfully notifies the user, it emits a NOTIFICATION_HANDLE (see Section 6.4) on standard output. This NOTIFICATION_HANDLE can be used to dismiss the notification (see Section 2.8) if the application determines it is no longer needed (for example, when the button has been pressed, and the USB hardware token returned a response).
opac notify ignores the cache, and uses no configuration variables, so it ignores OPAC_SUBSESSION.
dismiss: Dismiss a Notification

opac dismiss NOTIFICATION_HANDLE

- Standard Input: ignored
- Standard Output: nothing
This invocation is used solely to dismiss a notification that had been created with opac notify (see Section 2.7).
The only additional argument is a NOTIFICATION_HANDLE (see Section 6.4), which should match a value emitted from opac notify (see Section 2.7).
It always succeeds, even if the NOTIFICATION_HANDLE could not be dismissed.
opac dismiss ignores the cache, and uses no configuration variables, so it ignores OPAC_SUBSESSION.
Error Codes
opac, like any other process, returns an error code. The following values are defined for specific errors.

Return Codes for opac
Value | Name | Description
0 | SUCCESS | Success
1 | UNSUPPORTED_SUBCOMMAND | Unsupported subcommand
2 | UNSUPPORTED_OPTION | Unsupported option
3 | MALFORMED_MESSAGE | Message is malformed
4 | USER_DECLINED | User declined to provide a password or permit use of the cache
5 | PROMPT_TIMED_OUT | User failed to take action in response to a prompt
6 | CACHE_DECLINED | Cache entry was not added
7 | INVALID_FILENAME | An inappropriate filename was requested for caching
8 | COULD_NOT_REMOVE_FROM_CACHE | An entry remains in the cache that should have been dropped
9 | NO_ASSOCIATED_VALUE | A request was made to drop a password or internal cache entry that did not exist
10 | INVALID_CONFIGURATION_FILE | The config file was not well-formed
11 | MALFORMED_PASSWORD | Password format is not supported
12 | MALFORMED_INTERNAL_LABEL | The Internal contextual label was malformed
13 | MALFORMED_INTERNAL_VALUE | The Internal value was too short
14 | INCOMPATIBLE_OPTIONS | The command line options and environment variables supplied are mutually incompatible
15 | COULD_NOT_NOTIFY | A notification was requested, but could not be produced
16 | MALFORMED_SUBSESSION_ID | A malformed subsession identifier was supplied
Configuration
opac should behave sensibly without any configuration file, or with an empty configuration file.
It always looks for its configuration as an "inifile" in exactly one location:
- If $OPAC_SUBSESSION is set to a non-empty string, and $OPAC_SUBSESSION_CONFIG is set, it looks for the file in $OPAC_SUBSESSION_CONFIG. FIXME: if $OPAC_SUBSESSION_CONFIG is unset, should it really fall through to reading the main session config?
- Otherwise, if $XDG_CONFIG_HOME is set, it looks in $XDG_CONFIG_HOME/opac/config
- Otherwise, it looks in ~/.config/opac/config
opac does not attempt to coalesce multiple configuration files.
Configuration Example

[opac]
cache_duration=10s

[opac "~/.private/bob.key"]
on_reuse=confirm

Configuration File Format
The OPAC configuration file is an "inifile".
It has one optional base section, simply named opac, which contains configuration values that supersede the built-in defaults, and take effect when no override is present.
It has any number of file-specific subsections, which contain configuration overrides for specific objects in the filesystem. A file-specific subsection (within opac) is named with a representation of a path in the filesystem. The path MUST start with either / (indicating an absolute path to a file) or ~/ (indicating a filename relative to the user's home directory).
opac MUST ignore any unexpected or unknown sections or values. If opac ever updates its own configuration, it MUST NOT alter any unexpected or unknown sections or values, reorder sections, or modify comments.
When looking up a configuration option for a prompt or a cache for a password or internal value associated with FILENAME, opac looks up such a key in the following places:
1. In the [opac "FILENAME"] subsection of the config file
2. In [opac] section of the config file
3. In the application's built-in settings
Each configuration option is looked up separately, so in the example above, when using ~/.private/bob.key, on_reuse is set to confirm and cache_duration is set to 10s.
What are the configuration values?
There are three configuration directives available.
cache_duration: Time to Cache
The cache_duration directive accepts a CACHE_DURATION value (see Section 6.2).
A reasonable default value for cache_duration is 1h.
prompt_timeout: Time to Wait For User Feedback
The prompt_timeout directive accepts a PROMPT_TIMEOUT value (see Section 6.1).
A reasonable default value for prompt_timeout is 1m.
on_reuse: Action on Cache Retrieval
The on_reuse directive accepts a REUSE_DIRECTIVE value (see Section 6.3).
A reasonable default value for on_reuse is notify.
Subsessions
OPAC is typically used directly in association with the user's session. However, it also supports the idea of a "subsession", to support an isolated cache for the use cases of test suites (see Appendix A.2) or isolated applications (see Appendix A.3).
A subsession shares the same prompting mechanism as the main session, but uses a completely distinct cache and configuration.
opac knows to use a subsession when the environment variable OPAC_SUBSESSION is set to a non-empty string. The value of OPAC_SUBSESSION is the subsession identifier.
Subsession Identifiers
The subsession is identified by an ASCII string consisting of printable (non-whitespace) characters. The subsession identifier must be at least one octet long, and no more than 64 octets.
If opac is using the cache, and OPAC_SUBSESSION is non-empty and it is not conformant to this specification, opac fails with MALFORMED_SUBSESSION_ID.
Subsession Configuration
When using a subsession (that is, when OPAC_SUBSESSION is set), if the environment variable OPAC_SUBSESSION_CONFIG is set, opac reads its the configuration file from the location named in OPAC_SUBSESSION_CONFIG.
Data Types
PROMPT_TIMEOUT
This represents a length of time that a prompt should be displayed to the user before it is dismissed as having been ignored.
It is represented in an integer number of hours, minutes, and seconds, using letter suffixes. Hours, if present, come first and are suffixed with h. Minutes, if present, appear in the middle, and are suffixed with m. Seconds, if present, appear at the end, and are suffixed with s.
If no letter suffix is present, a completely numeric value is read as seconds.
For example, 1m30s means the same timeout as 90, and 1h2s means the same timeout as 3602
Using the value 0 means that where prompting is necessary, opac MUST NOT prompt, but should instead immediately fail with PROMPT_TIMED_OUT.
Using the special value never means to never deliberately time out the prompt. An OPAC implementation may nevertheless fail with PROMPT_TIMED_OUT due to other compelling circumstances. For example, if the prompt is a confirmation prompt for the reuse of a cached value, and that cached value is removed from the cache, or if the user's session is ending and the OPAC implementation is trying to clean up, it MAY fail with PROMPT_TIMED_OUT.
CACHE_DURATION
This represents the maximum amount of time that a value will persist in its position in the cache.
It is represented in an integer number of days, hours, minutes, and seconds, using letter suffixes. Days, if present, come first and are suffixed with d. Hours, if present, follow days and are suffixed with h. Minutes, if present, follow hours, and are suffixed with m. Seconds, if present, appear at the end, and are suffixed with s.
If no letter suffix is present, a completely numeric value is read as seconds.
For example, 1m30s means the same timeout as 90, and 1h2s means the same timeout as 3602
Using the special value never is the same as 0, meaning that OPAC should never cache this value. (This is particularly useful in the configuration file; from the command line, the easier thing would be to simply never ask for caching in the first place)
This value may be supplied from the command line during cache insertion (see Section 2.3 and Section 2.4) and from the configuration file (see Section 4.3.1). If it is specified in both the command line and the configuration file, the shorter of the two values provided is preferred.
REUSE_DIRECTIVE
This form describes what an OPAC implementation should do when a value is elicited from the cache, and the cache can supply the value.
It can be one of three possible values, in increasing order of strictness: ok, notify, or confirm.
ok permits the use of the cached value without any interaction with the user.
notify emits a standard notification while permitting the use of the cached value.
confirm asks the user to confirm the use of the cached value. In this case, the user may decline its use.
This value may be supplied from the command line during cache insertion (see Section 2.3 and Section 2.4) and from the configuration file (see Section 4.3.3). If it is specified in both the command line and the configuration file, the stricter of the two values provided is preferred. For example, if the configuration file indicates that a given value can be replayed from the cache as long as the user is notified (notify), then a command-line option of --on-reuse=confirm will ask the user to confirm the use, but a command-line option of --on-reuse=ok will not suppress the notification.
NOTIFICATION_HANDLE
This is a unique string that can be used to dismiss a previous notification when that notification is no longer relevant. It is produced by opac notify (see Section 2.7) and can be used to dismiss a specific notification with opac dismiss (see Section 2.8). It consists only of printable ASCII characters (no whitespace), and will be no more than 64 characters in length.
INTERNAL_LABEL
This is a label used by an OpenPGP implementation to associate the output of a specific cryptographic process associated with a file. It is implementation-specific, but must be a UTF-8 encoded string of no more than 128 octets in length. It is used, with the filename itself, as an index into the OPAC cache in opac cache-internal (see Section 2.4) and opac get-internal (see Section 2.5).
As a simple example, an OpenPGP implementation might choose to base64-encode the S2K Usage Octet and the accompanying parameters from a Secret Key Packet, and then prefix that with a short identifying label.
INTERNAL_VALUE
An internal value is simply a non-empty octet string of no more than 1000 octets.
Beyond the length limits, there are no constraints on its content.
MESSAGE
This is a limited textual string that is presented to the user when the user is prompted or notified. It is a way for an application requesting data from the user to provide some additional context for what is being requested or why it is being requested.
It MUST be UTF-8-encoded, no more than 120 characters, with no more than two internal LINE FEED (U+000A) characters. Leading and trailing whitespace will be ignored.
PASSWORD
OPAC expects a password to be a UTF-8-encoded, non-empty string. The password MUST NOT contain any LINE FEED (U+000A) characters, and leading or trailing whitespace may be stripped. A password MUST NOT be more than 1000 octets in length in its UTF-8 form. If a password is supplied that cannot be brought into conformance with these specifications, opac will fail with MALFORMED_PASSWORD.
The OpenPGP standard permits the use of a password as an arbitrary bytestream, so it is possible that some OpenPGP material may be encrypted with a password that does not conform to these constraints. It is recommended to use an OpenPGP tool that does not use OPAC change to such a password to a conformant string.
Subtleties and Nuance
The design of OPAC is intended to make normal, straightforward use cases easy and simple. It cannot handle all possible scenarios.
This section describes some details about the underlying assumptions in its design, including some descriptions of scenarios where it will not work as expected.
The OPAC Cache
The OPAC main cache is structured as a two-level nested tree. The top level index is by filename. Each filename can have zero or one PASSWORDs (see Section 6.8) associated with it. Additionally, each filename can have zero or more INTERNAL_LABELs (see Section 6.5) associated with it. If the (filename, INTERNAL_LABEL) tuple exists, it has an INTERNAL_VALUE associated with it.
Here is an example populated cache showing the indexing and the types of stored values:

├┬╴/tmp/secret-message.pgp
│└─╴"BarPGP:23e1b0a7cbe322bf5c096cb3d1bc" → INTERNAL_VALUE
├─╴/home/bob/src/mysoft/distribution.key → PASSWORD
└┬╴/home/bob/.private/bob.key → PASSWORD
 ├─╴"FooPGP:YXdsO2loeXFhaXdlZ2xhd2Vwa2Ewd2l0MjI" → INTERNAL_VALUE
 └─╴"FooPGP:Z3E0Mzl2bTtqOWZvd2x3" → INTERNAL_VALUE

Some observations about this example:
- No password was cached for /tmp/secret-message.pgp, but an internal value was cached, probably by the BarPGP implementation.
- Only a password was cached for the distribution.key.
- bob.key has cached a password and multiple internal values. One internal value might correspond to the output of some stage of the S2K process for unlocking the primary key, while the other might be for unlocking a subkey.
- The choice of the INTERNAL_LABELs present in the cache are arbitrary and up to the implementations. In this example, FooPGP and BarPGP have used a namespacing approach within labels to ensure that they don't accidentally collide with each other's labels.
The cache depends on a view of the filesystem that aligns with the view seen by the process invoking opac.
Cache Lifecycle
When the user's sessions starts, the OPAC cache is empty.
As elements are added to the cache, they should be marked clearly with expected expiration times, and removed (any associated memory wiped if possible) promptly when the stored password or internal value expires from the cache.
FIXME: configuration might change between insertion and retrieval from the cache; if they differ, which one should take precedence at retrieval time?
Subsession Cache Structure
Each subsession, identified by a subsession ID, has an independent cache that is shaped just like the main cache.
If OPAC gets a cache retrieval request for a previously unknown subsession ID, it treats it as a cache miss. If OPAC gets a cache insertion request for a previously unknown subsession ID, it initializes a new, empty cache for the subsession, and inserts the password or internal value in the new cache.
When a cache insertion or retrieval is performed under a subsession, it works only with that subsession cache, and does not look in the main cache. When an insertion or retrieval is done outside of a subsession, it only uses the main cache, and does not look in any subsession cache.
See also Section 7.3 for more about subsessions.
Cache Insertion vs. Cache Retrieval
Note that the opac interface completely separates cache insertion from cache retrieval.
In particular, opac get-password retrieves a password from the cache or from the user, but does not insert a user-provided password in the cache. This is because opac is not expected to directly interact with the OpenPGP objects, so it cannot know whether the user's supplied password works or not.
OPAC Prompting
OPAC supports three types of user interaction, collectively known as "prompting".
- Notification -- this might happen during cache retrieval, depending on the configuration, or explicitly via opac notify.
- Confirmation (boolean approval) -- this might happen during cache retrieval, depending on the configuration.
- Elicit a string of text -- this might happen during opac get-password, if the requested password is not found in the cache.
Additionally, a user of OPAC may dismiss a prior notification (when it is no longer relevant) -- but only notifications from opac notify can be dismissed in this way.
Each of these forms of prompting MAY be accompanied by an application-supplied MESSAGE, which will be clearly delimited from any OPAC-specific metadata, such as the path to the associated file, or any information about the caller of opac.
OPAC Subsessions
OPAC needs subsessions because there are circumstances where the desired scope of an OpenPGP unlocking step isn't contiguous with the user's interactive session as a whole.
In particular, a software test suite is likely to want to lock and unlock various OpenPGP objects independently of the rest of the session (see Appendix A.2). And an application that spans multiple processes want to lock and unlock various OpenPGP objects without interference from the rest of the session (see Appendix A.3).
Subsession Configuration
Note that the user's configuration values can't necessarily override subsession configuration, because subsession config might come from somewhere else, via the OPAC_SUBSESSION_CONFIG environment variable.
The two main use cases for subsessions are test suites and isolated apps. A test suite wants to use its own dedicated configuration, and for reproducibility it shouldn't be influenced by configuration that happens to live outside its own workspace, even if run within the user's session.
Similarly, an isolated app might want the user to be able to configure how the app handles OpenPGP prompting and caching, but without asking the user to manage some system-wide configuration.
Passwords vs. Internal Values
OPAC handles both passwords and internal values. Note the differences between how it handles each of these things:
- passwords are human-readable UTF-8 strings, internal values are octet strings.
- one password per FILENAME (in the main cache)
- arbitrary numbers of internal values per FILENAME
- nonetheless, they are all bound together at the FILENAME level for things like configuration and drop-cache.

Normative References
[RFC2119]

Key words for use in RFCs to Indicate Requirement Levels
S. Bradner
March 1997
BCP 14
RFC 2119
DOI 10.17487/RFC2119
[RFC8174]

Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words
B. Leiba
May 2017
BCP 14
RFC 8174
DOI 10.17487/RFC8174
[RFC9580]

OpenPGP
P. Wouters, Ed.
D. Huigens
J. Winter
Y. Niibe
July 2024
RFC 9580
DOI 10.17487/RFC9580

Informative References
[SEMVER]

Semantic Versioning 2.0.0
Tom Preston-Werner
18 June 2013
[I-D.dkg-openpgp-stateless-cli]

Stateless OpenPGP Command Line Interface
Daniel Kahn Gillmor
American Civil Liberties Union
1 March 2024
Internet-Draft draft-dkg-openpgp-stateless-cli-10
[I-D.dkg-openpgp-external-secrets]

OpenPGP External Secret Keys
Daniel Kahn Gillmor
American Civil Liberties Union
2 August 2024
Internet-Draft draft-dkg-openpgp-external-secrets-01
Example Uses
opac offers a minimal interface that can be used for a variety of common use cases. This section provides a few simple motivational examples of how it can be integrated to support these cases.
These examples are not necessarily complete, but should give a flavor of the supported scenarios.
Integration with the Stateless OpenPGP Command-Line Interface (SOP)
It should be easy to integrate OPAC into SOP, the Stateless OpenPGP command-line interface described in [I-D.dkg-openpgp-stateless-cli]. The integration can make use of prompting on its own, or it may also make use of the caching capabilities for a more usable, session-oriented approach.
OPAC + SOP: Prompting
Using POSIX-compliant shell, an OpenPGP application can combine opac and sop to decrypt a message with a locked Secret Key like this:

BOBPW=$(opac get-password bob.key) \
  sop decrypt --with-key-password @ENV:BOBPW bob.key < msg.txt

Alternately, using slightly fancier shells like bash or zsh that offer command substitution:

sop decrypt --with-key-password <(opac get-password bob.key) \
    bob.key < msg.txt

Other similar combinations (such as file descriptor redirection) should be straightforward from a wide range of programming environments.
OPAC + SOP: Prompting and Caching
If the OpenPGP application also wants to make use of opac's cache, it can do something like the following POSIX shell:

# from SOP's standard return codes:
KEY_IS_PROTECTED=67

BOBPW=$(opac get-password bob.key)
PASS=$BOBPW sop decrypt --with-key-password @ENV:PASS \
                bob.key < msg.txt > msg.decrypted;
SOP_RESULT=$?
if [ 0 -eq $SOP_RESULT ]; then
    printf '%s' "$PASS" | opac cache-password bob.key
elif [ $KEY_IS_PROTECTED -eq $SOP_RESULT ]; then
    opac drop-cache bob.key
fi

This process shows the use of the password cache: when a secret is successfully unlocked, the password is retained in the cache; if it does not work, the cache for that object is deliberately cleared so that the user would be prompted in the future.
Test Suite Isolation
A test suite that includes locked OpenPGP material typically wants to run code that would normally interact directly with the user, but should not require actual user interaction. Furthermore, a test suite typically does not want to interact with the caching or prompting of the interactive session of the developer running the test suite.
OPAC aims to support this use case with subsessions (see Section 5). An OPAC subsession is identified with a unique string, and a subsession indicated to opac through an environment variable.
Here is a comprehensive example in POSIX shell, wrapping a test suite so that it uses an OPAC subsession with a randomly-derived subsession identifier. It uses a dedicated configuration file to discourage prompting, and to cache everything for the entire session. It pre-emptively seeds the password cache with the password for the locked key. After the test suite is run, it cleans up after itself.

export OPAC_SUBSESSION=$(head -c12 < /dev/urandom | base64)
cat > opac.config <<EOF
[opac]
cache_duration=session
prompt_timeout=0
EOF
export OPAC_SUBSESSION_CONFIG=$(pwd)/opac.config
printf correct horse battery staple | opac cache-password ./bob.key

# ... run test suite that uses the locked bob.key...

opac drop-cache
unset -v OPAC_SUBSESSION OPAC_SUBSESSION_CONFIG

Single-Application Subsession Use
An OPAC subsession can also be used by a single application, to isolate its prompting and caching state from other applications also running in the session.
For example, an application might be split across multiple processes, and each process should be able to use the same shared cache as the other processes in the application. But the application might not want its own OpenPGP material to be available for other applications running in the same user session.
An application in this situation might set up an OPAC subsession upon application start, exporting it to all child processes. The two examples in this subsection are in Python-like pseudocode.

def app_init():
    ...
    if use_subsession:
        opac_subsession = base64(get_random(12))
        subprocess_environment["OPAC_SUBSESSION"] = opac_subsession
    ...

And on application shutdown, it could flush the cache:

def app_cleanup():
    ...
    if "OPAC_SUBSESSION" in subprocess_environemnt:
       subprocess("opac", "drop-cache")
    ...

OpenPGP Internal Use
An application that uses OpenPGP in a more fine-grained way than just interacting with a SOP instance might also want to cache some intermediate/internal values to free up resources in future processing.
For example, if an S2K operation takes a gigabyte of RAM and 3 seconds on the current processor, the application might want to cache the result of the S2K operation, rather than just caching the password.
The application can use OPAC for this kind of caching as well, but it uses a different interface than the password interface, because the user is never prompted to supply the value itself. Depending on the configuration, the user may be prompted to confirm the use of the value, though.
In Pythonic pseudocode, that might look something like:

def unlock_key(key, explanation):
    label = get_internal_label(key)
    value = subprocess("opac", "get-internal",
                       "--message", explanation,
                       key.filename, label)
    if value is None:
        password = subprocess("opac", "get-password",
                              "--message", explanation,
                              key.filename).get_stdout()
        value = OpenPGP_string_to_key(key.s2kparams, password)
    if unlock_key_with_intermediate(key, value):
        subprocess("opac", "cache-internal",
                   key.filename, label).write_stdin(value)
    else:
        subprocess("opac", "drop-cache", key.filename)

Notifying the User of External Requirements
In some cases, an OpenPGP-using application might need access to external secret key material, like a smartcard, USB hardware token, or some other device (see [I-D.dkg-openpgp-external-secrets]). In such a case, the application may need to notify the user that they need to take some additional action, like pressing a button on a USB device, or scanning their fingerprint.
When the application is aware of this need, it might want to send the user a generic notification. Once the user presses the button, the application might want to dismiss the notification. In Pythonic pseudocode, that might look like:

def handle_external_key():
    handle = subprocess("opac", "notify", "--message",
                        "Please press the USB button")
    ... # wait on external mechadimfor operation to complete
    subprocess("opac", "notify", "--clear", handle)

Future Work
This document describes a command-line interface, as that is a widely interoperable interface that anyone can implement.
It might be useful to describe more nuanced, alternate interfaces that would be easier to work with for some implementations, like a C library API, or even a D-Bus interface for those platforms that use D-Bus.
One of the tricks for this is that the interface -- in particular opac get-password or opac-get-internal-value -- can be quite high-latency, particularly when awaiting a response from the user. This probably means that a more nuanced interfce will require some kind of asynchronous interface functionality to avoid blocking the main application.
Acknowledgements
Much appreciation to the people who gave feedback on this document, including Jameson Rollins and Heiko Schäfer.
Document History
draft-dkg-openpgp-prompting-caching-00
Initial sketch of the interface.