1760 lines
42 KiB
C
1760 lines
42 KiB
C
/*
|
|
* Copyright (c) 2022 Martijn van Duren <martijn@openbsd.org>
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
|
|
#include <openssl/evp.h>
|
|
#include <openssl/pem.h>
|
|
#include <openssl/sha.h>
|
|
#include <openssl/err.h>
|
|
|
|
#include <arpa/nameser.h>
|
|
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <event.h>
|
|
#include <limits.h>
|
|
#include <netdb.h>
|
|
#include <opensmtpd.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <asr.h>
|
|
|
|
#include "unpack_dns.h"
|
|
#include "ltok.h"
|
|
|
|
/*
|
|
* Use RFC8601 (Authentication-Results) codes instead of RFC6376 codes,
|
|
* since they're more expressive.
|
|
*/
|
|
enum state {
|
|
DKIM_UNKNOWN,
|
|
DKIM_PASS,
|
|
DKIM_FAIL,
|
|
DKIM_POLICY,
|
|
DKIM_NEUTRAL,
|
|
DKIM_TEMPERROR,
|
|
DKIM_PERMERROR
|
|
};
|
|
|
|
struct signature {
|
|
struct header *header;
|
|
enum state state;
|
|
const char *state_reason;
|
|
int v;
|
|
const char *a;
|
|
size_t asz;
|
|
int ak;
|
|
int sephash;
|
|
const EVP_MD *ah;
|
|
char *b;
|
|
size_t bsz;
|
|
const char *bheader;
|
|
/* Make sure padding bits for base64 decoding fit */
|
|
char bh[EVP_MAX_MD_SIZE + (3 - (EVP_MAX_MD_SIZE % 3))];
|
|
size_t bhsz;
|
|
EVP_MD_CTX *bhctx;
|
|
int c;
|
|
#define CANON_HEADER_SIMPLE 0
|
|
#define CANON_HEADER_RELAXED 1
|
|
#define CANON_HEADER 1
|
|
#define CANON_BODY_SIMPLE 0
|
|
#define CANON_BODY_RELAXED 1 << 1
|
|
#define CANON_BODY 1 << 1
|
|
#define CANON_DONE 1 << 2
|
|
char d[HOST_NAME_MAX + 1];
|
|
char **h;
|
|
const char *i;
|
|
size_t isz;
|
|
ssize_t l;
|
|
int q;
|
|
char s[HOST_NAME_MAX + 1];
|
|
time_t t; /* Signature t=/timestamp */
|
|
#define KT_Y 1
|
|
#define KT_S 1 << 1
|
|
int kt; /* Key t=/Flags */
|
|
time_t x;
|
|
int z;
|
|
struct event_asr *query;
|
|
EVP_PKEY *p;
|
|
};
|
|
|
|
struct header {
|
|
struct message *msg;
|
|
uint8_t readdone;
|
|
uint8_t parsed;
|
|
char *buf;
|
|
size_t buflen;
|
|
struct signature *sig;
|
|
};
|
|
|
|
#define AUTHENTICATION_RESULTS_LINELEN 78
|
|
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
|
|
|
struct message {
|
|
struct osmtpd_ctx *ctx;
|
|
FILE *origf;
|
|
int parsing_headers;
|
|
size_t body_whitelines;
|
|
int has_body;
|
|
struct header *header;
|
|
size_t nheaders;
|
|
int err;
|
|
int readdone;
|
|
};
|
|
|
|
void usage(void);
|
|
void dkim_err(struct message *, char *);
|
|
void dkim_errx(struct message *, char *);
|
|
void dkim_conf(const char *, const char *);
|
|
void dkim_dataline(struct osmtpd_ctx *, const char *);
|
|
void dkim_commit(struct osmtpd_ctx *);
|
|
void *dkim_message_new(struct osmtpd_ctx *);
|
|
void dkim_message_free(struct osmtpd_ctx *, void *);
|
|
void dkim_header_add(struct osmtpd_ctx *, const char *);
|
|
void dkim_signature_parse(struct header *);
|
|
void dkim_signature_parse_v(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_a(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_b(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_bh(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_c(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_d(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_h(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_i(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_l(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_q(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_s(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_t(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_x(struct signature *, const char *, const char *);
|
|
void dkim_signature_parse_z(struct signature *, const char *, const char *);
|
|
void dkim_signature_verify(struct signature *);
|
|
void dkim_signature_header(EVP_MD_CTX *, struct signature *, struct header *);
|
|
void dkim_signature_state(struct signature *, enum state, const char *);
|
|
const char *dkim_state2str(enum state);
|
|
void dkim_header_cat(struct osmtpd_ctx *, const char *);
|
|
void dkim_body_parse(struct message *, const char *);
|
|
void dkim_body_verify(struct signature *);
|
|
void dkim_rr_resolve(struct asr_result *, void *);
|
|
void dkim_message_verify(struct message *);
|
|
ssize_t dkim_ar_cat(char **ar, size_t *n, size_t aroff, const char *fmt, ...)
|
|
__attribute__((__format__ (printf, 4, 5)));
|
|
void dkim_ar_print(struct osmtpd_ctx *, const char *);
|
|
int dkim_key_text_parse(struct signature *, const char *);
|
|
|
|
char *authservid;
|
|
EVP_ENCODE_CTX *ectx = NULL;
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
if (argc != 1)
|
|
osmtpd_errx(1, "Invalid argument count");
|
|
|
|
OpenSSL_add_all_digests();
|
|
|
|
if (pledge("tmppath stdio dns", NULL) == -1)
|
|
osmtpd_err(1, "pledge");
|
|
|
|
if ((ectx = EVP_ENCODE_CTX_new()) == NULL)
|
|
osmtpd_err(1, "EVP_ENCODE_CTX_new");
|
|
|
|
osmtpd_register_conf(dkim_conf);
|
|
osmtpd_register_filter_dataline(dkim_dataline);
|
|
osmtpd_register_filter_commit(dkim_commit);
|
|
osmtpd_local_message(dkim_message_new, dkim_message_free);
|
|
osmtpd_run();
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
dkim_conf(const char *key, const char *value)
|
|
{
|
|
const char *end;
|
|
|
|
if (key == NULL) {
|
|
if (authservid == NULL)
|
|
osmtpd_errx(1, "Didn't receive admd config option");
|
|
return;
|
|
}
|
|
if (strcmp(key, "admd") == 0 && authservid == NULL) {
|
|
if ((authservid = strdup(value)) == NULL)
|
|
osmtpd_err(1, "malloc");
|
|
end = osmtpd_ltok_skip_value(authservid, 0);
|
|
if (authservid + strlen(authservid) != end)
|
|
osmtpd_errx(1, "Invalid authservid");
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_dataline(struct osmtpd_ctx *ctx, const char *line)
|
|
{
|
|
struct message *msg = ctx->local_message;
|
|
size_t i;
|
|
|
|
if (msg->err) {
|
|
if (line[0] == '.' && line[1] =='\0') {
|
|
msg->readdone = 1;
|
|
osmtpd_filter_dataline(ctx, ".");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (fprintf(msg->origf, "%s\n", line) < 0) {
|
|
dkim_err(msg, "Couldn't write to tempfile");
|
|
return;
|
|
}
|
|
if (line[0] == '.') {
|
|
line++;
|
|
if (line[0] == '\0') {
|
|
msg->readdone = 1;
|
|
for (i = 0; i < msg->nheaders; i++) {
|
|
if (msg->header[i].sig == NULL)
|
|
continue;
|
|
dkim_body_verify(msg->header[i].sig);
|
|
}
|
|
dkim_message_verify(msg);
|
|
return;
|
|
}
|
|
}
|
|
if (msg->parsing_headers) {
|
|
dkim_header_add(ctx, line);
|
|
if (line[0] == '\0') {
|
|
msg->parsing_headers = 0;
|
|
for (i = 0; i < msg->nheaders; i++) {
|
|
if (msg->header[i].sig == NULL)
|
|
continue;
|
|
if (msg->header[i].sig->query == NULL)
|
|
dkim_signature_verify(
|
|
msg->header[i].sig);
|
|
}
|
|
}
|
|
return;
|
|
} else {
|
|
dkim_body_parse(msg, line);
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_commit(struct osmtpd_ctx *ctx)
|
|
{
|
|
struct message *msg = ctx->local_message;
|
|
|
|
if (msg->err)
|
|
osmtpd_filter_disconnect(ctx, "Internal server error");
|
|
else
|
|
osmtpd_filter_proceed(ctx);
|
|
}
|
|
|
|
void *
|
|
dkim_message_new(struct osmtpd_ctx *ctx)
|
|
{
|
|
struct message *msg;
|
|
|
|
if ((msg = malloc(sizeof(*msg))) == NULL)
|
|
osmtpd_err(1, NULL);
|
|
|
|
if ((msg->origf = tmpfile()) == NULL) {
|
|
dkim_err(msg, "Can't open tempfile");
|
|
return NULL;
|
|
}
|
|
msg->ctx = ctx;
|
|
msg->parsing_headers = 1;
|
|
msg->body_whitelines = 0;
|
|
msg->has_body = 0;
|
|
msg->header = NULL;
|
|
msg->nheaders = 0;
|
|
msg->err = 0;
|
|
msg->readdone = 0;
|
|
|
|
return msg;
|
|
}
|
|
|
|
void
|
|
dkim_message_free(struct osmtpd_ctx *ctx, void *data)
|
|
{
|
|
struct message *msg = data;
|
|
size_t i, j;
|
|
|
|
fclose(msg->origf);
|
|
for (i = 0; i < msg->nheaders; i++) {
|
|
if (msg->header[i].sig != NULL) {
|
|
free(msg->header[i].sig->b);
|
|
EVP_MD_CTX_free(msg->header[i].sig->bhctx);
|
|
for (j = 0; msg->header[i].sig->h != NULL &&
|
|
msg->header[i].sig->h[j] != NULL; j++)
|
|
free(msg->header[i].sig->h[j]);
|
|
free(msg->header[i].sig->h);
|
|
EVP_PKEY_free(msg->header[i].sig->p);
|
|
}
|
|
free(msg->header[i].buf);
|
|
free(msg->header[i].sig);
|
|
}
|
|
free(msg->header);
|
|
free(msg);
|
|
}
|
|
|
|
void
|
|
dkim_header_add(struct osmtpd_ctx *ctx, const char *line)
|
|
{
|
|
struct message *msg = ctx->local_message;
|
|
const char *start, *end, *verify;
|
|
struct header *headers;
|
|
size_t i;
|
|
|
|
if (msg->nheaders > 0 &&
|
|
msg->header[msg->nheaders - 1].readdone == 0) {
|
|
if (line[0] != ' ' && line[0] != '\t') {
|
|
msg->header[msg->nheaders - 1].readdone = 1;
|
|
start = msg->header[msg->nheaders - 1].buf;
|
|
end = osmtpd_ltok_skip_field_name(start, 0);
|
|
/* In case someone uses an obs-optional */
|
|
if (end != NULL)
|
|
verify = osmtpd_ltok_skip_wsp(end, 1);
|
|
if (end != NULL &&
|
|
strncasecmp(
|
|
start, "DKIM-Signature", end - start) == 0 &&
|
|
verify[0] == ':')
|
|
dkim_signature_parse(
|
|
&msg->header[msg->nheaders - 1]);
|
|
if (line[0] == '\0')
|
|
return;
|
|
} else {
|
|
dkim_header_cat(ctx, line);
|
|
return;
|
|
}
|
|
}
|
|
if (msg->nheaders % 10 == 0) {
|
|
if ((headers = recallocarray(msg->header, msg->nheaders,
|
|
msg->nheaders + 10, sizeof(*msg->header))) == NULL) {
|
|
dkim_err(msg, "malloc");
|
|
return;
|
|
}
|
|
msg->header = headers;
|
|
for (i = 0; i < msg->nheaders; i++) {
|
|
if (msg->header[i].sig == NULL)
|
|
continue;
|
|
msg->header[i].sig->header = &msg->header[i];
|
|
}
|
|
}
|
|
msg->header[msg->nheaders].msg = msg;
|
|
msg->nheaders++;
|
|
dkim_header_cat(ctx, line);
|
|
}
|
|
|
|
void
|
|
dkim_header_cat(struct osmtpd_ctx *ctx, const char *line)
|
|
{
|
|
struct message *msg = ctx->local_message;
|
|
struct header *header = &msg->header[msg->nheaders - 1];
|
|
char *buf;
|
|
|
|
size_t needed = header->buflen + strlen(line) + 2;
|
|
|
|
if (needed > (header->buflen / 1024) + 1) {
|
|
buf = reallocarray(header->buf, (needed / 1024) + 1, 1024);
|
|
if (buf == NULL) {
|
|
dkim_err(msg, "malloc");
|
|
return;
|
|
}
|
|
header->buf = buf;
|
|
}
|
|
header->buflen += snprintf(header->buf + header->buflen,
|
|
(((needed / 1024) + 1) * 1024) - header->buflen, "%s%s",
|
|
header->buflen == 0 ? "" : "\r\n", line);
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse(struct header *header)
|
|
{
|
|
struct signature *sig;
|
|
struct asr_query *query;
|
|
const char *buf, *i, *end;
|
|
char tagname[3];
|
|
char subdomain[HOST_NAME_MAX + 1];
|
|
size_t ilen, dlen;
|
|
|
|
/* Format checked by dkim_header_add */
|
|
buf = osmtpd_ltok_skip_field_name(header->buf, 0);
|
|
buf = osmtpd_ltok_skip_wsp(buf, 1) + 1;
|
|
|
|
if ((header->sig = calloc(1, sizeof(*header->sig))) == NULL) {
|
|
dkim_err(header->msg, "malloc");
|
|
return;
|
|
}
|
|
sig = header->sig;
|
|
sig->header = header;
|
|
sig->l = -1;
|
|
sig->t = -1;
|
|
sig->x = -1;
|
|
|
|
end = osmtpd_ltok_skip_tag_list(buf, 0);
|
|
if (end == NULL || end[0] != '\0') {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid tag-list");
|
|
return;
|
|
}
|
|
|
|
while (buf[0] != '\0') {
|
|
buf = osmtpd_ltok_skip_fws(buf, 1);
|
|
end = osmtpd_ltok_skip_tag_name(buf, 0);
|
|
|
|
/* Unknown tag-name */
|
|
if ((size_t)(end - buf) >= sizeof(tagname))
|
|
tagname[0] = '\0';
|
|
else
|
|
strlcpy(tagname, buf, (end - buf) + 1);
|
|
buf = osmtpd_ltok_skip_fws(end, 1);
|
|
/* '=' */
|
|
buf = osmtpd_ltok_skip_fws(buf + 1, 1);
|
|
end = osmtpd_ltok_skip_tag_value(buf, 1);
|
|
if (strcmp(tagname, "v") == 0)
|
|
dkim_signature_parse_v(sig, buf, end);
|
|
else if (strcmp(tagname, "a") == 0)
|
|
dkim_signature_parse_a(sig, buf, end);
|
|
else if (strcmp(tagname, "b") == 0)
|
|
dkim_signature_parse_b(sig, buf, end);
|
|
else if (strcmp(tagname, "bh") == 0)
|
|
dkim_signature_parse_bh(sig, buf, end);
|
|
else if (strcmp(tagname, "c") == 0)
|
|
dkim_signature_parse_c(sig, buf, end);
|
|
else if (strcmp(tagname, "d") == 0)
|
|
dkim_signature_parse_d(sig, buf, end);
|
|
else if (strcmp(tagname, "h") == 0)
|
|
dkim_signature_parse_h(sig, buf, end);
|
|
else if (strcmp(tagname, "i") == 0)
|
|
dkim_signature_parse_i(sig, buf, end);
|
|
else if (strcmp(tagname, "l") == 0)
|
|
dkim_signature_parse_l(sig, buf, end);
|
|
else if (strcmp(tagname, "q") == 0)
|
|
dkim_signature_parse_q(sig, buf, end);
|
|
else if (strcmp(tagname, "s") == 0)
|
|
dkim_signature_parse_s(sig, buf, end);
|
|
else if (strcmp(tagname, "t") == 0)
|
|
dkim_signature_parse_t(sig, buf, end);
|
|
else if (strcmp(tagname, "x") == 0)
|
|
dkim_signature_parse_x(sig, buf, end);
|
|
else if (strcmp(tagname, "z") == 0)
|
|
dkim_signature_parse_z(sig, buf, end);
|
|
|
|
buf = osmtpd_ltok_skip_fws(end, 1);
|
|
if (buf[0] == ';')
|
|
buf++;
|
|
else if (buf[0] != '\0') {
|
|
dkim_signature_state(sig, DKIM_PERMERROR,
|
|
"Invalid tag-list");
|
|
return;
|
|
}
|
|
}
|
|
if (sig->state != DKIM_UNKNOWN)
|
|
return;
|
|
|
|
if (sig->v != 1)
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Missing v tag");
|
|
else if (sig->ah == NULL)
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Missing a tag");
|
|
else if (sig->b == NULL)
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Missing b tag");
|
|
else if (sig->bhsz == 0)
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Missing bh tag");
|
|
else if (sig->d[0] == '\0')
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Missing d tag");
|
|
else if (sig->h == NULL)
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Missing h tag");
|
|
else if (sig->s[0] == '\0')
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Missing s tag");
|
|
if (sig->state != DKIM_UNKNOWN)
|
|
return;
|
|
|
|
if (sig->i != NULL) {
|
|
i = osmtpd_ltok_skip_local_part(sig->i, 1) + 1;
|
|
ilen = sig->isz - (size_t)(i - sig->i);
|
|
dlen = strlen(sig->d);
|
|
if (ilen < dlen) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR,
|
|
"i tag not subdomain of d");
|
|
return;
|
|
}
|
|
i += ilen - dlen;
|
|
if ((i[-1] != '.' && i[-1] != '@') ||
|
|
strncasecmp(i, sig->d, dlen) != 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR,
|
|
"i tag not subdomain of d");
|
|
return;
|
|
}
|
|
}
|
|
if (sig->t != -1 && sig->x != -1 && sig->t > sig->x) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "t tag after x tag");
|
|
return;
|
|
}
|
|
|
|
if ((size_t)snprintf(subdomain, sizeof(subdomain), "%s._domainkey.%s",
|
|
sig->s, sig->d) >= sizeof(subdomain)) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR,
|
|
"dns/txt query too long");
|
|
return;
|
|
}
|
|
|
|
if ((query = res_query_async(subdomain, C_IN, T_TXT, NULL)) == NULL) {
|
|
dkim_err(header->msg, "res_query_async");
|
|
return;
|
|
}
|
|
if ((sig->query = event_asr_run(query, dkim_rr_resolve, sig)) == NULL) {
|
|
dkim_err(header->msg, "event_asr_run");
|
|
asr_abort(query);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_v(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
if (sig->v != 0) { /* Duplicate tag */
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate v tag");
|
|
return;
|
|
}
|
|
/* Unsupported version */
|
|
if (start[0] != '1' || start + 1 != end)
|
|
dkim_signature_state(sig, DKIM_NEUTRAL, "Unsupported v tag");
|
|
else
|
|
sig->v = 1;
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_a(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
char ah[sizeof("sha256")];
|
|
|
|
if (sig->ah != NULL) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate a tag");
|
|
return;
|
|
}
|
|
|
|
if (osmtpd_ltok_skip_sig_a_tag_alg(start, 0) != end) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid a tag");
|
|
return;
|
|
}
|
|
sig->a = start;
|
|
sig->asz = (size_t)(end - start);
|
|
if (strncmp(start, "rsa-", 4) == 0) {
|
|
start += 4;
|
|
sig->ak = EVP_PKEY_RSA;
|
|
sig->sephash = 0;
|
|
#if HAVE_ED25519
|
|
} else if (strncmp(start, "ed25519-", 8) == 0) {
|
|
start += 8;
|
|
sig->ak = EVP_PKEY_ED25519;
|
|
sig->sephash = 1;
|
|
#endif
|
|
} else {
|
|
dkim_signature_state(sig, DKIM_NEUTRAL, "Unsuppored a tag k");
|
|
return;
|
|
}
|
|
if ((size_t)(end - start) >= sizeof(ah)) {
|
|
dkim_signature_state(sig, DKIM_NEUTRAL, "Unsuppored a tag h");
|
|
return;
|
|
}
|
|
strlcpy(ah, start, sizeof(ah));
|
|
ah[end - start] = '\0';
|
|
if ((sig->ah = EVP_get_digestbyname(ah)) == NULL) {
|
|
dkim_signature_state(sig, DKIM_NEUTRAL, "Unsuppored a tag h");
|
|
return;
|
|
}
|
|
if ((sig->bhctx = EVP_MD_CTX_new()) == NULL) {
|
|
dkim_err(sig->header->msg, "EVP_MD_CTX_new");
|
|
return;
|
|
}
|
|
if (EVP_DigestInit_ex(sig->bhctx, sig->ah, NULL) <= 0) {
|
|
dkim_err(sig->header->msg, "EVP_DigestInit_ex");
|
|
return;
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_b(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
int decodesz;
|
|
|
|
if (sig->b != NULL) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate b tag");
|
|
return;
|
|
}
|
|
sig->bheader = start;
|
|
if ((sig->b = malloc((((end - start) / 4) + 1) * 3)) == NULL) {
|
|
dkim_err(sig->header->msg, "malloc");
|
|
return;
|
|
}
|
|
/* EVP_DecodeBlock doesn't handle internal whitespace */
|
|
EVP_DecodeInit(ectx);
|
|
if (EVP_DecodeUpdate(ectx, sig->b, &decodesz, start,
|
|
(int)(end - start)) == -1) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid b tag");
|
|
return;
|
|
}
|
|
sig->bsz = decodesz;
|
|
if (EVP_DecodeFinal(ectx, sig->b + sig->bsz,
|
|
&decodesz) == -1) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid b tag");
|
|
return;
|
|
}
|
|
sig->bsz += decodesz;
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_bh(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
const char *b64;
|
|
size_t n;
|
|
int decodesz;
|
|
|
|
if (sig->bhsz != 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate bh tag");
|
|
return;
|
|
}
|
|
/*
|
|
* EVP_Decode* expects sig->bh to be large enough,
|
|
* so count the actual b64 characters.
|
|
*/
|
|
b64 = start;
|
|
n = 0;
|
|
while (1) {
|
|
b64 = osmtpd_ltok_skip_fws(b64, 1);
|
|
if (osmtpd_ltok_skip_alphadigitps(b64, 0) == NULL)
|
|
break;
|
|
n++;
|
|
b64++;
|
|
}
|
|
if (b64[0] == '=') {
|
|
n++;
|
|
b64 = osmtpd_ltok_skip_fws(b64 + 1, 1);
|
|
if (b64[0] == '=') {
|
|
n++;
|
|
b64++;
|
|
}
|
|
}
|
|
/* Invalid tag value */
|
|
if (b64 != end || n % 4 != 0 || (n / 4) * 3 > sizeof(sig->bh)) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid bh tag");
|
|
return;
|
|
}
|
|
/* EVP_DecodeBlock doesn't handle internal whitespace */
|
|
EVP_DecodeInit(ectx);
|
|
if (EVP_DecodeUpdate(ectx, sig->bh, &decodesz, start,
|
|
(int)(end - start)) == -1) {
|
|
/* Paranoia check */
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid bh tag");
|
|
return;
|
|
}
|
|
sig->bhsz = decodesz;
|
|
if (EVP_DecodeFinal(ectx, sig->bh + sig->bhsz, &decodesz) == -1) {
|
|
/* Paranoia check */
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid bh tag");
|
|
return;
|
|
}
|
|
sig->bhsz += decodesz;
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_c(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
if (sig->c != 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate c tag");
|
|
return;
|
|
}
|
|
if (strncmp(start, "simple", 6) == 0) {
|
|
sig->c = CANON_HEADER_SIMPLE;
|
|
start += 6;
|
|
} else if (strncmp(start, "relaxed", 7) == 0) {
|
|
sig->c = CANON_HEADER_RELAXED;
|
|
start += 7;
|
|
} else {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid c tag");
|
|
return;
|
|
}
|
|
if (start[0] == '/') {
|
|
start++;
|
|
if (strncmp(start, "simple", 6) == 0) {
|
|
sig->c |= CANON_BODY_SIMPLE;
|
|
start += 6;
|
|
} else if (strncmp(start, "relaxed", 7) == 0) {
|
|
sig->c |= CANON_BODY_RELAXED;
|
|
start += 7;
|
|
} else {
|
|
dkim_signature_state(sig, DKIM_PERMERROR,
|
|
"Invalid c tag");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (start != end) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid c tag");
|
|
return;
|
|
}
|
|
sig->c |= CANON_DONE;
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_d(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
if (sig->d[0] != '\0') {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate d tag");
|
|
return;
|
|
}
|
|
if (osmtpd_ltok_skip_sig_d_tag_value(start, 0) != end ||
|
|
(size_t)(end - start) >= sizeof(sig->d)) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid d tag");
|
|
return;
|
|
}
|
|
strlcpy(sig->d, start, end - start + 1);
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_h(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
const char *h;
|
|
size_t n = 0;
|
|
|
|
if (sig->h != NULL) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate h tag");
|
|
return;
|
|
}
|
|
if (osmtpd_ltok_skip_sig_h_tag_value(start, 0) < end) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid h tag");
|
|
return;
|
|
}
|
|
h = start;
|
|
while (1) {
|
|
if ((h = osmtpd_ltok_skip_hdr_name(h, 0)) == NULL) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR,
|
|
"Invalid h tag");
|
|
return;
|
|
}
|
|
n++;
|
|
/* ';' is part of hdr-name */
|
|
if (h > end) {
|
|
h = end;
|
|
break;
|
|
}
|
|
h = osmtpd_ltok_skip_fws(h, 1);
|
|
if (h[0] != ':')
|
|
break;
|
|
h = osmtpd_ltok_skip_fws(h + 1, 1);
|
|
}
|
|
if ((sig->h = calloc(n + 1, sizeof(*sig->h))) == NULL) {
|
|
dkim_err(sig->header->msg, "malloc");
|
|
return;
|
|
}
|
|
n = 0;
|
|
h = start;
|
|
while (1) {
|
|
h = osmtpd_ltok_skip_hdr_name(start, 0);
|
|
/* ';' is part of hdr-name */
|
|
if (h > end) {
|
|
sig->h[n] = strndup(start, end - start);
|
|
break;
|
|
}
|
|
if ((sig->h[n++] = strndup(start, h - start)) == NULL) {
|
|
dkim_err(sig->header->msg, "malloc");
|
|
return;
|
|
}
|
|
start = osmtpd_ltok_skip_fws(h, 1);
|
|
if (start[0] != ':')
|
|
break;
|
|
start = osmtpd_ltok_skip_fws(start + 1, 1);
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_i(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
if (sig->i != NULL) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate i tag");
|
|
return;
|
|
}
|
|
if (osmtpd_ltok_skip_sig_i_tag_value(start, 0) != end) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid i tag");
|
|
return;
|
|
}
|
|
sig->i = start;
|
|
sig->isz = (size_t)(end - start);
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_l(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
long long l;
|
|
char *lend;
|
|
|
|
if (sig->l != -1) { /* Duplicate tag */
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate l tag");
|
|
return;
|
|
}
|
|
errno = 0;
|
|
l = strtoll(start, &lend, 10);
|
|
/* > 76 digits in stroll is an overflow */
|
|
if (osmtpd_ltok_skip_digit(start, 0) == NULL ||
|
|
lend != end || errno != 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid l tag");
|
|
return;
|
|
}
|
|
if (l > SSIZE_MAX) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "l tag too large");
|
|
return;
|
|
}
|
|
sig->l = (ssize_t)l;
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_q(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
const char *qend;
|
|
|
|
if (sig->q != 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate q tag");
|
|
return;
|
|
}
|
|
|
|
while (1) {
|
|
start = osmtpd_ltok_skip_fws(start, 1);
|
|
qend = osmtpd_ltok_skip_sig_q_tag_method(start, 0);
|
|
if (qend == NULL) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid q tag");
|
|
return;
|
|
}
|
|
if (strncmp(start, "dns/txt", qend - start) == 0)
|
|
sig->q = 1;
|
|
start = osmtpd_ltok_skip_fws(qend, 1);
|
|
if (start[0] != ':')
|
|
break;
|
|
}
|
|
if (start != end) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid q tag");
|
|
return;
|
|
}
|
|
if (sig->q != 1) {
|
|
sig->q = 1;
|
|
dkim_signature_state(sig, DKIM_NEUTRAL, "No useable q found");
|
|
return;
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_s(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
if (sig->s[0] != '\0') {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate s tag");
|
|
return;
|
|
}
|
|
if (osmtpd_ltok_skip_selector(start, 0) != end) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid s tag");
|
|
return;
|
|
}
|
|
strlcpy(sig->s, start, end - start + 1);
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_t(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
char *tend;
|
|
|
|
if (sig->t != -1) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate t tag");
|
|
return;
|
|
}
|
|
errno = 0;
|
|
sig->t = strtoll(start, &tend, 10);
|
|
if (osmtpd_ltok_skip_digit(start, 0) == NULL || tend != end ||
|
|
tend - start > 12 || errno != 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid t tag");
|
|
return;
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_x(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
char *xend;
|
|
|
|
if (sig->x != -1) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate x tag");
|
|
return;
|
|
}
|
|
errno = 0;
|
|
sig->x = strtoll(start, &xend, 10);
|
|
if (osmtpd_ltok_skip_digit(start, 0) == NULL || xend != end ||
|
|
xend - start > 12 || errno != 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid x tag");
|
|
return;
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_signature_parse_z(struct signature *sig, const char *start, const char *end)
|
|
{
|
|
if (sig->z != 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Duplicate z tag");
|
|
return;
|
|
}
|
|
|
|
sig->z = 1;
|
|
if (osmtpd_ltok_skip_sig_z_tag_value(start, 0) != end) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid z tag");
|
|
return;
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_signature_verify(struct signature *sig)
|
|
{
|
|
struct message *msg = sig->header->msg;
|
|
static EVP_MD_CTX *bctx = NULL;
|
|
char digest[EVP_MAX_MD_SIZE];
|
|
unsigned int digestsz;
|
|
const char *end;
|
|
size_t i, header;
|
|
|
|
if (sig->state != DKIM_UNKNOWN)
|
|
return;
|
|
|
|
if (bctx == NULL) {
|
|
if ((bctx = EVP_MD_CTX_new()) == NULL) {
|
|
dkim_errx(msg, "EVP_MD_CTX_new");
|
|
return;
|
|
}
|
|
}
|
|
EVP_MD_CTX_reset(bctx);
|
|
if (!sig->sephash) {
|
|
if (EVP_DigestVerifyInit(bctx, NULL, sig->ah, NULL,
|
|
sig->p) != 1) {
|
|
dkim_errx(msg, "EVP_DigestVerifyInit");
|
|
return;
|
|
}
|
|
} else {
|
|
if (EVP_DigestInit_ex(bctx, sig->ah, NULL) != 1) {
|
|
dkim_errx(msg, "EVP_DigestInit_ex");
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < msg->nheaders; i++)
|
|
msg->header[i].parsed = 0;
|
|
|
|
for (header = 0; sig->h[header] != NULL; header++) {
|
|
for (i = msg->nheaders; i > 0; ) {
|
|
i--;
|
|
if (msg->header[i].parsed ||
|
|
strncasecmp(msg->header[i].buf, sig->h[header],
|
|
strlen(sig->h[header])) != 0 ||
|
|
msg->header[i].sig == sig)
|
|
continue;
|
|
end = osmtpd_ltok_skip_fws(
|
|
msg->header[i].buf + strlen(sig->h[header]), 1);
|
|
if (end[0] != ':')
|
|
continue;
|
|
dkim_signature_header(bctx, sig, &(msg->header[i]));
|
|
msg->header[i].parsed = 1;
|
|
}
|
|
}
|
|
dkim_signature_header(bctx, sig, sig->header);
|
|
if (!sig->sephash) {
|
|
if (EVP_DigestVerifyFinal(bctx, sig->b, sig->bsz) != 1)
|
|
dkim_signature_state(sig, DKIM_FAIL, "b mismatch");
|
|
} else {
|
|
if (EVP_DigestFinal_ex(bctx, digest, &digestsz) == 0) {
|
|
dkim_errx(msg, "EVP_DigestFinal_ex");
|
|
return;
|
|
}
|
|
if (EVP_DigestVerifyInit(bctx, NULL, NULL, NULL, sig->p) != 1) {
|
|
dkim_errx(msg, "EVP_DigestVerifyInit");
|
|
return;
|
|
}
|
|
switch (EVP_DigestVerify(bctx, sig->b, sig->bsz, digest,
|
|
digestsz)) {
|
|
case 1:
|
|
break;
|
|
case 0:
|
|
dkim_signature_state(sig, DKIM_FAIL, "b mismatch");
|
|
break;
|
|
default:
|
|
dkim_errx(msg, "EVP_DigestVerify");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* EVP_DigestVerifyUpdate is a macro, so we can't alias this on a variable */
|
|
#define dkim_b_digest_update(a, b, c) \
|
|
(sig->sephash ? EVP_DigestUpdate((a), (b), (c)) :\
|
|
EVP_DigestVerifyUpdate((a), (b), (c)))
|
|
|
|
void
|
|
dkim_signature_header(EVP_MD_CTX *bctx, struct signature *sig,
|
|
struct header *header)
|
|
{
|
|
char c;
|
|
const char *ptr = header->buf, *end;
|
|
int inhdrname = 1;
|
|
int canon = sig->c & CANON_HEADER;
|
|
|
|
for (ptr = header->buf; ptr[0] != '\0'; ptr++) {
|
|
if (inhdrname) {
|
|
if (canon == CANON_HEADER_RELAXED) {
|
|
ptr = osmtpd_ltok_skip_fws(ptr, 1);
|
|
c = tolower(ptr[0]);
|
|
} else
|
|
c = ptr[0];
|
|
if (c == ':') {
|
|
inhdrname = 0;
|
|
if (canon == CANON_HEADER_RELAXED)
|
|
ptr = osmtpd_ltok_skip_fws(
|
|
ptr + 1, 1) - 1;
|
|
}
|
|
if (dkim_b_digest_update(bctx, &c, 1) == 0) {
|
|
dkim_errx(sig->header->msg,
|
|
"dkim_b_digest_update");
|
|
return;
|
|
}
|
|
continue;
|
|
}
|
|
end = osmtpd_ltok_skip_fws(ptr, 1);
|
|
if (end == ptr) {
|
|
if (sig->header == header && ptr == sig->bheader) {
|
|
ptr = osmtpd_ltok_skip_tag_value(
|
|
ptr, 0) - 1;
|
|
continue;
|
|
}
|
|
if (dkim_b_digest_update(bctx, ptr, 1) == 0) {
|
|
dkim_errx(sig->header->msg,
|
|
"dkim_b_digest_update");
|
|
return;
|
|
}
|
|
} else {
|
|
if (canon == CANON_HEADER_RELAXED) {
|
|
if (end[0] == '\0')
|
|
continue;
|
|
if (dkim_b_digest_update(bctx, " ", 1) == 0) {
|
|
dkim_errx(sig->header->msg,
|
|
"dkim_b_digest_update");
|
|
return;
|
|
}
|
|
} else {
|
|
if (dkim_b_digest_update(bctx, ptr,
|
|
end - ptr) == 0) {
|
|
dkim_errx(sig->header->msg,
|
|
"dkim_b_digest_update");
|
|
return;
|
|
}
|
|
}
|
|
ptr = end - 1;
|
|
}
|
|
|
|
}
|
|
if (sig->header != header) {
|
|
if (dkim_b_digest_update(bctx, "\r\n", 2) == 0) {
|
|
dkim_errx(sig->header->msg, "dkim_b_digest_update");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_signature_state(struct signature *sig, enum state state,
|
|
const char *reason)
|
|
{
|
|
if (sig->query != NULL) {
|
|
event_asr_abort(sig->query);
|
|
sig->query = NULL;
|
|
}
|
|
switch (sig->state) {
|
|
case DKIM_UNKNOWN:
|
|
break;
|
|
case DKIM_PASS:
|
|
case DKIM_FAIL:
|
|
osmtpd_errx(1, "Unexpected transition");
|
|
case DKIM_POLICY:
|
|
if (state == DKIM_PASS)
|
|
return;
|
|
break;
|
|
case DKIM_NEUTRAL:
|
|
if (state == DKIM_PASS)
|
|
return;
|
|
if (state == DKIM_TEMPERROR || state == DKIM_PERMERROR)
|
|
break;
|
|
osmtpd_errx(1, "Unexpected transition");
|
|
case DKIM_TEMPERROR:
|
|
if (state == DKIM_PERMERROR)
|
|
break;
|
|
return;
|
|
case DKIM_PERMERROR:
|
|
return;
|
|
}
|
|
sig->state = state;
|
|
sig->state_reason = reason;
|
|
}
|
|
|
|
const char *
|
|
dkim_state2str(enum state state)
|
|
{
|
|
switch (state)
|
|
{
|
|
case DKIM_UNKNOWN:
|
|
return "unknown";
|
|
case DKIM_PASS:
|
|
return "pass";
|
|
case DKIM_FAIL:
|
|
return "fail";
|
|
case DKIM_POLICY:
|
|
return "policy";
|
|
case DKIM_NEUTRAL:
|
|
return "neutral";
|
|
case DKIM_TEMPERROR:
|
|
return "temperror";
|
|
case DKIM_PERMERROR:
|
|
return "permerror";
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_rr_resolve(struct asr_result *ar, void *arg)
|
|
{
|
|
struct signature *sig = arg;
|
|
char key[UINT16_MAX + 1];
|
|
const char *rr_txt;
|
|
size_t keylen, cstrlen;
|
|
struct unpack pack;
|
|
struct dns_header h;
|
|
struct dns_query q;
|
|
struct dns_rr rr;
|
|
|
|
sig->query = NULL;
|
|
|
|
if (ar->ar_h_errno == TRY_AGAIN || ar->ar_h_errno == NO_RECOVERY) {
|
|
dkim_signature_state(sig, DKIM_TEMPERROR,
|
|
hstrerror(ar->ar_h_errno));
|
|
goto verify;
|
|
}
|
|
if (ar->ar_h_errno != NETDB_SUCCESS) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR,
|
|
hstrerror(ar->ar_h_errno));
|
|
goto verify;
|
|
}
|
|
|
|
unpack_init(&pack, ar->ar_data, ar->ar_datalen);
|
|
if (unpack_header(&pack, &h) != 0 ||
|
|
unpack_query(&pack, &q) != 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Invalid dns/txt");
|
|
goto verify;
|
|
}
|
|
for (; h.ancount > 0; h.ancount--) {
|
|
unpack_rr(&pack, &rr);
|
|
if (rr.rr_type != T_TXT)
|
|
continue;
|
|
|
|
keylen = 0;
|
|
rr_txt = rr.rr.other.rdata;
|
|
while (rr.rr.other.rdlen > 0) {
|
|
cstrlen = ((const unsigned char *)rr_txt)[0];
|
|
if (cstrlen >= rr.rr.other.rdlen ||
|
|
keylen + cstrlen >= sizeof(key))
|
|
break;
|
|
/*
|
|
* RFC 6376 Section 3.6.2.2
|
|
* Strings in a TXT RR MUST be concatenated together
|
|
* before use with no intervening whitespace.
|
|
*/
|
|
strlcpy(key + keylen, rr_txt + 1, cstrlen + 1);
|
|
rr.rr.other.rdlen -= (cstrlen + 1);
|
|
rr_txt += (cstrlen + 1);
|
|
keylen += cstrlen;
|
|
}
|
|
if (rr.rr.other.rdlen > 0) /* Invalid TXT RDATA */
|
|
continue;
|
|
|
|
if (dkim_key_text_parse(sig, key))
|
|
break;
|
|
}
|
|
|
|
if (h.ancount == 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR,
|
|
"No matching key found");
|
|
} else {
|
|
/* Only verify if all headers have been read */
|
|
if (!sig->header->msg->parsing_headers)
|
|
dkim_signature_verify(sig);
|
|
}
|
|
verify:
|
|
free(ar->ar_data);
|
|
dkim_message_verify(sig->header->msg);
|
|
}
|
|
|
|
int
|
|
dkim_key_text_parse(struct signature *sig, const char *key)
|
|
{
|
|
char tagname, *hashname;
|
|
const char *end, *tagvend;
|
|
char pkraw[UINT16_MAX] = "", pkimp[UINT16_MAX];
|
|
size_t pkrawlen = 0, pkoff, linelen;
|
|
int h = 0, k = 0, n = 0, p = 0, s = 0, t = 0, first = 1;
|
|
BIO *bio;
|
|
#ifdef HAVE_ED25519
|
|
size_t pklen;
|
|
int tmp;
|
|
#endif
|
|
|
|
key = osmtpd_ltok_skip_fws(key, 1);
|
|
/* Validate syntax early */
|
|
if ((end = osmtpd_ltok_skip_tag_list(key, 0)) == NULL)
|
|
return 0;
|
|
|
|
while (key[0] != '\0') {
|
|
key = osmtpd_ltok_skip_fws(key, 1);
|
|
if ((end = osmtpd_ltok_skip_tag_name(key, 0)) == NULL)
|
|
return 0;
|
|
|
|
if ((size_t)(end - key) != 1)
|
|
tagname = '\0';
|
|
else
|
|
tagname = key[0];
|
|
key = osmtpd_ltok_skip_fws(end, 1);
|
|
/* '=' */
|
|
if (key[0] != '=')
|
|
return 0;
|
|
key = osmtpd_ltok_skip_fws(key + 1, 1);
|
|
if ((end = osmtpd_ltok_skip_tag_value(key, 0)) == NULL)
|
|
return 0;
|
|
switch (tagname) {
|
|
case 'v':
|
|
/*
|
|
* RFC 6376 section 3.6.1, v=:
|
|
* RECOMMENDED...This tag MUST be the first tag in the
|
|
* record.
|
|
*/
|
|
if (!first ||
|
|
osmtpd_ltok_skip_key_v_tag_value(key, 0) != end)
|
|
return 0;
|
|
key = end;
|
|
break;
|
|
case 'h':
|
|
if (h != 0) /* Duplicate tag */
|
|
return 0;
|
|
/* Invalid tag value */
|
|
if (osmtpd_ltok_skip_key_h_tag_value(key, 0) != end)
|
|
return 0;
|
|
while (1) {
|
|
if ((tagvend = osmtpd_ltok_skip_key_h_tag_alg(
|
|
key, 0)) == NULL)
|
|
break;
|
|
hashname = strndup(key, tagvend - key);
|
|
if (hashname == NULL) {
|
|
dkim_err(sig->header->msg, "malloc");
|
|
return 0;
|
|
}
|
|
if (EVP_get_digestbyname(hashname) == sig->ah) {
|
|
free(hashname);
|
|
h = 1;
|
|
break;
|
|
}
|
|
free(hashname);
|
|
key = osmtpd_ltok_skip_fws(tagvend, 1);
|
|
if (key[0] != ':')
|
|
break;
|
|
key = osmtpd_ltok_skip_fws(key + 1, 1);
|
|
}
|
|
if (h != 1)
|
|
return 0;
|
|
key = end;
|
|
break;
|
|
case 'k':
|
|
if (k != 0) /* Duplicate tag */
|
|
return 0;
|
|
k = 1;
|
|
if (strncmp(key, "rsa", end - key) == 0) {
|
|
if (sig->ak != EVP_PKEY_RSA)
|
|
return 0;
|
|
#if HAVE_ED25519
|
|
} else if (strncmp(key, "ed25519", end - key) == 0) {
|
|
if (sig->ak != EVP_PKEY_ED25519)
|
|
return 0;
|
|
#endif
|
|
} else
|
|
return 0;
|
|
key = end;
|
|
break;
|
|
case 'n':
|
|
if (n != 0) /* Duplicate tag */
|
|
return 0;
|
|
n = 1;
|
|
/* semicolon is part of safe-char */
|
|
if (osmtpd_ltok_skip_key_n_tag_value(key, 0) < end)
|
|
return 0;
|
|
key = end;
|
|
break;
|
|
case 'p':
|
|
if (p != 0) /* Duplicate tag */
|
|
return 0;
|
|
p = 1;
|
|
while (1) {
|
|
key = osmtpd_ltok_skip_fws(key, 1);
|
|
if (osmtpd_ltok_skip_alphadigitps(
|
|
key, 0) == NULL)
|
|
break;
|
|
pkraw[pkrawlen++] = key++[0];
|
|
if (pkrawlen >= sizeof(pkraw))
|
|
return 0;
|
|
}
|
|
if (key[0] == '=') {
|
|
pkraw[pkrawlen++] = '=';
|
|
key = osmtpd_ltok_skip_fws(key + 1, 1);
|
|
if (pkrawlen >= sizeof(pkraw))
|
|
return 0;
|
|
if (key[0] == '=') {
|
|
pkraw[pkrawlen++] = '=';
|
|
key++;
|
|
if (pkrawlen >= sizeof(pkraw))
|
|
return 0;
|
|
}
|
|
}
|
|
/* Invalid tag value */
|
|
if (pkrawlen % 4 != 0 || key != end)
|
|
return 0;
|
|
break;
|
|
case 's':
|
|
if (s != 0) /* Duplicate tag */
|
|
return 0;
|
|
/* Invalid tag value */
|
|
if (osmtpd_ltok_skip_key_s_tag_value(key, 0) != end)
|
|
return 0;
|
|
while (1) {
|
|
if ((tagvend =
|
|
osmtpd_ltok_skip_key_s_tag_type(
|
|
key, 0)) == NULL)
|
|
break;
|
|
if (strncmp(key, "*", tagvend - key) == 0 ||
|
|
strncmp(key, "email", tagvend - key) == 0) {
|
|
s = 1;
|
|
break;
|
|
}
|
|
key = osmtpd_ltok_skip_fws(tagvend, 1);
|
|
if (key[0] != ':')
|
|
break;
|
|
key = osmtpd_ltok_skip_fws(key + 1, 1);
|
|
}
|
|
if (s != 1)
|
|
return 0;
|
|
key = end;
|
|
break;
|
|
case 't':
|
|
if (t != 0) /* Duplicate tag */
|
|
return 0;
|
|
t = 1;
|
|
if (osmtpd_ltok_skip_key_t_tag_value(key, 0) != end)
|
|
return 0;
|
|
while (1) {
|
|
tagvend = osmtpd_ltok_skip_key_t_tag_flag(
|
|
key, 0);
|
|
if (strncmp(key, "y", tagvend - key) == 0)
|
|
sig->kt |= KT_Y;
|
|
else if (strncmp(key, "s", tagvend - key) == 0)
|
|
sig->kt |= KT_S;
|
|
key = osmtpd_ltok_skip_fws(tagvend, 1);
|
|
if (key[0] != ':')
|
|
break;
|
|
key = osmtpd_ltok_skip_fws(key + 1, 1);
|
|
}
|
|
break;
|
|
default:
|
|
key = end;
|
|
break;
|
|
}
|
|
|
|
first = 0;
|
|
key = osmtpd_ltok_skip_fws(key, 1);
|
|
if (key[0] == ';')
|
|
key++;
|
|
else if (key[0] != '\0')
|
|
return 0;
|
|
}
|
|
|
|
if (!p) /* Missing tag */
|
|
return 0;
|
|
if (k == 0 && sig->ak != EVP_PKEY_RSA) /* Default to RSA */
|
|
return 0;
|
|
|
|
if (pkraw[0] == '\0') {
|
|
dkim_signature_state(sig, DKIM_PERMERROR, "Key is revoked");
|
|
return 1;
|
|
}
|
|
|
|
switch (sig->ak) {
|
|
case EVP_PKEY_RSA:
|
|
pkoff = strlcpy(pkimp, "-----BEGIN PUBLIC KEY-----\n",
|
|
sizeof(pkimp));
|
|
linelen = 0;
|
|
for (key = pkraw; key[0] != '\0';) {
|
|
if (pkoff + 2 >= sizeof(pkimp))
|
|
return 0;
|
|
pkimp[pkoff++] = key++[0];
|
|
if (++linelen == 64) {
|
|
pkimp[pkoff++] = '\n';
|
|
linelen = 0;
|
|
}
|
|
}
|
|
/* Leverage pkoff check in loop */
|
|
if (linelen != 0)
|
|
pkimp[pkoff++] = '\n';
|
|
/* PEM_read_bio_PUBKEY will catch truncated keys */
|
|
pkoff += strlcpy(pkimp + pkoff, "-----END PUBLIC KEY-----\n",
|
|
sizeof(pkimp) - pkoff);
|
|
if ((bio = BIO_new_mem_buf(pkimp, pkoff)) == NULL) {
|
|
dkim_err(sig->header->msg, "BIO_new_mem_buf");
|
|
return 1;
|
|
}
|
|
sig->p = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
|
|
BIO_free(bio);
|
|
break;
|
|
#if HAVE_ED25519
|
|
case EVP_PKEY_ED25519:
|
|
if ((pkrawlen / 4) * 3 >= sizeof(pkimp))
|
|
return 0;
|
|
EVP_DecodeInit(ectx);
|
|
if (EVP_DecodeUpdate(ectx, pkimp, &tmp, pkraw, pkrawlen) == -1)
|
|
return 0;
|
|
pklen = tmp;
|
|
if (EVP_DecodeFinal(ectx, pkimp, &tmp) == -1)
|
|
return 0;
|
|
pklen += tmp;
|
|
sig->p = EVP_PKEY_new_raw_public_key(sig->ak, NULL, pkimp,
|
|
pklen);
|
|
break;
|
|
#endif
|
|
}
|
|
if (sig->p == NULL) {
|
|
/*
|
|
* XXX No clue how to differentiate between invalid key and
|
|
* temporary failure like *alloc.
|
|
* Assume invalid key, because it's more likely.
|
|
*/
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
void
|
|
dkim_body_parse(struct message *msg, const char *line)
|
|
{
|
|
struct signature *sig;
|
|
const char *end = line, *hash, *prev;
|
|
size_t hashn, len, i;
|
|
int wsp, ret;
|
|
|
|
if (line[0] == '\0') {
|
|
msg->body_whitelines++;
|
|
return;
|
|
}
|
|
|
|
while (msg->body_whitelines-- > 0) {
|
|
for (i = 0; i < msg->nheaders; i++) {
|
|
if ((sig = msg->header[i].sig) == NULL ||
|
|
sig->state != DKIM_UNKNOWN)
|
|
continue;
|
|
hashn = sig->l == -1 ? 2 : MIN(2, sig->l);
|
|
sig->l -= sig->l == -1 ? 0 : hashn;
|
|
if (EVP_DigestUpdate(sig->bhctx, "\r\n", hashn) == 0) {
|
|
dkim_errx(msg, "EVP_DigestUpdate");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
msg->body_whitelines = 0;
|
|
msg->has_body = 1;
|
|
|
|
while (line[0] != '\0') {
|
|
while (1) {
|
|
prev = end;
|
|
if ((end = osmtpd_ltok_skip_wsp(end, 0)) == NULL)
|
|
break;
|
|
}
|
|
end = prev;
|
|
wsp = end != line;
|
|
if (!wsp) {
|
|
while (osmtpd_ltok_skip_wsp(end, 0) == NULL &&
|
|
end[0] != '\0')
|
|
end++;
|
|
}
|
|
for (i = 0; i < msg->nheaders; i++) {
|
|
sig = msg->header[i].sig;
|
|
if (sig == NULL || sig->state != DKIM_UNKNOWN)
|
|
continue;
|
|
if (wsp &&
|
|
(sig->c & CANON_BODY) == CANON_BODY_RELAXED) {
|
|
hash = " ";
|
|
len = end[0] == '\0' ? 0 : 1;
|
|
} else {
|
|
hash = line;
|
|
len = (size_t)(end - line);
|
|
}
|
|
hashn = sig->l == -1 ? len : MIN(len, (size_t)sig->l);
|
|
sig->l -= sig->l == -1 ? 0 : hashn;
|
|
ret = EVP_DigestUpdate(sig->bhctx, hash, hashn);
|
|
if (ret == 0) {
|
|
dkim_errx(msg, "EVP_DigestUpdate");
|
|
return;
|
|
}
|
|
}
|
|
line = end;
|
|
}
|
|
for (i = 0; i < msg->nheaders; i++) {
|
|
sig = msg->header[i].sig;
|
|
if (sig == NULL || sig->state != DKIM_UNKNOWN)
|
|
continue;
|
|
hashn = sig->l == -1 ? 2 : MIN(2, sig->l);
|
|
sig->l -= sig->l == -1 ? 0 : hashn;
|
|
ret = EVP_DigestUpdate(sig->bhctx, "\r\n", hashn);
|
|
if (ret == 0) {
|
|
dkim_errx(msg, "EVP_DigestUpdate");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
dkim_body_verify(struct signature *sig)
|
|
{
|
|
unsigned char digest[EVP_MAX_MD_SIZE];
|
|
unsigned int digestsz;
|
|
|
|
if (sig->state != DKIM_UNKNOWN)
|
|
return;
|
|
|
|
if ((sig->c & CANON_BODY) == CANON_BODY_SIMPLE &&
|
|
!sig->header->msg->has_body) {
|
|
if (EVP_DigestUpdate(sig->bhctx, "\r\n",
|
|
sig->l == -1 ? 2 : MIN(2, sig->l)) <= 0) {
|
|
dkim_errx(sig->header->msg,
|
|
"Can't update hash context");
|
|
return;
|
|
}
|
|
}
|
|
if (sig->l > 0) {
|
|
dkim_signature_state(sig, DKIM_PERMERROR,
|
|
"l tag larger than body");
|
|
return;
|
|
}
|
|
|
|
if (EVP_DigestFinal_ex(sig->bhctx, digest, &digestsz) == 0) {
|
|
dkim_errx(sig->header->msg, "EVP_DigestFinal_ex");
|
|
return;
|
|
}
|
|
|
|
if (digestsz != sig->bhsz || memcmp(digest, sig->bh, digestsz) != 0)
|
|
dkim_signature_state(sig, DKIM_FAIL, "bh mismatch");
|
|
}
|
|
|
|
void
|
|
dkim_message_verify(struct message *msg)
|
|
{
|
|
struct signature *sig;
|
|
size_t i;
|
|
ssize_t n, aroff = 0;
|
|
int found = 0;
|
|
char *line = NULL;
|
|
size_t linelen = 0;
|
|
|
|
if (!msg->readdone)
|
|
return;
|
|
|
|
for (i = 0; i < msg->nheaders; i++) {
|
|
if (msg->header[i].sig == NULL)
|
|
continue;
|
|
if (msg->header[i].sig->query != NULL)
|
|
return;
|
|
if (msg->header[i].sig->state != DKIM_UNKNOWN)
|
|
continue;
|
|
dkim_signature_state(msg->header[i].sig, DKIM_PASS, NULL);
|
|
}
|
|
|
|
if ((aroff = dkim_ar_cat(&line, &linelen, aroff,
|
|
"Authentication-Results: %s", authservid)) == -1) {
|
|
dkim_err(msg, "malloc");
|
|
goto fail;
|
|
}
|
|
for (i = 0; i < msg->nheaders; i++) {
|
|
sig = msg->header[i].sig;
|
|
if (sig == NULL)
|
|
continue;
|
|
found = 1;
|
|
if ((aroff = dkim_ar_cat(&line, &linelen, aroff, "; dkim=%s",
|
|
dkim_state2str(sig->state))) == -1) {
|
|
dkim_err(msg, "malloc");
|
|
goto fail;
|
|
}
|
|
if (sig->state_reason != NULL) {
|
|
if ((aroff = dkim_ar_cat(&line, &linelen, aroff,
|
|
" reason=\"%s\"", sig->state_reason)) == -1) {
|
|
dkim_err(msg, "malloc");
|
|
goto fail;
|
|
}
|
|
}
|
|
if (sig->s[0] != '\0') {
|
|
if ((aroff = dkim_ar_cat(&line, &linelen, aroff,
|
|
" header.s=%s", sig->s)) == -1) {
|
|
dkim_err(msg, "malloc");
|
|
goto fail;
|
|
}
|
|
}
|
|
if (sig->d[0] != '\0') {
|
|
if ((aroff = dkim_ar_cat(&line, &linelen, aroff,
|
|
" header.d=%s", sig->d)) == -1) {
|
|
dkim_err(msg, "malloc");
|
|
goto fail;
|
|
}
|
|
}
|
|
/*
|
|
* Don't print i-tag, since localpart can be a quoted-string,
|
|
* which can contain FWS and CFWS.
|
|
*/
|
|
if (sig->a != NULL) {
|
|
if ((aroff = dkim_ar_cat(&line, &linelen, aroff,
|
|
" header.a=%.*s", (int)sig->asz, sig->a)) == -1) {
|
|
dkim_err(msg, "malloc");
|
|
goto fail;
|
|
}
|
|
}
|
|
}
|
|
if (!found) {
|
|
aroff = dkim_ar_cat(&line, &linelen, aroff, "; dkim=none");
|
|
if (aroff == -1) {
|
|
dkim_err(msg, "malloc");
|
|
goto fail;
|
|
}
|
|
}
|
|
dkim_ar_print(msg->ctx, line);
|
|
|
|
rewind(msg->origf);
|
|
while ((n = getline(&line, &linelen, msg->origf)) != -1) {
|
|
line[n - 1] = '\0';
|
|
osmtpd_filter_dataline(msg->ctx, "%s", line);
|
|
}
|
|
if (ferror(msg->origf))
|
|
dkim_err(msg, "getline");
|
|
fail:
|
|
free(line);
|
|
return;
|
|
}
|
|
|
|
void
|
|
dkim_ar_print(struct osmtpd_ctx *ctx, const char *start)
|
|
{
|
|
const char *scan, *checkpoint, *ncheckpoint;
|
|
size_t arlen = 0;
|
|
int first = 1, arid = 1;
|
|
|
|
checkpoint = start;
|
|
ncheckpoint = osmtpd_ltok_skip_hdr_name(start, 0) + 1;
|
|
for (scan = start; scan[0] != '\0'; scan++) {
|
|
if (scan[0] == '\t')
|
|
arlen = (arlen + 8) & ~7;
|
|
else
|
|
arlen++;
|
|
if (arlen >= AUTHENTICATION_RESULTS_LINELEN) {
|
|
osmtpd_filter_dataline(ctx, "%s%.*s", first ? "" : "\t",
|
|
(int)((checkpoint == start ?
|
|
ncheckpoint : checkpoint) - start), start);
|
|
start = osmtpd_ltok_skip_cfws(checkpoint, 1);
|
|
scan = start;
|
|
arlen = 8;
|
|
first = 0;
|
|
}
|
|
if (scan == ncheckpoint) {
|
|
checkpoint = ncheckpoint;
|
|
ncheckpoint = osmtpd_ltok_skip_cfws(ncheckpoint, 1);
|
|
/* authserv-id */
|
|
if (arid) {
|
|
ncheckpoint = osmtpd_ltok_skip_value(
|
|
ncheckpoint, 0);
|
|
arid = 0;
|
|
/* methodspec */
|
|
} else if (strncmp(ncheckpoint, "dkim",
|
|
sizeof("dkim") - 1) == 0) {
|
|
ncheckpoint = osmtpd_ltok_skip_keyword(
|
|
ncheckpoint + sizeof("dkim"), 0);
|
|
/* reasonspec */
|
|
} else if (strncmp(ncheckpoint, "reason",
|
|
sizeof("reason") - 1) == 0) {
|
|
ncheckpoint = osmtpd_ltok_skip_value(
|
|
ncheckpoint + sizeof("reason"), 0);
|
|
/* propspec */
|
|
} else {
|
|
ncheckpoint += sizeof("header.x=") - 1;
|
|
ncheckpoint = osmtpd_ltok_skip_ar_pvalue(
|
|
ncheckpoint, 0);
|
|
if (ncheckpoint[0] == ';')
|
|
ncheckpoint++;
|
|
}
|
|
}
|
|
}
|
|
osmtpd_filter_dataline(ctx, "%s%s", first ? "" : "\t", start);
|
|
}
|
|
|
|
ssize_t
|
|
dkim_ar_cat(char **ar, size_t *n, size_t aroff, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
char *artmp;
|
|
int size;
|
|
size_t nn;
|
|
|
|
va_start(ap, fmt);
|
|
size = vsnprintf(*ar + aroff, *n - aroff, fmt, ap);
|
|
va_end(ap);
|
|
if (size + aroff <= *n)
|
|
return (ssize_t)size + aroff;
|
|
nn = (((aroff + size) / 256) + 1) * 256;
|
|
artmp = realloc(*ar, nn);
|
|
if (artmp == NULL)
|
|
return -1;
|
|
*ar = artmp;
|
|
*n = nn;
|
|
va_start(ap, fmt);
|
|
size = vsnprintf(*ar + aroff, *n - aroff, fmt, ap);
|
|
va_end(ap);
|
|
return (ssize_t)size + aroff;
|
|
}
|
|
|
|
void
|
|
dkim_err(struct message *msg, char *text)
|
|
{
|
|
msg->err = 1;
|
|
fprintf(stderr, "%s: %s\n", text, strerror(errno));
|
|
}
|
|
|
|
void
|
|
dkim_errx(struct message *msg, char *text)
|
|
{
|
|
msg->err = 1;
|
|
fprintf(stderr, "%s\n", text);
|
|
}
|
|
|
|
__dead void
|
|
usage(void)
|
|
{
|
|
fprintf(stderr, "usage: filter-dkimverify\n");
|
|
exit(1);
|
|
}
|