By Neal | April 8, 2023
I’ve just released a new version of sq
, our general-purpose
command-line tool for Sequoia PGP, and it’s packed full of exciting,
user-visible changes. In line with our goal of providing great
end-to-end authentication, this release of sq
moves from working
exclusively in a stateless manner to including a full PKI, and a local
certificate store. It also adds a new high-level trust management
interface, sq link
. sq link
builds on the web of trust, but uses
concepts from address book management, which hopefully makes it easier
for end users to understand.
Introduction
sq
is a general-purpose command-line tool for Sequoia-PGP’s suite of
libraries. These include the sequoia-openpgp, sequoia-wot, and
sequoia-cert-store libraries. sq
aims to provide end users with
safe, and convenient access to OpenPGP functionality from the command
line. It uses a git
subcommand-style CLI, which, in our experience,
simplifies discovery, and improves usability.
This release of sq
is the culmination of two years of work, and
includes several major user-visible improvements. To date, sq
has
operated in a stateless manner: users explicitly passed the keys and
certificates that it should operate on, and implemented their own
trust model by maintaining an ad-hoc curated keyring. Version 0.29.0
of sq
adds support for a certificate store, includes a powerful
web-of-trust engine based on flow networks, and introduces an
easier-to-use interface, sq link
, to manage authentication decisions
based on concepts from how people use address books.
A Certificate Store
The first user-visible change to sq
is the addition of a certificate
store. This work was partially funded by NLNet as part of NGI
Assure.
Until now, sq
operated exclusively in a stateless manner. To
encrypt a message, the user pointed sq
at one or more files
containing OpenPGP certificates, and sq
encrypted the message to
each certificate, like this:
$ echo "Hi, Alice!" | sq encrypt --recipient-file alice.pgp
-----BEGIN PGP MESSAGE-----
...
Now that sq
supports a certificate store, it is possible to import
certificates, and then designate them by fingerprint or key ID:
$ sq import alice.pgp
Imported A117E54E8893FA93BB024B022CCBD78F9C18A871, "Alice <alice@a-company.com>"
Imported 1 new certificates, updated 0 certificates, 0 certificates unchanged, 0 errors.
$ echo "Something private" | sq encrypt --recipient-cert A117E54E8893FA93BB024B022CCBD78F9C18A871
-----BEGIN PGP MESSAGE-----
...
In addition to importing certificates from files, as shown above, this
version of sq
also changes sq keyserver get
, sq wkd get
, and sq dane get
to import the returned certificates directly into the
certificate store:
$ sq wkd get alice@a-company.com
...
(The old behavior, which exported the certificates to stdout
, is
still available by passing --output -
, where -
is a shortcut for
stdout
.)
Operations that implicitly use certificates like sq verify
now
automatically consult the certificate store to find any required
certificates.
To demonstrate how sq verify
works, we first show how Abe generates
a key, and then uses it to sign a message:
$ sq key generate --userid 'Abe <abe@a-company.com>' --export abe.pgp
$ echo 'Hello, world!' | sq sign --signer-file abe.pgp > abes-msg.pgp
Note: that even if we had imported Abe’s certificate into the
certificate store, we couldn’t have used it to sign the message. The
certificate store is only for public keys. Adding support for a
private key store is a separate project, which we plan to complete
and integrate into sq
by the end of the summer. Until then, private
keys still need to be listed explicitly.
We can try to verify the signed message, but this will fail, because
sq
doesn’t have the certificate:
$ sq verify abes-msg.pgp
No key to check checksum from C8AB24FA64E1BD64F0626CE2735C4591E5A61DF8
1 unknown checksum.
Error: Verification failed: could not fully authenticate any signatures
Perhaps surprisingly, after we import the certificate, sq verify
still fails:
$ sq import abe.pgp
Imported ABE2CC892F469F769BBA7EA78B3FC38FAAA2372F, "Abe <abe@a-company.com>"
Imported 1 new certificates, updated 0 certificates, 0 certificates unchanged, 0 errors.
$ sq verify abes-msg.pgp
Unauthenticated checksum from 735C4591E5A61DF8 ("Abe <abe@a-company.com>")
After checking that 735C4591E5A61DF8 belongs to "Abe <abe@a-company.com>",
you can authenticate the binding using
'sq link add 735C4591E5A61DF8 "Abe <abe@a-company.com>"'.
1 unauthenticated checksum.
Error: Verification failed: could not fully authenticate any signatures
$ echo $?
1
The two attempts to verify the signature fail in different ways. In
the first attempt, sq
couldn’t check the signature’s validity at all
(1 unknown checksum
). In the second attempt—after we imported
Abe’s certificate—sq
found the certificate in the certificate
store, and checked that the corresponding key really created the
signature, but it complained that it couldn’t authenticate the
signature (1 unauthenticated checksum
).
The reason sq
rejects the signature even though it has the signer’s
certificate is that it couldn’t establish a chain of trust to the
certificate, which it needs to authenticate the signature.
To understand why this is necessary, imagine that Mallory creates a
key with the user ID “Abe”, and he then convinces Bob to import the
corresponding certificate, and verify a message signed by the key. If
sq
indicated that the signature was authentic just because the
signature was mathematically correct, Bob might think that the message
actually came from Abe, although it actually came from Mallory. To
prevent this, sq
requires end-to-end authentication; there has
to be a chain of trust from a trust root to the signature that is
being verified.
To authenticate the signature, Bob could tell sq
what certificate he
expects the signature to come from using the --signer-cert
option:
$ sq verify --signer-cert ABE2CC892F469F769BBA7EA78B3FC38FAAA2372F abes-msg.pgp
Good signature from 735C4591E5A61DF8 ("Abe <abe@a-company.com>")
Hello, world!
1 good signature.
$ echo $?
0
$
Now, sq verify
indicates that the signature is good (1 good signature
), and the command’s exit code (0
) also indicates success.
Specifying the expected certificate is a simple trust model. But, it
is tedious, which makes it dangerous. As hinted at in the output,
this version of sq
also introduces a way to mark a certificate and a
User ID pair as authenticated using sq link
. An introduction to
this feature is presented below.
Exporting Certificates
We’ve now seen that we can import certificates, and use them both
explicitly (with sq encrypt
) and implicitly (with sq verify
).
Sometimes we also want to export certificates in order to use them in
a different context, or to share them with someone else. This is done
using sq export
. By default, it exports the entire certificate
store:
$ # Import a few more certificates
$ sq import bob.pgp ca.pgp
Imported B0B710EDCE7ECCF4986512706A910E85700FE600, "Bob <bob@some.org>"
Imported CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82, "OpenPGP CA <openpgp-ca@a-company.com>"
Imported 2 new certificates, updated 0 certificates, 0 certificates unchanged, 0 errors.
$ sq export | sq keyring list
0. A117E54E8893FA93BB024B022CCBD78F9C18A871 Alice <alice@a-company.com>
1. ABE2CC892F469F769BBA7EA78B3FC38FAAA2372F Abe <abe@a-company.com>
2. B0B710EDCE7ECCF4986512706A910E85700FE600 Bob <bob@some.org>
3. CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 OpenPGP CA <openpgp-ca@a-company.com>
Because we rarely want all the certificates, sq export
includes
several filters. The --cert
argument causes sq export
to only
export a certificate if the certificate’s fingerprint matches the
specified fingerprint or key ID; the --key
argument matches a
certificate if the fingerprint of the certificate or any of its
subkeys matches the specified fingerprint or key ID; --userid
matches if a user ID exactly matches the specified user ID; --email
matches if a user ID contains the specified email address; --domain
matches if an email address is from the specified domain; and --grep
matches if a user ID contains the specified substring. Here are a few
examples:
$ # Export a particular certificate.
$ sq export --cert A117E54E8893FA93BB024B022CCBD78F9C18A871 | sq keyring list
0. A117E54E8893FA93BB024B022CCBD78F9C18A871 Alice <alice@a-company.com>
$ # Export all certificates with an email address for a particular domain:
$ sq export --domain a-company.com | sq keyring list
0. A117E54E8893FA93BB024B022CCBD78F9C18A871 Alice <alice@a-company.com>
1. ABE2CC892F469F769BBA7EA78B3FC38FAAA2372F Abe <abe@a-company.com>
2. CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 OpenPGP CA <openpgp-ca@a-company.com>
$ # Export certificates that have a user ID that contains the string 'openpgp-ca@':
$ sq export --grep openpgp-ca@ | sq keyring list
0. CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 OpenPGP CA <openpgp-ca@a-company.com>
In all cases, the binding signatures (the signatures that associate a
user ID or a subkey with the certificate) are not checked. This is
because whether or not a binding signature is correct does not
authenticate the certificate, and, in the case of petnames (e.g.,
“Dad”), may not even require a self signature to be authentic! For
that a trust model is needed, and that’s up to the consumer of the
certificates. If you are creating a curated keyring, then you’ll need
to post process the output of sq export
.
Web of Trust
Most people who have heard of the web of trust know it from key signing parties. The web of trust is a lot more than a way to codify whose passport and fingerprint someone checked at an event, though. The web of trust is a highly expressive trust model, which can be used in a decentralized manner, a centralized manner (like X.509 as deployed by TLS on the web), and a mix thereof in which centralized authorities are granted only limited powers.
The web of trust is based on two similar, but distinct types of
assertions. First, someone can assert that an identity should be
associated with a certificate. That is, Alice, using her key, can
certify the statement: “the certificate identified by the fingerprint
B0B710EDCE7ECCF4986512706A910E85700FE600
is controlled by Bob,” like
so:
$ sq certify alice.pgp B0B710EDCE7ECCF4986512706A910E85700FE600 'Bob <bob@some.org>' | sq import
Imported B0B710EDCE7ECCF4986512706A910E85700FE600, "Bob <bob@some.org>"
Imported 0 new certificates, updated 1 certificates, 0 certificates unchanged, 0 errors.
That doesn’t mean that she is willing to rely on Bob in any way. Indeed, she may even know that Bob is a conman. A certification just means that she is convinced that Bob uses this key, which makes it easier to identify messages from him.
Second, a person can certify that they are willing to rely on certifications issued by a particular key. In this case, the person considers the key to be a certification authority (CA). CAs are also sometimes referred to as trusted introducers, as they are authorized to introduce you to someone.
For instance, “A Company” might use OpenPGP CA to manage a CA, which they use to certify their employees’ keys. This works similar to:
$ sq certify ca.pgp A117E54E8893FA93BB024B022CCBD78F9C18A871 'Alice <alice@a-company.com>' | sq import
Imported A117E54E8893FA93BB024B022CCBD78F9C18A871, "Alice <alice@a-company.com>"
Imported 0 new certificates, updated 1 certificates, 0 certificates unchanged, 0 errors.
$ sq certify ca.pgp ABE2CC892F469F769BBA7EA78B3FC38FAAA2372F 'Abe <abe@a-company.com>' | sq import
Imported ABE2CC892F469F769BBA7EA78B3FC38FAAA2372F, "Abe <abe@a-company.com>"
Imported 0 new certificates, updated 1 certificates, 0 certificates unchanged, 0 errors.
And they might certify an external partner, like Ollie from the Other company:
$ sq import ollie.pgp
Imported 0116ACD44F75E920EAEB05D5AAA5B9A16F7F4EA6, "Ollie <ollie@other.com>"
Imported 1 new certificates, updated 0 certificates, 0 certificates unchanged, 0 errors.
$ sq certify ca.pgp 0116ACD44F75E920EAEB05D5AAA5B9A16F7F4EA6 'Ollie <ollie@other.com>' | sq import
Imported 0116ACD44F75E920EAEB05D5AAA5B9A16F7F4EA6, "Ollie <ollie@other.com>"
Imported 0 new certificates, updated 1 certificates, 0 certificates unchanged, 0 errors.
The employees could also certify that the CA’s key is a CA:
$ sq certify --depth 1 alice.pgp CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 'OpenPGP CA <openpgp-ca@a-company.com>' | sq import
Imported CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82, "OpenPGP CA <openpgp-ca@a-company.com>"
Imported 0 new certificates, updated 1 certificates, 0 certificates unchanged, 0 errors.
$ sq certify --depth 1 abe.pgp CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 'OpenPGP CA <openpgp-ca@a-company.com>' | sq import
Imported CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82, "OpenPGP CA <openpgp-ca@a-company.com>"
Imported 0 new certificates, updated 1 certificates, 0 certificates unchanged, 0 errors.
(The relevant argument here is the --depth
parameter.)
Now, Alice can find and authenticate a certificate for her colleague Abe:
$ sq --trust-root A117E54E8893FA93BB024B022CCBD78F9C18A871 wot lookup --email abe@a-company.com
[✓] ABE2CC892F469F769BBA7EA78B3FC38FAAA2372F Abe <abe@a-company.com>: fully authenticated (100%)
◯ A117E54E8893FA93BB024B022CCBD78F9C18A871 ("Alice <alice@a-company.com>")
│ certified the following certificate on 2023-04-07 (expiry: 2028-04-06) as a fully trusted introducer (depth: 1)
├ CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 ("OpenPGP CA <openpgp-ca@a-company.com>")
│ certified the following binding on 2023-04-07 (expiry: 2028-04-06)
└ ABE2CC892F469F769BBA7EA78B3FC38FAAA2372F "Abe <abe@a-company.com>"
Note that sq
doesn’t only indicate that Alice can authenticate a
certificate for Abe, but shows why that is the case. Although most
users won’t need or want this information most of the time, this
output provides visibility into how the mechanism works. Visibility
into how a system works is the first heuristic for User Interface
Design according to Jakob Nielsen. It provides curious users a way
to verify their mental model of the system, and gives them assurance
that the system is working as expected.
OpenPGP’s web of trust mechanisms also allow users to limit the scope
of a CA. Employees, like Alice and Abe, may be willing to completely
rely on their employer’s CA, but an external person, say Bob, might
only be willing to use that CA for authenticating people in the
company. OpenPGP’s web of trust mechanisms allow Bob to say that he
is willing to rely on A Company’s CA to certify user IDs that have an
email address for the domain a-company.com
by using a regular
expression like this:
$ sq certify --depth 1 --regex '<[^>]+[@.]a-company\.com>$' bob.pgp \
CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 'OpenPGP CA <openpgp-ca@a-company.com>' | sq import
Imported CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82, "OpenPGP CA <openpgp-ca@a-company.com>"
Imported 0 new certificates, updated 1 certificates, 0 certificates unchanged, 0 errors.
Now, Bob can authenticate a certificate for Alice (and Abe), but not Ollie, even though the CA certified a certificate for Ollie:
$ sq --trust-root B0B710EDCE7ECCF4986512706A910E85700FE600 \
wot authenticate A117E54E8893FA93BB024B022CCBD78F9C18A871 "Alice <alice@a-company.com>"
[✓] A117E54E8893FA93BB024B022CCBD78F9C18A871 Alice <alice@a-company.com>: fully authenticated (100%)
◯ B0B710EDCE7ECCF4986512706A910E85700FE600 ("Bob <bob@some.org>")
│ certified the following certificate on 2023-04-07 (expiry: 2028-04-06) as a fully trusted introducer (depth: 1)
├ CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 ("OpenPGP CA <openpgp-ca@a-company.com>")
│ certified the following binding on 2023-04-07 (expiry: 2028-04-06)
└ A117E54E8893FA93BB024B022CCBD78F9C18A871 "Alice <alice@a-company.com>"
$ sq --trust-root B0B710EDCE7ECCF4986512706A910E85700FE600 \
wot authenticate 0116ACD44F75E920EAEB05D5AAA5B9A16F7F4EA6 "Ollie <ollie@other.com>"
No paths found.
Error: No paths found
Bob can use sq wot path
to see that he can’t authenticate a
certificate for Ollie, due to the regular expression he used:
$ sq --trust-root B0B710EDCE7ECCF4986512706A910E85700FE600 \
wot path B0B710EDCE7ECCF4986512706A910E85700FE600 \
CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 \
0116ACD44F75E920EAEB05D5AAA5B9A16F7F4EA6 "Ollie <ollie@other.com>"
[ ] 0116ACD44F75E920EAEB05D5AAA5B9A16F7F4EA6 Ollie <ollie@other.com>: not authenticated (0%)
◯ B0B710EDCE7ECCF4986512706A910E85700FE600 ("Bob <bob@some.org>")
│ No adequate certification found.
│ No active certifications by 6A910E85700FE600 for 089D20ADB5BEFC82 that make it at least a level-1 trusted introducer with a trust amount of at least 120
│ None of the certification's (B6BB by 6A910E85700FE600 on 089D20ADB5BEFC82 at 2023-04-07 15:41.05) regular expressions ("<[^>]+[@.]a-company\\.com>$") match the target User ID ("Ollie <ollie@other.com>")
├ CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 ("OpenPGP CA <openpgp-ca@a-company.com>")
│ certified the following binding on 2023-04-07 (expiry: 2028-04-06)
└ 0116ACD44F75E920EAEB05D5AAA5B9A16F7F4EA6 "Ollie <ollie@other.com>"
The other major scoping parameter is the trust amount. People can
use this parameter to indicate that a CA or a certification should
only be partially relied upon in authentication decisions. This is
useful when a person decides that a CA provides some evidence that a
binding is correct, but they don’t want to rely on them exclusively.
For instance, a verifying, and certifying keyserver like Proton’s
hkps://mail-api.proton.me checks and certifies the email address of
the certificates that it serves. So Bob could mark the Proton CA key
(0A8652FE5D53386057899FE9D806C1AF5978E8C7
) as a partially trusted
introducer like so:
$ sq keyserver -s hkps://mail-api.proton.me get 0A8652FE5D53386057899FE9D806C1AF5978E8C7
...
$ sq certify --depth 1 --amount 40 --regex '<[^>]+[@.]proton\.me>$' bob.pgp \
0A8652FE5D53386057899FE9D806C1AF5978E8C7 "openpgp-ca@proton.me <openpgp-ca@proton.me>" | sq import
Imported 0A8652FE5D53386057899FE9D806C1AF5978E8C7, "openpgp-ca@proton.me <openpgp-ca@proton.me>"
Imported 0 new certificates, updated 1 certificates, 0 certificates unchanged, 0 errors.
Then if Bob wanted to find the certificate for Proton’s security contact, he could do:
$ sq keyserver -s hkps://mail-api.proton.me get security@proton.me
$ sq --trust-root B0B710EDCE7ECCF4986512706A910E85700FE600 wot lookup --email security@proton.me
[ ] C4BC9337CC23A0BA855A3EF32EE753BBBA66EE76 security@proton.me <security@proton.me>: partially authenticated (34%)
Path #1 of 2, trust amount 40:
◯ B0B710EDCE7ECCF4986512706A910E85700FE600 ("Bob <bob@some.org>")
│ partially certified (amount: 40 of 120) the following certificate on 2023-04-07 (expiry: 2028-04-06) as a partially trusted (40 of 120) introducer (depth: 1)
├ 0A8652FE5D53386057899FE9D806C1AF5978E8C7 ("openpgp-ca@proton.me <openpgp-ca@proton.me>")
│ certified the following binding on 2022-10-19 (expiry: 2023-09-25)
└ C4BC9337CC23A0BA855A3EF32EE753BBBA66EE76 "security@proton.me <security@proton.me>"
Path #2 of 2, trust amount 1:
◯ 68F17E7A0AB6096E182F1FF278FAEE8058FBF25C ("Local Trust Root")
│ partially certified (amount: 1 of 120) the following certificate on 2023-04-07 as a partially trusted (1 of 120) introducer (depth: 1)
├ CA477353CCE0DE1B526CFF7D3E3D5B623283EAB2 ("Downloaded from the keyserver mail-api.proton.me")
│ certified the following binding on 2023-04-07
└ C4BC9337CC23A0BA855A3EF32EE753BBBA66EE76 "security@proton.me <security@proton.me>"
Could not authenticate any paths.
Error: Could not authenticate any paths
Here we see that there are actually two paths to a certificate for
security@proton.me
. The second one is via a minimally trusted (1
out of 120) shadow CA for hkps://mail-api.proton.me
. As discussed
further below, sq
automatically saves provenance information when
downloading certificates from verifying key servers.
A partially authenticated binding isn’t sufficient to authenticate a signature, for instance. But, it provides the user with some information about the binding, which can be used to further bootstrap trust. Also, the web of trust allows partially trusted paths in the web of trust to be combined, as shown above. Although in this case, it is not enough to fully authenticate the binding.
It’s now hopefully clear that the web of trust is a flexible mechanism to describe, and authenticate information both in a decentralized context as relied upon by activists, as well as in a centralized context as used on the web or in many companies.
Although the OpenPGP RFC specifies the low-level web-of-trust mechanisms, it doesn’t discuss how to interpret them, and neither of the other two OpenPGP implementations that implement the web of trust document how their implementations work in sufficient detail to recreate them.
One of the goals for our web of trust implementation is to be easy to understand, and fast. To achieve this, we explicitly decided to not be compatible with GnuPG. That said, our design produces similar judgments most of the time.
Our design is built around flow networks. We feel that they intuitively match how people think about trust. Imagine Alice partially trusts Mallory as a trusted introducer, and Mallory decides to trick Alice into considering a certificate for Bob to be authentic. Mallory could create a bunch of certificates, mark them as trusted introducers, and have them certify a fake certificate for Bob. But this won’t make our engine consider it any more authentic than when Mallory certifies it; Alice’s certification of Mallory’s certificate added a bottleneck.
We documented our design in a draft specification so that people can understand how it works, and other OpenPGP implementations can create interoperable implementations, which PGPainless is working on.
The above examples showed a small sample of the web-of-trust
functionality that sq
implements. sq wot
implements several
commands to work with a web of trust. sq wot authenticate
authenticates a binding between a certificate and a user ID; sq wot lookup
shows what certificates can be authenticated for a given user
ID; sq wot identify
shows what user IDs can be authenticated for a
given certificate; sq wot list
lists all bindings that can be
authenticated; and, sq wot path
lints a concrete path.
Sometimes it is useful to just hear what other people think even if we
haven’t decided to rely on them yet. This can be done using the
--gossip
option, which basically means: show paths to a given target
without respect to a trust root. This is helpful when attempting to
bootstrap trust. Imagine Ollie wants to find a certificate for
Alice. He might do:
$ sq wot --gossip lookup --email alice@a-company.com
[ ] A117E54E8893FA93BB024B022CCBD78F9C18A871 Alice <alice@a-company.com>: not authenticated (0%)
◯ A117E54E8893FA93BB024B022CCBD78F9C18A871 ("Alice <alice@a-company.com>")
│ certified the following binding on 2023-04-07
└ A117E54E8893FA93BB024B022CCBD78F9C18A871 "Alice <alice@a-company.com>"
[ ] A117E54E8893FA93BB024B022CCBD78F9C18A871 Alice <alice@a-company.com>: not authenticated (0%)
◯ CA0A31FD5B370E2067A91EE6089D20ADB5BEFC82 ("OpenPGP CA <openpgp-ca@a-company.com>")
│ certified the following binding on 2023-04-07 (expiry: 2028-04-06)
└ A117E54E8893FA93BB024B022CCBD78F9C18A871 "Alice <alice@a-company.com>"
...
Here, we see that there is a certificate with a self-signed user ID
for Alice, and that a certificate identifying itself as a CA for
a-company.com
has certified the same certificate. Ollie could now
go to the company’s website to look for additional evidence that that
is in fact their CA’s certificate. If he is sufficiently convinced,
he could certify it as a CA for company.com
, and then authenticate
the certificate for Alice, as well as other employees of
company.com
.
An Address Book-Style Trust Model
Although powerful, the sq certify
subcommand is a bit unwieldy for
most users. In particular, they need to think about a trust root, and
pro-actively make certifications. Taking inspiration from how
importing contacts works on many mobile phones, we’ve added a simpler
interface to sq
for managing certifications, sq link
, and some
supporting machinery to make authenticating links for users easier.
sq link
is a subcommand for managing links between user IDs and
certificates. The first major difference from sq certify
is that
sq link
uses an implicit trust root, which is created automatically.
To link a certificate and user ID using sq link
, the user just does:
$ sq import justus.pgp
Imported CBCD8F030588653EEDD7E2659B7DD433F254904A, "<teythoon@uber.space>"
Imported D2F2C5D45BE9FDE6A4EE0AAF31855247603831FD, "Justus Winter (Code Signing Key) <justus@pep-project.org>"
Imported 2 new certificates, updated 0 certificates, 0 certificates unchanged, 0 errors.
$ sq link add CBCD8F030588653EEDD7E2659B7DD433F254904A justus@sequoia-pgp.org
Linking CBCD8F030588653EEDD7E2659B7DD433F254904A and "Justus Winter <justus@sequoia-pgp.org>".
When provided with an email address as above, sq link add
automatically finds the matching self-signed User IDs, and certifies
those.
If we later decide that we are willing to rely on Justus’s
certifications for sequoia-pgp.org
users, we can make him a CA for
just that domain:
$ sq link add --ca sequoia-pgp.org CBCD8F030588653EEDD7E2659B7DD433F254904A justus@sequoia-pgp.org
CBCD8F030588653EEDD7E2659B7DD433F254904A, Justus Winter <justus@sequoia-pgp.org> was already linked at 2023-04-05 21:31:44 UTC.
Update trust depth: 0 -> 255.
Updating regular expressions:
Current link:
Updated link:
1. "<[^>]+[@.]sequoia-pgp\\.org>$"
Link parameters changed, updating link.
Linking CBCD8F030588653EEDD7E2659B7DD433F254904A and "Justus Winter <justus@sequoia-pgp.org>".
Or, if we are willing to rely on any certification that he makes, we would do:
$ sq link add --ca '*' CBCD8F030588653EEDD7E2659B7DD433F254904A justus@sequoia-pgp.org
CBCD8F030588653EEDD7E2659B7DD433F254904A, Justus Winter <justus@sequoia-pgp.org> was already linked at 2023-04-05 21:34:43 UTC.
Updating regular expressions:
Current link:
1. "<[^>]+[@.]sequoia-pgp\\.org>$"
Updated link:
Link parameters changed, updating link.
Linking CBCD8F030588653EEDD7E2659B7DD433F254904A and "Justus Winter <justus@sequoia-pgp.org>".
If at some point we realize we made a mistake, we can retract any
links using sq link retract
:
$ sq link retract CBCD8F030588653EEDD7E2659B7DD433F254904A
You never linked "<teythoon@uber.space>" to CBCD8F030588653EEDD7E2659B7DD433F254904A, no need to retract it.
You never linked "Justus Winter" to CBCD8F030588653EEDD7E2659B7DD433F254904A, no need to retract it.
You never linked "Justus Winter <justus@gnupg.org>" to CBCD8F030588653EEDD7E2659B7DD433F254904A, no need to retract it.
You never linked "Justus Winter <justus@pep.foundation>" to CBCD8F030588653EEDD7E2659B7DD433F254904A, no need to retract it.
CBCD8F030588653EEDD7E2659B7DD433F254904A, Justus Winter <justus@sequoia-pgp.org> was linked at 2023-04-05 21:35:54 UTC.
Updating trust amount: 120 -> 0.
Update trust depth: 255 -> 0.
Link parameters changed, updating link.
Breaking link between CBCD8F030588653EEDD7E2659B7DD433F254904A and "Justus Winter <justus@sequoia-pgp.org>".
You never linked "Justus Winter <justuswinter@gmx.de>" to CBCD8F030588653EEDD7E2659B7DD433F254904A, no need to retract it.
You never linked "Justus Winter <teythoon@avior.uberspace.de>" to CBCD8F030588653EEDD7E2659B7DD433F254904A, no need to retract it.
We are also able to create aliases, so-called petnames. For instance, I could create a shortcut for Justus:
$ sq link add CBCD8F030588653EEDD7E2659B7DD433F254904A --petname justus
Note: "justus" is NOT a self signed User ID. If this was a mistake, use
`sq link retract CBCD8F030588653EEDD7E2659B7DD433F254904A "justus"` to undo it.
Linking CBCD8F030588653EEDD7E2659B7DD433F254904A and "justus".
Because sq
encrypts to all certificates that can be fully
authenticated for a given name, this feature can be used to create a
group:
$ sq link add FEC154296C79773B1562511A65AC504EB50A8C43 --petname '<founders@sequoia-pgp.org>'
Note: "<founders@sequoia-pgp.org>" is NOT a self signed User ID. If this was a mistake,
use `sq link retract FEC154296C79773B1562511A65AC504EB50A8C43 "<founders@sequoia-pgp.org>"` to undo it.
Linking FEC154296C79773B1562511A65AC504EB50A8C43 and "<founders@sequoia-pgp.org>".
$ sq link add CBCD8F030588653EEDD7E2659B7DD433F254904A --petname '<founders@sequoia-pgp.org>'
...
$ sq link add 8F17777118A33DDA9BA48E62AACB3243630052D9 --petname '<founders@sequoia-pgp.org>'
...
$ sq wot list --email founders@sequoia-pgp.org
[✓] 8F17777118A33DDA9BA48E62AACB3243630052D9 <founders@sequoia-pgp.org>: fully authenticated (100%)
◯ F1D5E77C73C56AA1CF09A99E3C12CAC0894064D9 ("Local Trust Root")
│ certified the following binding on 2023-04-05
└ 8F17777118A33DDA9BA48E62AACB3243630052D9 "<founders@sequoia-pgp.org>"
[✓] CBCD8F030588653EEDD7E2659B7DD433F254904A <founders@sequoia-pgp.org>: fully authenticated (100%)
◯ F1D5E77C73C56AA1CF09A99E3C12CAC0894064D9 ("Local Trust Root")
│ certified the following binding on 2023-04-05
└ CBCD8F030588653EEDD7E2659B7DD433F254904A "<founders@sequoia-pgp.org>"
[✓] FEC154296C79773B1562511A65AC504EB50A8C43 <founders@sequoia-pgp.org>: fully authenticated (100%)
◯ F1D5E77C73C56AA1CF09A99E3C12CAC0894064D9 ("Local Trust Root")
│ certified the following binding on 2023-04-05
└ FEC154296C79773B1562511A65AC504EB50A8C43 "<founders@sequoia-pgp.org>"
$ echo | sq encrypt --recipient-email founders@sequoia-pgp.org | sq inspect
-: Encrypted OpenPGP Message.
Recipient: C2B819056C652598
Recipient: 08CC70F8D8CC765A
Recipient: BCD24A69A96B859F
Recipient: FF45D156D908BE1F
Sometimes we find a certificate, and aren’t able to immediately
confirm its authenticity. If the message we want to send doesn’t
require strong protection, we may decide to accept the risk that
someone else may read it. That doesn’t mean that we want to accept
the certificate permanently, though. To remove the burden to remember
to check the binding’s authenticity in the future, sq
makes it easy
to temporarily accept a link:
$ sq link add --all --temporary CBCD8F030588653EEDD7E2659B7DD433F254904A
Linking CBCD8F030588653EEDD7E2659B7DD433F254904A and "<teythoon@uber.space>".
Linking CBCD8F030588653EEDD7E2659B7DD433F254904A and "Justus Winter".
Linking CBCD8F030588653EEDD7E2659B7DD433F254904A and "Justus Winter <justus@gnupg.org>".
Linking CBCD8F030588653EEDD7E2659B7DD433F254904A and "Justus Winter <justus@pep.foundation>".
CBCD8F030588653EEDD7E2659B7DD433F254904A, Justus Winter <justus@sequoia-pgp.org> was retracted at 2023-04-05 21:36:40 UTC.
Updating expiration time: no expiration -> 2023-04-12 21:40:11 UTC.
Updating trust amount: 0 -> 120.
Creating a temporary link, which expires in a week.
Linking CBCD8F030588653EEDD7E2659B7DD433F254904A and "Justus Winter <justus@sequoia-pgp.org>".
Linking CBCD8F030588653EEDD7E2659B7DD433F254904A and "Justus Winter <justuswinter@gmx.de>".
As stated in the output, this command creates a link, which expires after a week. What isn’t said, but happens behind the scenes, is that the binding is certified twice: once, a second ago, as a partially trusted link (trust amount: 40 out of 120), and a second time, now, as a fully trusted link, which expires in a week. This means that we are able to use the link now, as we wanted:
$ sq encrypt --recipient-email justus@sequoia-pgp.org
-----BEGIN PGP MESSAGE-----
...
And in a week, we’ll get an error that justus@sequoia-pgp.org
can’t
be fully unauthenticated, which is exactly what we want. But we’ll
get a reminder about what we did in the form of the certificate still
being partially authenticated:
$ faketime -f +8d sq encrypt --recipient-email justus@sequoia-pgp.org
None of the certificates with the email address "justus@sequoia-pgp.org" can be authenticated using the configured trust model:
1. When considering CBCD8F030588653EEDD7E2659B7DD433F254904A (Justus Winter <justus@sequoia-pgp.org>):
CBCD8F030588653EEDD7E2659B7DD433F254904A, "Justus Winter <justus@sequoia-pgp.org>" cannot be authenticated at the required level (40 of 120). After checking that Justus Winter <justus@sequoia-pgp.org> really controls CBCD8F030588653EEDD7E2659B7DD433F254904A, you could certify their certificate by running `sq link add CBCD8F030588653EEDD7E2659B7DD433F254904A "Justus Winter <justus@sequoia-pgp.org>"`.
Error: --recipient-email
Caused by:
None of the certificates with the email address "justus@sequoia-pgp.org" can be authenticated using the configured trust model
It is possible to view active links using sq link list
:
$ sq link list
CBCD8F030588653EEDD7E2659B7DD433F254904A, "<teythoon@uber.space>" is linked: expiry: 2023-04-12.
CBCD8F030588653EEDD7E2659B7DD433F254904A, "Justus Winter" is linked: expiry: 2023-04-12.
CBCD8F030588653EEDD7E2659B7DD433F254904A, "Justus Winter <justus@gnupg.org>" is linked: expiry: 2023-04-12.
CBCD8F030588653EEDD7E2659B7DD433F254904A, "Justus Winter <justus@pep.foundation>" is linked: expiry: 2023-04-12.
CBCD8F030588653EEDD7E2659B7DD433F254904A, "Justus Winter <justus@sequoia-pgp.org>" is linked: expiry: 2023-04-12.
CBCD8F030588653EEDD7E2659B7DD433F254904A, "Justus Winter <justuswinter@gmx.de>" is linked: expiry: 2023-04-12.
sq
automatically creates links when there is evidence that a binding
between a user ID and a certificate is correct. For instance, there
are currently three OpenPGP keyservers that do a basic check that the
certificate should be associated with the returned user IDs:
keys.openpgp.org
,
keys.mailvelope.com
, and
mail-api.proton.me
.
When creating these links, sq
doesn’t use the local trust root, but
a keyserver-specific shadow CA. That is, sq
generates a separate
key, and certifies that as a minimally trusted CA (that is, with a
trust amount of 1 out of 120) using the local trust root.
$ sq keyserver get neal@sequoia-pgp.org
Recorded provenance information for 52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0, "Downloaded from the keyserver keys.openpgp.org"
Created the local CA "Downloaded from the keyserver keys.openpgp.org" for
certifying certificates downloaded from this service. The CA's trust amount
is set to 1 of 120. Use
`sq link add --ca '*' --amount N 52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0` to override it.
Or `sq link retract 52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0` to disable it.
Recorded provenance information for 8F17777118A33DDA9BA48E62AACB3243630052D9, "Neal H. Walfield <neal@gnupg.org>"
Recorded provenance information for 8F17777118A33DDA9BA48E62AACB3243630052D9, "Neal H. Walfield <neal@pep-project.org>"
Recorded provenance information for 8F17777118A33DDA9BA48E62AACB3243630052D9, "Neal H. Walfield <neal@sequoia-pgp.org>"
Recorded provenance information for 8F17777118A33DDA9BA48E62AACB3243630052D9, "Neal H. Walfield <neal@walfield.org>"
Importing 1 certificates into the certificate store:
1. 8F17777118A33DDA9BA48E62AACB3243630052D9 Neal H. Walfield <neal@walfield.org>
Imported 1 new certificates, updated 0 certificates, 0 certificates unchanged, 0 errors.
After checking that a certificate really belongs to the stated owner, use "sq link add FINGERPRINT" to mark the certificate as authenticated.
In the above output, we see that the key
52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0
was generated, and assigned
the user ID “Downloaded from the keyserver keys.openpgp.org,” and
links were established for each of the returned user IDs.
By using an intermediate CA instead of the local trust root, it is
easy for the user to fine tune how much they trust different
certificate directories. For instance, if my threat model allows me
to completely rely on keys.openpgp.org
, then I could do:
$ sq link add --all --ca \* 52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0
52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0, Downloaded from the keyserver keys.openpgp.org was already linked at 2023-04-05 21:53:07 UTC.
Updating trust amount: 1 -> 120.
Update trust depth: 1 -> 255.
Updating exportable flag: true -> false.
Link parameters changed, updating link.
Linking 52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0 and "Downloaded from the keyserver keys.openpgp.org".
And, any user ID and certificate pairs downloaded from
keys.openpgp.org
—both in the past and in the future—would be
considered fully authenticated:
$ sq wot identify 8F17777118A33DDA9BA48E62AACB3243630052D9
[✓] 8F17777118A33DDA9BA48E62AACB3243630052D9 Neal H. Walfield <neal@sequoia-pgp.org>: fully authenticated (100%)
◯ 3C7BAE3E00BC082958601495187648EA171CDA4A ("Local Trust Root")
│ certified the following certificate on 2023-04-05 as a fully trusted meta-introducer (depth: unconstrained)
├ 52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0 ("Downloaded from the keyserver keys.openpgp.org")
│ certified the following binding on 2023-04-05
└ 8F17777118A33DDA9BA48E62AACB3243630052D9 "Neal H. Walfield <neal@sequoia-pgp.org>"
...
Of course, the intermediate CA’s link can be retracted in the usual way:
$ sq link retract 52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0
52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0, Downloaded from the keyserver keys.openpgp.org was linked at 2023-04-05 21:56:05 UTC.
Updating trust amount: 120 -> 0.
Update trust depth: 255 -> 0.
Link parameters changed, updating link.
Breaking link between 52DAF1EAC4C63A5E7C5054A91D13C3C9527ABDD0 and "Downloaded from the keyserver keys.openpgp.org".
$ sq wot identify 8F17777118A33DDA9BA48E62AACB3243630052D9
No paths found.
These links are also automatically created for certificates downloaded
from a WKD and using DANE. In those cases, sq
only links the user
IDs that contain the email address that was looked up.
In the future, we plan to automatically record other types of authentication evidence in the user’s web of trust. For instance, we can record when a certificate is first used for TOFU purposes.
As all of the certifications that sq link
makes include a
non-exportable flag, no special care is needed to avoid leaking the
user’s social graph when exporting certificates. Of course, this does
mean that an attacker who gets access to the user’s machine may be
able to see that information. To reduce the impact of this type of
attack, we plan to store this information in an encrypted database,
which users can protect using a password, a token, or not at all,
depending on their threat model.
sq
+ gpg
Perhaps the biggest hurdle to adopting sq
is adding support for
Sequoia to existing programs. To work around that, we created the
Sequoia Chameleon, a project to reimplement the gpg
CLI using
Sequoia. You can read about the first preview release, which we
made at the end of last year. Although still not complete, most
functionality required for day-to-day usage is already implemented,
and many test suites pass when using the chameleon instead of gpg
.
Starting with version 0.3, the Chameleon uses both sq
’s certificate
store, automatically imports gpg
’s public keyring into it, and uses
sq
’s local trust root when authenticating bindings. This means that
it is possible to use sq
’s PKI, and programs that use the Chameleon
will automatically see your trust judgments, as the following
transcript shows (note: gpg
is the chameleon):
$ # gpg doesn't know about the certificate.
$ gpg -k A117E54E8893FA93BB024B022CCBD78F9C18A871
gpg: error reading key: No public key
$ # Import it using sq.
$ sq import alice.pgp
Imported A117E54E8893FA93BB024B022CCBD78F9C18A871, "Alice <alice@a-company.com>"
Imported 1 new certificates, updated 0 certificates, 0 certificates unchanged, 0 errors.
$ # gpg now sees it, but it is unauthenticated (`unknown`).
$ gpg -k A117E54E8893FA93BB024B022CCBD78F9C18A871
pub ed25519 2023-04-07 [C] [expires: 2026-04-07]
A117E54E8893FA93BB024B022CCBD78F9C18A871
uid [ unknown] Alice <alice@a-company.com>
sub ed25519 2023-04-07 [S] [expires: 2026-04-07]
sub ed25519 2023-04-07 [A] [expires: 2026-04-07]
sub cv25519 2023-04-07 [E] [expires: 2026-04-07]
$ # Use sq to mark the certificate and User ID as authenticated.
$ sq link add --all A117E54E8893FA93BB024B022CCBD78F9C18A871
Linking A117E54E8893FA93BB024B022CCBD78F9C18A871 and "Alice <alice@a-company.com>".
$ # gpg now also considers it to be authenticated ('full')
$ gpg -k A117E54E8893FA93BB024B022CCBD78F9C18A871
pub ed25519 2023-04-07 [C] [expires: 2026-04-07]
A117E54E8893FA93BB024B022CCBD78F9C18A871
uid [ full ] Alice <alice@a-company.com>
sub ed25519 2023-04-07 [S] [expires: 2026-04-07]
sub ed25519 2023-04-07 [A] [expires: 2026-04-07]
sub cv25519 2023-04-07 [E] [expires: 2026-04-07]
Conclusion
We believe that this release of sq
is a significant step toward our
goal of improving the tooling in the OpenPGP ecosystem. If you
disagree, have ideas on how to improve the interfaces that we
designed, want to collaborate, or support us financially, please get
in touch!
Release
I have published sequoia-sq on crates.io. You can also fetch version 0.29.0 using the v0.29.0 tag, which I signed:
$ git verify-tag v0.29.0
gpg: Signature made Fri Apr 07 23:52:44 2023 +02:00
gpg: using RSA key C03FA6411B03AE12576461187223B56678E02528
gpg: Good signature from "Neal H. Walfield <neal@walfield.org>" [ultimate]
gpg: "Neal H. Walfield <neal@gnupg.org>"
gpg: "Neal H. Walfield <neal@pep-project.org>"
gpg: "Neal H. Walfield <neal@pep.foundation>"
gpg: "Neal H. Walfield <neal@sequoia-pgp.org>"
Note: sq used to be part of our main repository, but it has now been split off into its own repository.
Example Certificates
The certificates used in the above examples were created as follows:
$ sq key generate --userid 'Alice <alice@a-company.com>' --export alice.pgp
$ sq key generate --userid 'Abe <abe@a-company.com>' --export abe.pgp
$ sq key generate --userid 'OpenPGP CA <openpgp-ca@a-company.com>' --export ca.pgp
$ sq key generate --userid 'Bob <bob@some.org>' --export bob.pgp
$ sq key generate --userid 'Ollie <ollie@other.com>' --export ollie.pgp
$ export SQ_CERT_HOME=$(mktemp -d)
$ sq import alice.pgp abe.pgp ca.pgp bob.pgp ollie.pgp
$ sq keyserver get FEC154296C79773B1562511A65AC504EB50A8C43
$ sq keyserver get CBCD8F030588653EEDD7E2659B7DD433F254904A
$ sq keyserver get 8F17777118A33DDA9BA48E62AACB3243630052D9
Financial Support
Since the start of the project over five years ago, the p≡p foundation financially supports the people who work on Sequoia. In 2021, the NLnet foundation awarded us six grants as part of the NGI Assure program.
We are actively looking for additional financial support to diversify our funding.
You don’t need to directly use Sequoia to be positively impacted by it. We’re focused on creating tools for activists, lawyers, and journalists who can’t rely on centralized authentication solutions. So, consider donating. Of course, if your company is using Sequoia, consider sponsoring a developer (or two). Note: if you want to use Sequoia under a license other than the LGPLv2+, please contact the foundation.