OPENPGPKEY records are cool

OPENPGPKEY are a nice feature of modern DNS for email encryption and authentication. Let's see what talk there is about it!

Correction: I have reinterpreted the RFC, and I figured out my original interpretation of it was wrong (I interpreted "octets" as hex digest octets, rather than binary hex hash digest octets on accident). I have done this in a short period (around an hour) and I have republished the corrected version.

# OPENPGPKEY DNS record

I just found out about OPENPGPKEY records in modern DNS while exploring the deSEC homepage. I logged into my deSEC account and there it was - OPENPGPKEY DNS record type.

Well, I wondered - what is it, and how can I set it up.

Which led me to multiple articles and generators, which then led me to the RFC document at https://www.rfc-editor.org/rfc/rfc7929.txt.

I began hand-crafting my own OPENPGPKEY record generator based off the RFC.

# OPENPGPKEY record generator

I read the intro of the aforementioned RFC until I found this:

  1. Location of the OPENPGPKEY Record

The DNS does not allow the use of all characters that are supported in the "local-part" of email addresses as defined in [RFC5322] and [RFC6530]. Therefore, email addresses are mapped into DNS using the following method:

  1. The "left-hand side" of the email address, called the "local- part" in both the mail message format definition [RFC5322] and in the specification for internationalized email [RFC6530]) is encoded in UTF-8 (or its subset ASCII). If the local-part is written in another charset, it MUST be converted to UTF-8.

  2. The local-part is first canonicalized using the following rules. If the local-part is unquoted, any comments and/or folding whitespace (CFWS) around dots (".") is removed. Any enclosing double quotes are removed. Any literal quoting is removed.

  3. If the local-part contains any non-ASCII characters, it SHOULD be normalized using the Unicode Normalization Form C from [Unicode90]. Recommended normalization rules can be found in Section 10.1 of [RFC6530].

  4. The local-part is hashed using the SHA2-256 [RFC5754] algorithm, with the hash truncated to 28 octets and represented in its hexadecimal representation, to become the left-most label in the prepared domain name.

  5. The string "_openpgpkey" becomes the second left-most label in the prepared domain name.

  6. The domain name (the "right-hand side" of the email address, called the "domain" in [RFC5322]) is appended to the result of step 2 to complete the prepared domain name.

For example, to request an OPENPGPKEY resource record for a user whose email address is "hugh@example.com", an OPENPGPKEY query would be placed for the following QNAME: "c93f1e400f26708f98cb19d936620da35 eec8f72e57f9eec01c1afd6._openpgpkey.example.com". The corresponding RR in the example.com zone might look like (key shortened for formatting):

c9[..]d6._openpgpkey.example.com. IN OPENPGPKEY <base64 public key>

Based off this part alone pretty much, I made a shell script that generated a valid output! It is located at https://gist.github.com/TruncatedDinoSour/a0874bf1e90647a9a49985e531d9d15f licensed under the public domain (CC0) and it looks like this:

#!/usr/bin/env sh

...

set -eu

main() {
    if [ "$#" -ne 2 ]; then
        {
            echo "Generates an OPENPGPKEY DNS record based off your Email and a Public GPG key ID."
            echo "Usage: $0 <email> <GPG key ID>"
        } >&2
        return 1
    fi

    # Confirm user localpart

    localpart="$(printf -- '%s' "$1" | cut -d'@' -f1 | tr '[:upper:]' '[:lower:]')"

    printf -- " * Your email username (localpart) is '\033[1m%s\033[0m', correct? In lowercase, enter either 'y' (for yes) or 'n' (for no): " "$localpart"
    read -r yn
    if [ "$yn" -ne 'y' ]; then
        echo " * Incorrect information provided." >&2
        return 1
    fi

    # Localpart digest is the SHA256 hex digest truncated to 28 octets (which is 56 hex bytes 0x??)
    localpart_digest="$(printf -- '%s' "$localpart" | sha256sum | cut -d' ' -f1 | cut -c1-56)"

    # And the value has to be the base64-encoded public key, exported as binary
    gpg_public_key_b64="$(gpg --export --export-options export-minimal,no-export-attributes -- "$2" | base64 -w 0)"

    printf '\n\033[32m%s\033[0m._openpgpkey. \033[90mIN OPENPGPKEY\033[0m \033[1m%s\033[0m\n' "$localpart_digest" "$gpg_public_key_b64"
}

main "$@"

(... just signifies the cut out status message, author, and license)

The main functionality is located in two lines of code:

    # Localpart digest is the SHA256 hex digest truncated to 28 octets (which is 56 hex bytes 0x??)
    localpart_digest="$(printf -- '%s' "$localpart" | sha256sum | cut -d' ' -f1 | cut -c1-28)"

    # And the value has to be the base64-encoded public key, exported as binary
    gpg_public_key_b64="$(gpg --export --export-options export-minimal,no-export-attributes -- "$2" | base64 -w 0)"

localpart_digest SHA-256 hex digest of the localpart (username) of an email address (for example hi@ari.lt becomes just 'hi'). As per the RFC point 3.4 it is truncated to 28 octets (or 56 hex bytes), for which we use the cut utility.

gpg_public_key_b64 is the public key encoded in base64, pretty self-explanatory. It exports solely the key by using GPG options, ignoring all attributes and only exporting the essential parts of the key, and then encoding it in a single-line of base64.

After which, we just print out the DNS record in a pretty way :D

<SHA-256 hex hash of the lowercase localpart truncated to 56 characters>._openpgpkey. IN OPENPGPKEY <base64-encoded public GPG/OpenPGP key>

And now, you can even verify my key this way by checking the OPENPGPKEY record on d2efaa6dd6ae6136c19944fae329efd3fb2babe1e6eec26982a422aa._openpgpkey.ari.lt - this is for ari@ari.lt :)

Peace ✌