Line data Source code
1 : // SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief GpgIdSigner, which signs a store's `.gpg-id` and verifies an existing
6 : * `.gpg-id.sig` against the configured signing keys.
7 : */
8 : #ifndef SRC_GPGIDSIGNER_H_
9 : #define SRC_GPGIDSIGNER_H_
10 :
11 : #include <QString>
12 : #include <QStringList>
13 : #include <functional>
14 :
15 : /**
16 : * @class GpgIdSigner
17 : * @ingroup keys
18 : * @brief The detached signature on a store's `.gpg-id`: whether the
19 : * configured signing key is usable, signing the file, and checking that an
20 : * existing `.gpg-id.sig` was made by one of the configured keys.
21 : *
22 : * Every gpg run goes through the @c Exec given at construction, so the
23 : * re-encryption worker can hand in its cancellable runner and tests a fake.
24 : * With no signing keys configured signing is off and verification passes:
25 : * `pass` itself only checks signatures when `PASSWORD_STORE_SIGNING_KEY` is
26 : * set.
27 : */
28 : class GpgIdSigner {
29 : public:
30 : /**
31 : * @brief Most bytes of a .gpg-id.sig verify() hands to gpg.
32 : *
33 : * A detached signature is well under a KiB; far beyond any real one.
34 : */
35 : static constexpr qint64 kMaxSignatureBytes = 64 * 1024;
36 :
37 : /**
38 : * @brief Blocking process runner: Executor::executeBlocking()'s shape.
39 : * @return The exit code, or -1 when the process could not run.
40 : */
41 : using Exec =
42 : std::function<int(const QString &app, const QStringList &args,
43 : const QString &input, QString *out, QString *err)>;
44 :
45 : /**
46 : * @brief Create a signer.
47 : * @param gpgExecutable The gpg binary; a `wsl ` prefix routes file paths
48 : * through wslpath.
49 : * @param signingKeys Full fingerprints from the settings (40 hex characters
50 : * for v4 keys, 64 for v5/v6), as pass's PASSWORD_STORE_SIGNING_KEY
51 : * requires: gpg reports fingerprints in its KEY_CONSIDERED and
52 : * VALIDSIG status lines, so a short key ID would never pass
53 : * haveSecretKey() or verify(). The first one signs, any of them
54 : * verifies.
55 : * @param exec Process runner; Executor::executeBlocking() when empty.
56 : */
57 : GpgIdSigner(QString gpgExecutable, QStringList signingKeys,
58 : Exec exec = Exec());
59 :
60 : /**
61 : * @brief Split the settings value into keys, upper-cased the way gpg
62 : * prints fingerprints.
63 : * @param passSigningKey The space-separated setting.
64 : * @return The keys, without empties.
65 : */
66 : static auto keysFromSetting(const QString &passSigningKey) -> QStringList;
67 :
68 : /**
69 : * @brief Whether any signing key is configured at all.
70 : * @return true when sign() will do something.
71 : */
72 128 : auto enabled() const -> bool { return !m_keys.isEmpty(); }
73 :
74 : /**
75 : * @brief The keys this signer was given.
76 : * @return The list from the constructor.
77 : */
78 : auto keys() const -> const QStringList & { return m_keys; }
79 :
80 : /**
81 : * @brief Whether gpg has the secret key that sign() will use, i.e. the
82 : * first configured key.
83 : * @return true when signing can work; false when no key is configured.
84 : */
85 : auto haveSecretKey() const -> bool;
86 :
87 : /**
88 : * @brief Write `<file>.sig`, a detached signature by the first key over
89 : * @p contents, the bytes the caller just wrote as @p gpgIdFile.
90 : *
91 : * gpg signs what it is fed on stdin, not what is under the file's name by
92 : * the time it reads it (a co-writer of the store could have swapped the
93 : * list between the write and the signing, and the user's key would have
94 : * vouched for theirs), and writes the signature into a directory of
95 : * QtPass's own; the signature then goes next to the list through
96 : * Util::writeFileReplacing(), never by opening the `.sig` name.
97 : *
98 : * Only the first key signs, as `--local-user`: gpg then fails when that
99 : * secret key is missing, where `--default-key` would quietly sign with
100 : * another one. The new signature is verified against @p contents the way
101 : * verify() does before it is placed; one that is not by a configured key
102 : * is not written.
103 : * @param gpgIdFile The `.gpg-id` the signature is for; `<gpgIdFile>.sig`
104 : * is written.
105 : * @param contents The exact bytes of that file, as written. UTF-8 text,
106 : * as a recipient list is; anything else is refused, as verify()
107 : * refuses it.
108 : * @param error Receives gpg's stderr, or the reason, when it fails.
109 : * @return true when the signature is in place and verified. Does nothing
110 : * and returns true when no key is configured.
111 : */
112 : auto sign(const QString &gpgIdFile, const QByteArray &contents,
113 : QString *error = nullptr) const -> bool;
114 :
115 : /**
116 : * @brief Check a detached signature against the given bytes: gpg must
117 : * report a VALIDSIG whose key or primary-key fingerprint is one of the
118 : * configured keys.
119 : *
120 : * The data goes to gpg on stdin, so the caller verifies exactly the bytes
121 : * it is about to use as the recipient list; verifying a path and reading
122 : * it again afterwards would let anyone who can write to the store swap
123 : * the file in between (#1842).
124 : * @param contents The `.gpg-id` bytes.
125 : * @param signatureFile The `.gpg-id.sig` next to it.
126 : * @return true when the signature is good, or when no key is configured.
127 : */
128 : auto verify(const QByteArray &contents, const QString &signatureFile) const
129 : -> bool;
130 :
131 : /**
132 : * @brief Read @p gpgIdFile and verify it against `<file>.sig`.
133 : * @param gpgIdFile The `.gpg-id`.
134 : * @param contents Receives the bytes that were verified, so the caller can
135 : * parse those and nothing else.
136 : * @return true when the signature is good, or when no key is configured.
137 : * false when the file cannot be read.
138 : */
139 : auto verifyFile(const QString &gpgIdFile, QByteArray *contents) const -> bool;
140 :
141 : /**
142 : * @brief Pick the fingerprints out of gpg's `--status-fd` VALIDSIG line.
143 : * @param statusOutput gpg's status output.
144 : * @return The signing key's fingerprint and the primary key's fingerprint,
145 : * or an empty list when there is no VALIDSIG, or when gpg says the
146 : * key that made that signature is revoked (REVKEYSIG, KEYREVOKED).
147 : */
148 : static auto validSigFingerprints(const QString &statusOutput) -> QStringList;
149 :
150 : private:
151 : /** @brief The gpg executable, as configured. */
152 : QString m_gpg;
153 : /**
154 : * @brief The configured signing keys. The first one signs; a signature by
155 : * any of them verifies.
156 : */
157 : QStringList m_keys;
158 : /** @brief How gpg is run; replaced by a fake in the unit tests. */
159 : Exec m_exec;
160 :
161 : /**
162 : * @brief Run gpg with @p args through m_exec.
163 : * @param args Arguments for gpg.
164 : * @param out If non-null, receives stdout.
165 : * @param err If non-null, receives stderr.
166 : * @param input Data for gpg's stdin; empty for none.
167 : * @return gpg's exit code.
168 : */
169 : auto run(const QStringList &args, QString *out = nullptr,
170 : QString *err = nullptr, const QString &input = QString()) const
171 : -> int;
172 : };
173 :
174 : #endif // SRC_GPGIDSIGNER_H_
|