Re: [exim] DKIM ed25519 signing issues

Top Page
Delete this message
Reply to this message
Author: Phil Pennock
Date:  
To: Graham McAlister
CC: Exim-users
Subject: Re: [exim] DKIM ed25519 signing issues
On 2020-03-04 at 09:06 +0000, Graham McAlister via Exim-users wrote:
> Has anyone successfully used Exim and DKIM with ed25519 keys? Any pointers?


Yes. I dual-sign. It's amusing to see all the status reports from
systems which don't implement Ed25519. At least most of them now will
accept seeing the RSA signature and accept it as good enough, so ignore
the failure from the Ed25519 side.

I've attached `/etc/exim/dk/Makefile` from my own mailhub; there's other
process flow to get the secrets safely stashed away, etc. A couple of
hard-coded paths you'll need to update: I doubt that
`OPENSSL:=/opt/openssl-1.1.1/bin/openssl` works for you.

Oh, it's a BSD Makefile. I don't think GNU make likes the `.for`
syntax. So unless you're on a BSD, this will at best provide you with a
crib source.

Running `make` will make the new RSA and Ed25519 keys for "this month".
I use dYYYYMM as the selector for RSA and dYYYYMMe2 as the selector for
Ed25519.

`make list-current` gets me the exact input I use for my DKIM selector
lookup input (it actually makes a CDB from this).

The actual key generation is done with:

    ( umask 027 && $(OPENSSL) genpkey -algorithm ed25519 > ${.TARGET} )


Git history of the secrets archive shows that I have been running with
this since making the d201804 keys. It's been working fine in all that
time.

-Phil
.MAIN: all

# http://www.keylength.com/en/3/ -- ECRYPT II "smallest general purpose level"
# is 1248 bits symmetric
# Switching 2018-04-11 from 1248 to 2048, hope to switch to EC by end of 2020.
KEYSIZE_RSA=2048
ED25519:=true

# Path to an OpenSSL binary with support for Ed25519
.ifdef ED25519
OPENSSL:=/opt/openssl-1.1.1/bin/openssl
.else
OPENSSL:=openssl
.endif

DKIM_SIGN_DOMAINS_FILE:=/etc/mail/flat/dkim.selectors.hermes

DOMAINS!=sed -n 's/^\([^\#%][^:]*\):.*$$/\1/p' < $(DKIM_SIGN_DOMAINS_FILE)
DATE!=date +%Y%m

ALL_KEY_FILES:=
DNS_FRAGMENTS:=

.for domain in $(DOMAINS)
_DOMAIN= ${domain}

# Note:
# RFC 6376 paragraph 3.6.2.2:
#                             TXT RRs MUST be unique for a particular
#    selector name; that is, if there are multiple records in an RRset,
#    the results are undefined.
#
# Thus we use 'e2' as a suffix for ed25519 keys.
_KEYFILE_RSA= rsa.private.d$(DATE).$(_DOMAIN)
_KEYFILE_ED25519= ed25519.private.d$(DATE)e2.$(_DOMAIN)
_DNSFRAGMENT_RSA= _rsa.dns.d$(DATE).$(_DOMAIN)
_DNSFRAGMENT_ED25519= _ed25519.dns.d$(DATE)e2.$(_DOMAIN)
_DNS_COMBINED= dns.d$(DATE).$(_DOMAIN)


# Beware that modern OpenSSL does not honor umask and uses 0600 permissions as
# the base for -out, so we write to stdout and redirect instead.

$(_KEYFILE_RSA):
    ( umask 027 && $(OPENSSL) genrsa $(KEYSIZE_RSA) > ${.TARGET} )


$(_KEYFILE_ED25519):
    ( umask 027 && $(OPENSSL) genpkey -algorithm ed25519 > ${.TARGET} )


