Branching Out: `sq` Grows a Certificate Store, and More Convenient Trust Management

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.