<!--
SPDX-FileCopyrightText: 2021-2023 Wiktor Kwapisiewicz <wiktor@metacode.biz>
SPDX-License-Identifier: MIT OR Apache-2.0
-->

# `ssh-openpgp-auth`

This tool provides client-side functionality to transparently verify the identity of remote SSH hosts, based on trust chains rooted in the user's OpenPGP configuration.

Concretely, this tool fetches OpenPGP certificates for remote SSH hosts before opening SSH connections. The host certificate is verified based on OpenPGP trust chains, starting from the user's configured trust roots. If there is a valid trust chain, this tool adds the remote host's SSH public key to the local OpenSSH "known host" configuration.

To gracefully handle host key life cycle events, the remote host's OpenPGP certificate is automatically refreshed when it expires.

All OpenPGP certificates are stored locally in the standard [CertD](https://datatracker.ietf.org/doc/draft-nwjw-openpgp-cert-d/) directory (e.g. on Linux the default path is `.local/share/pgp.cert.d`, see [3.8. Platform-specific conventions](https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#section-3.8)).

## Installation

This tool can be used either on a per-remote-host basis, or globally, by editing the `.ssh/config` file:

```
Host example.com
	KnownHostsCommand /usr/bin/ssh-openpgp-auth authenticate %H
```

(As a sample real-life host which supports SSH OpenPGP Certificates one can use `metacode.biz` instead of `example.com`).

## Verification flags

By default, this tool fetches missing certificates and does basic integrity checks on the remote host's certificate. It is possible to enable stricter checks by appending additional flags. Additional verification flags cause the tool to perform stricter checks.

### Web of Trust verification (`--verify-wot`)

The following example illustrates defining trust roots in the user's OpenPGP certificate store, to perform host certificate verification using the Web of Trust.

Note that if the user already has a Web of Trust setup (e.g., to rely on their organization's OpenPGP CA instance), these trust roots are leveraged automatically. This tool will then "just work", and rely on chains from trust roots to remote host certificates. Remote host certificates are automatically fetched over the network (using the WKD protocol), and trust calculations happen locally whenever the tool runs.

### DNS/Keyoxide proof verification (`--verify-dns-proof`)

This validation requires that the key fingerprint is present in the DNS zone of a host:

```
$ dig +short TXT metacode.biz
"openpgp4fpr:198c722a4bac336e9daaae44579d01b3abe1540e"
"openpgp4fpr:653909a2f0e37c106f5faf546c8857e0d8e8f074"
```

The exact format will be specified in the future (see [issue #25](https://codeberg.org/wiktor/ssh-openpgp-auth/issues/25)).

## Usage example

Let's do an example run of using this tool in an isolated environment. To set up a test environment, we configure a temporary directory to use as OpenPGP certificate store:

```sh
export SQ_CERT_STORE=$(mktemp -d)
export PGP_CERT_D=$SQ_CERT_STORE
```

Then we import the sample host certificate (which has the fingerprint `D9E95D7F42E87610676C40B47E8432836DA1625E`) into our temporary local certificate store, and directly configure it as a local trust root:

```sh
sq cert import < fixtures/example.asc

sq pki link add --all D9E95D7F42E87610676C40B47E8432836DA1625E
```

After defining the certificate as a trust root, the "known hosts" configuration contains a single authentication key in SSH format:

```sh
KNOWN_HOSTS=$(ssh-openpgp-auth authenticate --verify-wot example.com)

[ "$KNOWN_HOSTS" = "example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1KLfPT949Gq15XcaTkxFntkp6fFyoNq0JkPOKaktJM F5CEDEED08E9EA536034F5823475162385DF08AF" ]
```

It is possible to add more verbose output to see which trust roots have been found and which certificates are being used by using the `--verbose` flag:

```sh
KNOWN_HOSTS=$(ssh-openpgp-auth authenticate --verbose --verify-wot example.com)

# Note that the trust root is automatically generated by sq thus cannot
# be mentioned here verbatim
RE='^# Found trust root: [A-F0-9]+
# Found local cert: D9E95D7F42E87610676C40B47E8432836DA1625E
# Using cert store: .*
# Web of Trust verification of D9E95D7F42E87610676C40B47E8432836DA1625E succeeded
# Certificate D9E95D7F42E87610676C40B47E8432836DA1625E, exporting subkey F5CEDEED08E9EA536034F5823475162385DF08AF
example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1KLfPT949Gq15XcaTkxFntkp6fFyoNq0JkPOKaktJM F5CEDEED08E9EA536034F5823475162385DF08AF'

[[ "$KNOWN_HOSTS" =~ $RE ]]
```

Next, we retract the link again, which means that we don't rely on this certificate as a trust root anymore.

Note that because the resolution of timestamps on OpenPGP signatures is limited to full seconds, we wait for two seconds to make sure the changed trust root configuration is in effect:

```sh
sleep 2
sq --force pki link retract D9E95D7F42E87610676C40B47E8432836DA1625E '<ssh-openpgp-auth@example.com>'
sleep 2
```

After unsetting this trust root, the SSH "known hosts" configuration is empty again, because the Web of Trust verification does not yield any valid SSH host certificates:

```sh
KNOWN_HOSTS=$(ssh-openpgp-auth authenticate --verify-wot example.com)

[ "$KNOWN_HOSTS" = "" ]
```

## Capturing verifications

Verification results can be captured by using the `authenticate` command with the `--store-verifications` option. With it verification results are added as OpenPGP certifications to the host's certificate:

```sh
sq cert import < fixtures/example.asc

sq pki link add --all D9E95D7F42E87610676C40B47E8432836DA1625E
sleep 2 # wait so that the binding becomes valid

ssh-openpgp-auth authenticate --verify-wot --store-verifications example.com
```

Note that the certifications created by this tool are local and non-exportable. They are only used to persist proof verification results for the machine which runs `ssh-openpgp-auth`.

The certification can be inspected by `sq` or `gpg`:

```sh
gpg --list-packets $PGP_CERT_D/d9/e95d7f42e87610676c40b47e8432836da1625e | grep -C 5 ssh-openpgp-auth-verification
```

Which outputs:

```
	version 4, created 1706631963, md5len 0, sigclass 0x13
	digest algo 10, begin of digest a3 9b
	critical hashed subpkt 2 len 4 (sig created 2024-01-30)
	critical hashed subpkt 4 len 1 (not exportable)
	hashed subpkt 16 len 8 (issuer key ID 07346F19049471F6)
	hashed subpkt 20 len 53 (notation: ssh-openpgp-auth-verification@metacode.biz=wot)
	hashed subpkt 20 len 70 (notation: salt@notations.sequoia-pgp.org=[not human readable])
	hashed subpkt 33 len 21 (issuer fpr v4 8C56EB2309B51FF2EF3621C407346F19049471F6)
	data: [256 bits]
	data: [256 bits]
```

The results are stored as a `ssh-openpgp-auth-verification@metacode.biz` notation where the value is a concatenation of the following values:

  - `dns` - DNS proof has been validated.
  - `wot` - Web of Trust verification succeeded.

## Funding

This project is funded through [NGI Assure](https://nlnet.nl/assure), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/OpenPGP-OpenSSH).

[<img src="https://nlnet.nl/logo/banner.png" alt="NLnet foundation logo" width="20%" />](https://nlnet.nl)
[<img src="https://nlnet.nl/image/logos/NGIAssure_tag.svg" alt="NGI Assure Logo" width="20%" />](https://nlnet.nl/assure)

## License

This project is licensed under either of:

  - [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0),
  - [MIT license](https://opensource.org/licenses/MIT).

at your option.

### Contribution

Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in this crate by you, as defined in the
Apache-2.0 license, shall be dual licensed as above, without any
additional terms or conditions.
