ref: bb2060028e616df560628cf6f1e6e5d196d50833
parent: 78cc69e9f24f9b3e256c29b73772eedafcb16792
author: ori@eigenstate.org <ori@eigenstate.org>
date: Thu Jun 23 20:08:34 EDT 2022
upas/dkim: dkim signing for upas This change adds support for dkim signing to upas. It has2 pieces: 1. Adding support for different asn1 formats to auth/rsa2asn1; we can now generate SubjectPublicKeyInfo RSA keys, which wrap the keys up with an algorithm identifier. 2. Adding a upas/dkim command which filters a message and signs it using dkim. To configure dkim, you need to generate a (small-ish) rsa key; large keys do not fit into DNS text records: # generate the private key and add it to factotum ramfs -p cd /tmp auth/rsagen -b 2048 -t 'service=dkim role=sign hash=sha256 domain=orib.dev owner=*' > dkim.key cat dkim.key > factotum.ctl # extract the public key, encode it, and strip out the junk pubkey=`{ <dkim.key auth/rsa2asn1 -f spki | \ auth/pemencode WHATEVER | \ grep -v 'WHATEVER' | \ ssam 'x/\n/d' } domain=example.org # then add it to /lib/ndb.local echo 'dom=dkim._domainkey.'$domain' soa= ip=144.202.1.203 refresh=600 ttl=600 ns=ns.orib.dev txt="k=rsa; v='$pubkey \ >> /lib/ndb/local Then, finally, insert it into your outgoing mail pipeline. One thing to be careful of is that upas will do some outgoing 'From:' rewriting, so you may need to make sure that either '$upasname' is set, or 'upas/dkim' is inserted after the rewrite stage. A good place is in /mail/lib/qmail, in place of upas/vf: % cat /mail/lib/qmail rfork s upas/dkim -d example.com | upas/qer /mail/queue mail $* || exit 'qer failed' upas/runq -n 10 /mail/queue /mail/lib/remotemail </dev/null >/dev/null >[2=1] &
--- a/sys/include/libsec.h
+++ b/sys/include/libsec.h
@@ -374,6 +374,7 @@
int pkcs1unpadbuf(uchar *buf, int len, mpint *modulus, int blocktype);
int asn1encodeRSApub(RSApub *pk, uchar *buf, int len);
int asn1encodeRSApriv(RSApriv *k, uchar *buf, int len);
+int asn1encodeRSApubSPKI(RSApub *pk, uchar *buf, int len);
int asn1encodedigest(DigestState* (*fun)(uchar*, ulong, uchar*, DigestState*),
uchar *digest, uchar *buf, int len);
--- a/sys/man/1/filter
+++ b/sys/man/1/filter
@@ -24,6 +24,15 @@
.I recipient
.I fromfile
.I mbox
+.B upas/dkim
+[
+.B -d
+.I domain
+]
+[
+.B -s
+.I selector
+]
.PP
.B upas/token
.I key
@@ -183,6 +192,23 @@
You should copy it into a directory that normally gets
bound by your profile onto
.BR /bin .
+.PP
+.I Dkim
+Takes a mail message as standard input, and signs
+a selection of headers and the body of the message.
+The
+.I -d
+flag specifies the domain,
+and the
+.I -s
+flag specifies the selector. If the selector is not
+specified, it defaults to
+.IR dkim .
+The keyspec searched for the signing key is:
+.IP
+.EX
+proto=rsa service=dkim role=sign hash=sha256 domain=$domain
+.EE
.PP
.I Vf
(virus filter)
--- a/sys/man/8/rsa
+++ b/sys/man/8/rsa
@@ -28,9 +28,13 @@
.PP
.B rsa2asn1
[
--a
+.B -a
]
[
+.B -f
+.I fmt
+]
+[
.I file
]
.PP
@@ -200,6 +204,16 @@
With the
.I -a
flag a private key is read and encoded in ANS.1/DER format.
+With the
+.I -f
+flag, the format of the ASN.1/DER encoded key is selected.
+The supported formats are
+.I pkcs1
+and
+.IR spki ,
+which refer to RFC3447 RSAPublicKey and RFC5280 SubjectPublicKeyInfo
+formatted RSA keys respectively.
+The default format is pkcs1.
.PP
.I Rsa2ssh
reads a Plan 9 RSA public or private key and prints the public portion
--- a/sys/src/cmd/auth/rsa2asn1.c
+++ b/sys/src/cmd/auth/rsa2asn1.c
@@ -6,11 +6,12 @@
#include "rsa2any.h"
int privatekey = 0;
+char *format = "pkcs1";
void
usage(void)
{
- fprint(2, "usage: auth/rsa2asn1 [-a] [file]\n");
+ fprint(2, "usage: auth/rsa2asn1 [-a] [-f fmt] [file]\n");
exits("usage");
}
@@ -25,6 +26,9 @@
case 'a':
privatekey = 1;
break;
+ case 'f':
+ format = EARGF(usage());
+ break;
default:
usage();
}ARGEND
@@ -32,14 +36,25 @@
if(argc > 1)
usage();
+ n = -1;
if((k = getrsakey(argc, argv, privatekey, nil)) == nil)
sysfatal("%r");
if(privatekey){
- if((n = asn1encodeRSApriv(k, buf, sizeof(buf))) < 0)
- sysfatal("asn1encodeRSApriv: %r");
+ if(strcmp(format, "pkcs1") == 0)
+ n = asn1encodeRSApriv(k, buf, sizeof(buf));
+ else
+ sysfatal("unknown format %s", format);
+ if(n < 0)
+ sysfatal("encode: %r");
}else{
- if((n = asn1encodeRSApub(&k->pub, buf, sizeof(buf))) < 0)
- sysfatal("asn1encodeRSApub: %r");
+ if(strcmp(format, "pkcs1") == 0)
+ n = asn1encodeRSApub(&k->pub, buf, sizeof(buf));
+ else if(strcmp(format, "spki") == 0)
+ n = asn1encodeRSApubSPKI(&k->pub, buf, sizeof(buf));
+ else
+ sysfatal("unknown format %s", format);
+ if(n < 0)
+ sysfatal("encode: %r");
}
if(write(1, buf, n) != n)
sysfatal("write: %r");
--- /dev/null
+++ b/sys/src/cmd/upas/dkim/dkim.c
@@ -1,0 +1,210 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <libsec.h>
+#include <auth.h>
+#include <authsrv.h>
+#include <pool.h>
+
+char *signhdr[] = {
+ "from:",
+ "to:",
+ "subject:",
+ "date:",
+ "message-id:",
+ nil
+};
+
+char *keyspec;
+char *domain;
+char *selector = "dkim";
+
+int
+trim(char *p)
+{
+ char *e;
+
+ for(e = p; *e != 0; e++)
+ if(*e == '\r' || *e == '\n')
+ break;
+ *e = 0;
+ return e - p;
+}
+
+int
+usehdr(char *ln, char **hs)
+{
+ char **p;
+
+ for(p = signhdr; *p; p++)
+ if(cistrncmp(ln, *p, strlen(*p)) == 0){
+ if((*hs = realloc(*hs, strlen(*hs) + strlen(*p) + 1)) == nil)
+ sysfatal("realloc: %r");
+ strcat(*hs, *p);
+ return 1;
+ }
+ return 0;
+}
+
+void
+append(char **m, int *nm, int *sz, char *ln, int n)
+{
+ while(*nm + n + 2 >= *sz){
+ *sz += *sz/2;
+ *m = realloc(*m, *sz);
+ }
+ memcpy(*m + *nm, ln, n);
+ memcpy(*m + *nm + n, "\r\n", 2);
+ *nm += n+2;
+}
+
+
+int
+sign(uchar *hash, int nhash, char **sig, int *nsig)
+{
+ AuthRpc *rpc;
+ int afd;
+
+ if((afd = open("/mnt/factotum/rpc", ORDWR|OCEXEC)) < 0)
+ return -1;
+ if((rpc = auth_allocrpc(afd)) == nil){
+ close(afd);
+ return -1;
+ }
+ if(auth_rpc(rpc, "start", keyspec, strlen(keyspec)) != ARok){
+ auth_freerpc(rpc);
+ close(afd);
+ return -1;
+ }
+
+ if(auth_rpc(rpc, "write", hash, nhash) != ARok)
+ sysfatal("sign: write hash: %r");
+ if(auth_rpc(rpc, "read", nil, 0) != ARok)
+ sysfatal("sign: read sig: %r");
+ if((*sig = malloc(rpc->narg)) == nil)
+ sysfatal("malloc: %r");
+ *nsig = rpc->narg;
+ memcpy(*sig, rpc->arg, *nsig);
+ auth_freerpc(rpc);
+ close(afd);
+ return 0;
+}
+
+void
+usage(void)
+{
+ fprint(2, "usage: %s [-s sel] -d dom\n", argv0);
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ int i, n, nhdr, nmsg, nsig, ntail, hdrsz, msgsz, use;
+ uchar hdrhash[SHA2_256dlen], msghash[SHA2_256dlen];
+ char *hdr, *msg, *sig, *ln, *hdrset, *dhdr;
+ Biobuf *rd, *wr;
+ DigestState *sh, *sb;
+
+ ARGBEGIN{
+ case 'd':
+ domain = EARGF(usage());
+ break;
+ case 's':
+ selector = EARGF(usage());
+ break;
+ default:
+ usage();
+ break;
+ }ARGEND;
+
+ if(domain == nil)
+ usage();
+ fmtinstall('H', encodefmt);
+ fmtinstall('[', encodefmt);
+ keyspec = smprint("proto=rsa service=dkim role=sign hash=sha256 domain=%s", domain);
+
+ rd = Bfdopen(0, OREAD);
+ wr = Bfdopen(1, OWRITE);
+
+ nhdr = 0;
+ hdrsz = 32;
+ if((hdr = malloc(hdrsz)) == nil)
+ sysfatal("malloc: %r");
+ nmsg = 0;
+ msgsz = 32;
+ if((msg = malloc(msgsz)) == nil)
+ sysfatal("malloc: %r");
+
+ use = 0;
+ sh = nil;
+ hdrset = strdup("");
+ while((ln = Brdstr(rd, '\n', 1)) != nil){
+ n = trim(ln);
+ if(n == 0
+ || (n == 1 && ln[0] == '\r' || ln[0] == '\n')
+ || (n == 2 && strcmp(ln, "\r\n") == 0))
+ break;
+ /*
+ * strip out existing DKIM signatures,
+ * for the sake of mailing lists and such.
+ */
+ if(cistrcmp(ln, "DKIM-Signature:") == 0)
+ continue;
+ if(ln[0] != ' ' && ln[0] != '\t')
+ use = usehdr(ln, &hdrset);
+ if(use){
+ sh = sha2_256((uchar*)ln, n, nil, sh);
+ sh = sha2_256((uchar*)"\r\n", 2, nil, sh);
+ }
+ append(&hdr, &nhdr, &hdrsz, ln, n);
+ }
+
+ sb = nil;
+ ntail = 0;
+ while((ln = Brdstr(rd, '\n', 0)) != nil){
+ n = trim(ln);
+ if(n == 0){
+ ntail++;
+ continue;
+ }
+ for(i = 0; i < ntail; i++){
+ sb = sha2_256((uchar*)"\r\n", 2, nil, sb);
+ append(&msg, &nmsg, &msgsz, "", 0);
+ ntail = 0;
+ }
+ sb = sha2_256((uchar*)ln, n, nil, sb);
+ sb = sha2_256((uchar*)"\r\n", 2, nil, sb);
+ append(&msg, &nmsg, &msgsz, ln, n);
+ }
+ if(nmsg == 0 || ntail > 1)
+ sb = sha2_256((uchar*)"\r\n", 2, nil, sb);
+ Bterm(rd);
+
+ sha2_256(nil, 0, msghash, sb);
+ dhdr = smprint(
+ "DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=%s;\r\n"
+ " h=%s; s=%s;\r\n"
+ " bh=%.*[; \r\n"
+ " b=",
+ domain, hdrset, selector,
+ (int)sizeof(msghash), msghash);
+ if(dhdr == nil)
+ sysfatal("smprint: %r");
+ sh = sha2_256((uchar*)dhdr, strlen(dhdr), nil, sh);
+ sha2_256(nil, 0, hdrhash, sh);
+ if(sign(hdrhash, sizeof(hdrhash), &sig, &nsig) == -1)
+ sysfatal("sign: %r");
+
+ Bwrite(wr, dhdr, strlen(dhdr));
+ Bprint(wr, "%.*[\r\n", nsig, sig);
+ Bwrite(wr, hdr, nhdr);
+ Bprint(wr, "\n");
+ Bwrite(wr, msg, nmsg);
+ Bterm(wr);
+
+ free(hdr);
+ free(msg);
+ free(sig);
+ exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/dkim/mkfile
@@ -1,0 +1,7 @@
+</$objtype/mkfile
+
+TARG=dkim
+OFILES=dkim.$O
+
+</sys/src/cmd/mkone
+<../mkupas
--- a/sys/src/cmd/upas/mkfile
+++ b/sys/src/cmd/upas/mkfile
@@ -6,6 +6,7 @@
alias\
bayes\
binscripts\
+ dkim\
filterkit\
fs\
imap4d\
--- a/sys/src/libsec/port/x509.c
+++ b/sys/src/libsec/port/x509.c
@@ -788,6 +788,7 @@
p = &uc;
err = enc(&p, e, 1);
+ *pbytes = nil;
if(err == ASN_OK) {
ans = newbytes(p-&uc);
p = ans->data;
@@ -2900,6 +2901,32 @@
}
memmove(buf, b->data, len = b->len);
freebytes(b);
+ return len;
+}
+
+int
+asn1encodeRSApubSPKI(RSApub *pk, uchar *buf, int len)
+{
+ Bytes *b, *k;
+ Elem e;
+
+ k = encode_rsapubkey(pk);
+ if(k == nil)
+ return -1;
+ e = mkseq(
+ mkel(mkalg(ALG_rsaEncryption),
+ mkel(mkbits(k->data, k->len),
+ nil)));
+ encode(e, &b);
+ freebytes(k);
+ if(b == nil)
+ return -1;
+ if(b->len > len){
+ freebytes(b);
+ werrstr("buffer too small");
+ return -1;
+ }
+ memmove(buf, b->data, len = b->len);
return len;
}