# These two yield identical outputs ... but I feel a little better about the first, not hard-coding
# the magic number of 13 (jumping first 12 of ASN.1)
#
# /opt/openssl-1.1.1/bin/openssl pkey -in ed25519.private.d201804.spodhuis.org -noout -text_pub | perl -ne 'next unless /^\s/; chomp; s/^\s+//; @a = split(/:/, $_); push @results, chr(hex($_)) foreach @a; END { print @results }' | openssl enc -a
# /opt/openssl-1.1.1/bin/openssl pkey -outform DER -pubout -in ed25519.private.d201804.spodhuis.org | tail -c +13 | openssl enc -a

$(_DNSFRAGMENT_RSA): $(_KEYFILE_RSA)
    $(OPENSSL) rsa -in ${.ALLSRC} -outform PEM -pubout 2>/dev/null | \
      perl -lne 'next unless /^-----BEGIN/../-----END/; next if /^-----/; chomp; $$str.=$$_; \
      END {$$str = qq!"v=DKIM1; k=rsa; p=$$str"!; $$str =~ s/(.{250})/\1" "/; \
      print qq!d$(DATE)._domainkey IN TXT $$str!}' \
      > ${.TARGET}


$(_DNSFRAGMENT_ED25519): $(_KEYFILE_ED25519)
    $(OPENSSL) pkey -in ${.ALLSRC} -noout -text_pub | \
      perl -MMIME::Base64 -lne \
        'next unless /^\s/; chomp; s/^\s+//; @a = split(/:/, $$_); push @results, chr(hex($$_)) foreach @a; \
        END { $$str = encode_base64(join("", @results), ""); \
              $$str = qq!"v=DKIM1; k=ed25519; p=$$str"!; \
              $$str =~ s/(.{250})/\1" "/; \
              print qq!d$(DATE)e2._domainkey IN TXT $$str! \
        }' > ${.TARGET}


$(_DNS_COMBINED): $(_DNSFRAGMENT_RSA)
.ifdef ED25519
$(_DNS_COMBINED): $(_DNSFRAGMENT_ED25519)
.endif

$(_DNS_COMBINED):
    cat ${.ALLSRC} > ${.TARGET}


DNS_FRAGMENTS:=$(DNS_FRAGMENTS) $(_DNS_COMBINED)
DNS_FRAGMENTS:=$(DNS_FRAGMENTS) $(_DNSFRAGMENT_RSA)
.ifdef ED25519
DNS_FRAGMENTS:=$(DNS_FRAGMENTS) $(_DNSFRAGMENT_ED25519)
.endif

ALL_KEY_FILES:=$(ALL_KEY_FILES) $(_KEYFILE_RSA)
.ifdef ED25519
ALL_KEY_FILES:=$(ALL_KEY_FILES) $(_KEYFILE_ED25519)
.endif

domain.${domain}: $(_DNS_COMBINED) $(_KEYFILE_RSA)
.ifdef ED25519
domain.${domain}: $(_DNSFRAGMENT_ED25519)
.endif

.endfor

all: $(ALL_KEY_FILES) $(DNS_FRAGMENTS)

# Easy enough to regenerate the DNS and we don't want it littering the dir
# We don't want to remove the keys.  Those are precious.
clean:
    rm -f $(DNS_FRAGMENTS)


list-current:
    @ls -1 | grep -F .private.d$(DATE) | perl -F\\. -lane 'print qq!@{[join(".",@F[3..$$#F])]}: $$F[2]=$$F[0]!' | perl -ne 'if (/([a-zA-Z0-9-.]+)/) { push @hosts, [join(".", reverse(split(/\./, $$1))), $$_] } else { push @hosts, ["", $$_]}; END { @hosts = sort {$$a->[0] cmp $$b->[0] || $$a->[1] cmp $$b->[1] } @hosts; print $$_->[1] foreach (@hosts)}' | perl -ape 'if ($$F[0] eq $$prev) {$$_ = join(" ", "", @F[1..$$#F]) } else {chomp; print "\n"}; $$prev = $$F[0]; END {print "\n"}' | column -t