Closed rbfajardo closed 5 years ago
They seem to be up to draft 21 now: https://tools.ietf.org/id/draft-ietf-dmarc-arc-protocol-21.txt
@jstedfast Would a good and easy to implement first step be to modify the Verify/VerifyAsync to accept a ARC-Message-Signature as it can be verified in the same way a Dkim-Signature header can be verified.
From the RFC
The AMS header field has the same syntax and semantics as the DKIM-Signature field [RFC6376], with three (3) differences:
the name of the header field itself; no version tag (“v”) is defined for the AMS header field. As required for undefined tags (in [RFC6376]), if seen, a version tag MUST be ignored; the “i” (AUID) tag is not imported from DKIM; instead, this tag is replaced by the “instance tag” as defined in Section 4.2.1;
I suppose so...
diff --git a/MimeKit/MimeMessage.cs b/MimeKit/MimeMessage.cs
index 63361162..a1b0a1a3 100644
--- a/MimeKit/MimeMessage.cs
+++ b/MimeKit/MimeMessage.cs
@@ -29,6 +29,7 @@ using System.IO;
using System.Text;
using System.Linq;
using System.Threading;
+using System.Globalization;
using System.Threading.Tasks;
using System.Collections.Generic;
@@ -2017,73 +2018,82 @@ namespace MimeKit {
return parameters;
}
- static void ValidateDkimSignatureParameters (IDictionary<string, string> parameters, out DkimSignatureAlgorithm algorithm, out DkimCanonicalizationAlgorithm headerAlgorithm,
+ static void ValidateSignatureParameters (IDictionary<string, string> parameters, HeaderId header, out DkimSignatureAlgorithm algorithm, out DkimCanonicalizationAlgorithm headerAlgorithm,
out DkimCanonicalizationAlgorithm bodyAlgorithm, out string d, out string s, out string q, out string[] headers, out string bh, out string b, out int maxLength)
{
bool containsFrom = false;
- string v, a, c, h, l, id;
- if (!parameters.TryGetValue ("v", out v))
- throw new FormatException ("Malformed DKIM-Signature header: no version parameter detected.");
+ if (header == HeaderId.DkimSignature) {
+ if (!parameters.TryGetValue ("v", out string v))
+ throw new FormatException ("Malformed DKIM-Signature header: no version parameter detected.");
- if (v != "1")
- throw new FormatException (string.Format ("Unrecognized DKIM-Signature version: v={0}", v));
+ if (v != "1")
+ throw new FormatException (string.Format ("Unrecognized DKIM-Signature version: v={0}", v));
+ }
- if (!parameters.TryGetValue ("a", out a))
- throw new FormatException ("Malformed DKIM-Signature header: no signature algorithm parameter detected.");
+ if (!parameters.TryGetValue ("a", out string a))
+ throw new FormatException (string.Format ("Malformed {0} header: no signature algorithm parameter detected.", header.ToHeaderName ()));
switch (a.ToLowerInvariant ()) {
case "rsa-sha256": algorithm = DkimSignatureAlgorithm.RsaSha256; break;
case "rsa-sha1": algorithm = DkimSignatureAlgorithm.RsaSha1; break;
- default: throw new FormatException (string.Format ("Unrecognized DKIM-Signature algorithm parameter: a={0}", a));
+ default: throw new FormatException (string.Format ("Unrecognized {0} algorithm parameter: a={1}", header.ToHeaderName (), a));
}
if (!parameters.TryGetValue ("d", out d))
- throw new FormatException ("Malformed DKIM-Signature header: no domain parameter detected.");
+ throw new FormatException (string.Format ("Malformed {0} header: no domain parameter detected.", header.ToHeaderName ()));
- if (parameters.TryGetValue ("i", out id)) {
- string ident;
- int at;
+ if (parameters.TryGetValue ("i", out string id)) {
+ switch (header) {
+ case HeaderId.DkimSignature:
+ string ident;
+ int at;
- if ((at = id.LastIndexOf ('@')) == -1)
- throw new FormatException ("Malformed DKIM-Signature header: no @ in the AUID value.");
+ if ((at = id.LastIndexOf ('@')) == -1)
+ throw new FormatException ("Malformed DKIM-Signature header: no @ in the AUID value.");
- ident = id.Substring (at + 1);
+ ident = id.Substring (at + 1);
- if (!ident.Equals (d, StringComparison.OrdinalIgnoreCase) && !ident.EndsWith ("." + d, StringComparison.OrdinalIgnoreCase))
- throw new FormatException ("Invalid DKIM-Signature header: the domain in the AUID does not match the domain parameter.");
+ if (!ident.Equals (d, StringComparison.OrdinalIgnoreCase) && !ident.EndsWith ("." + d, StringComparison.OrdinalIgnoreCase))
+ throw new FormatException ("Invalid DKIM-Signature header: the domain in the AUID does not match the domain parameter.");
+ break;
+ case HeaderId.ArcMessageSignature:
+ if (!int.TryParse (id, NumberStyles.Integer, CultureInfo.InvariantCulture, out int i) || i < 1 || i > 50)
+ throw new FormatException ("Malformed ARC-Message-Signature header: invalid instance value.");
+ break;
+ }
}
if (!parameters.TryGetValue ("s", out s))
- throw new FormatException ("Malformed DKIM-Signature header: no selector parameter detected.");
+ throw new FormatException (string.Format ("Malformed {0} header: no selector parameter detected.", header.ToHeaderName ()));
if (!parameters.TryGetValue ("q", out q))
q = "dns/txt";
- if (parameters.TryGetValue ("l", out l)) {
+ if (parameters.TryGetValue ("l", out string l)) {
if (!int.TryParse (l, out maxLength))
- throw new FormatException (string.Format ("Malformed DKIM-Signature header: invalid length parameter: l={0}", l));
+ throw new FormatException (string.Format ("Malformed {0} header: invalid length parameter: l={1}", header.ToHeaderName (), l));
} else {
maxLength = -1;
}
- if (parameters.TryGetValue ("c", out c)) {
+ if (parameters.TryGetValue ("c", out string c)) {
var tokens = c.ToLowerInvariant ().Split ('/');
if (tokens.Length == 0 || tokens.Length > 2)
- throw new FormatException (string.Format ("Malformed DKIM-Signature header: invalid canonicalization parameter: c={0}", c));
+ throw new FormatException (string.Format ("Malformed {0} header: invalid canonicalization parameter: c={1}", header.ToHeaderName (), c));
switch (tokens[0]) {
case "relaxed": headerAlgorithm = DkimCanonicalizationAlgorithm.Relaxed; break;
case "simple": headerAlgorithm = DkimCanonicalizationAlgorithm.Simple; break;
- default: throw new FormatException (string.Format ("Malformed DKIM-Signature header: invalid canonicalization parameter: c={0}", c));
+ default: throw new FormatException (string.Format ("Malformed {0} header: invalid canonicalization parameter: c={1}", header.ToHeaderName (), c));
}
if (tokens.Length == 2) {
switch (tokens[1]) {
case "relaxed": bodyAlgorithm = DkimCanonicalizationAlgorithm.Relaxed; break;
case "simple": bodyAlgorithm = DkimCanonicalizationAlgorithm.Simple; break;
- default: throw new FormatException (string.Format ("Malformed DKIM-Signature header: invalid canonicalization parameter: c={0}", c));
+ default: throw new FormatException (string.Format ("Malformed {0} header: invalid canonicalization parameter: c={1}", header.ToHeaderName (), c));
}
} else {
bodyAlgorithm = DkimCanonicalizationAlgorithm.Simple;
@@ -2093,8 +2103,8 @@ namespace MimeKit {
bodyAlgorithm = DkimCanonicalizationAlgorithm.Simple;
}
- if (!parameters.TryGetValue ("h", out h))
- throw new FormatException ("Malformed DKIM-Signature header: no signed header parameter detected.");
+ if (!parameters.TryGetValue ("h", out string h))
+ throw new FormatException (string.Format ("Malformed {0} header: no signed header parameter detected.", header.ToHeaderName ()));
headers = h.Split (':');
for (int i = 0; i < headers.Length; i++) {
@@ -2105,19 +2115,19 @@ namespace MimeKit {
}
if (!containsFrom)
- throw new FormatException (string.Format ("Malformed DKIM-Signature header: From header not signed."));
+ throw new FormatException (string.Format ("Malformed {0} header: From header not signed.", header.ToHeaderName ()));
if (!parameters.TryGetValue ("bh", out bh))
- throw new FormatException ("Malformed DKIM-Signature header: no body hash parameter detected.");
+ throw new FormatException (string.Format ("Malformed {0} header: no body hash parameter detected.", header.ToHeaderName ()));
if (!parameters.TryGetValue ("b", out b))
- throw new FormatException ("Malformed DKIM-Signature header: no signature parameter detected.");
+ throw new FormatException (string.Format ("Malformed {0} header: no signature parameter detected.", header.ToHeaderName ()));
}
- static Header GetSignedDkimSignatureHeader (Header dkimSignature)
+ static Header GetSignedSignatureHeader (Header header)
{
// modify the raw DKIM-Signature header value by chopping off the signature value after the "b="
- var rawValue = (byte[]) dkimSignature.RawValue.Clone ();
+ var rawValue = (byte[]) header.RawValue.Clone ();
int length = 0, index = 0;
do {
@@ -2155,14 +2165,14 @@ namespace MimeKit {
} while (index < rawValue.Length);
if (index == rawValue.Length)
- throw new FormatException ("Malformed DKIM-Signature header: missing signature parameter.");
+ throw new FormatException (string.Format ("Malformed {0} header: missing signature parameter.", header.Id.ToHeaderName ()));
while (index < rawValue.Length)
rawValue[length++] = rawValue[index++];
Array.Resize (ref rawValue, length);
- return new Header (dkimSignature.Options, dkimSignature.RawField, rawValue);
+ return new Header (header.Options, header.RawField, rawValue);
}
async Task<bool> VerifyAsync (FormatOptions options, Header dkimSignature, IDkimPublicKeyLocator publicKeyLocator, bool doAsync, CancellationToken cancellationToken)
@@ -2173,8 +2183,8 @@ namespace MimeKit {
if (dkimSignature == null)
throw new ArgumentNullException (nameof (dkimSignature));
- if (dkimSignature.Id != HeaderId.DkimSignature)
- throw new ArgumentException ("The dkimSignature parameter MUST be a DKIM-Signature header.", nameof (dkimSignature));
+ if (dkimSignature.Id != HeaderId.DkimSignature && dkimSignature.Id != HeaderId.ArcMessageSignature)
+ throw new ArgumentException ("The dkimSignature parameter MUST be a DKIM-Signature or ARC-Message-Signature header.", nameof (dkimSignature));
if (publicKeyLocator == null)
throw new ArgumentNullException (nameof (publicKeyLocator));
@@ -2187,8 +2197,8 @@ namespace MimeKit {
string[] headers;
int maxLength;
- ValidateDkimSignatureParameters (parameters, out signatureAlgorithm, out headerAlgorithm, out bodyAlgorithm,
- out d, out s, out q, out headers, out bh, out b, out maxLength);
+ ValidateSignatureParameters (parameters, dkimSignature.Id, out signatureAlgorithm, out headerAlgorithm, out bodyAlgorithm,
+ out d, out s, out q, out headers, out bh, out b, out maxLength);
if (doAsync)
key = await publicKeyLocator.LocatePublicKeyAsync (q, d, s, cancellationToken).ConfigureAwait (false);
@@ -2210,9 +2220,9 @@ namespace MimeKit {
DkimWriteHeaders (options, headers, headerAlgorithm, filtered);
- // now include the DKIM-Signature header that we are verifying,
+ // now include the DKIM-Signature or ARC-Message-Signature header that we are verifying,
// but only after removing the "b=" signature value.
- var header = GetSignedDkimSignatureHeader (dkimSignature);
+ var header = GetSignedSignatureHeader (dkimSignature);
switch (headerAlgorithm) {
case DkimCanonicalizationAlgorithm.Relaxed:
I need test cases for this.
I need test cases for this.
Gmail adds ARC to all the emails so thats one way to get them. Alternatively the python dkimpy project supports arc signing and could be used to generate examples.
A list of arc implementations can be found here http://arc-spec.org/?page_id=79 although currently there are not meany...
This is WAY WAY WAY more work than just passing the ARC-Message-Signature header into the same validator method used by DKIM-Signature :-\
Here's the start of some validator unit tests for anyone who decides to take on the task of implementing support for ARC validation: https://gist.github.com/jstedfast/36736af88424fa380c0ed1f382532f90
Here's a link to the draft tracker: https://datatracker.ietf.org/doc/draft-ietf-dmarc-arc-protocol/
Latest draft is https://tools.ietf.org/id/draft-ietf-dmarc-arc-protocol-23.txt
There are some failing tests that I can't figure out. Maybe one of you guys can look it over and figure out why they are failing.
Okay, all but 2 or 3 ARC unit tests now pass and the ones that still fail are questionable.
@The-Nutty please take a look at the latest DkimSigner
API: https://github.com/jstedfast/MimeKit/blob/master/MimeKit/Cryptography/DkimSigner.cs
Specifically, take a look at the Sign()
methods and help me come up with an API for generating ARC headers on a MimeMessage
.
Questions I need answers to:
ArcSigner
class's Sign()
method take an ArcVerifier
so that it can verify any existing ARC chains? Or should the caller supply the cv=
value? Or should it just assume "pass"
(or "none"
if it is the first ARC set)?i=
value to use or should ArcSigner
be responsible for that?ARC-Authentication-Results
be generated?Also feel free to comment on the ArcVerifier
API - is the current tri-state enum what people want? Or should it return more information? If so, what additional information should it return?
As for the ArcVerifier i think the one this that is defiantly would be good is returning the "i=" value as then the caller can at least find the corresponding ARC-Authentication-Results and do as they want with that data, perhaps its just worth returning those headers but im not sure that would fit into the api very well.
Furthermore it might be worth returning (for each step in the chain) the hostname of the party that signed the arc-seal (for example google.com), the "cv=" and the "i=" values. I say this as im aware that currently arc implementing services (for example google) dont blindly trust any arc implementing party but instead have a white list of people who they trust. By returning this extra data it would allow people to implement a similar white list (for example http://arc-spec.org/?page_id=155).
I really appropriate all the work you are doing to support arc if you have any other questions let me know.
Well, just wrote an [ARC-]Authentication-Results parser/serializer. Yikes that was a ton of work...
I hope there's some donations in this for me 😅
Ok, I've got a working ArcSigner now complete with unit tests.
Take a look at https://github.com/jstedfast/MimeKit/blob/master/UnitTests/Cryptography/ArcSignerTests.cs and https://github.com/jstedfast/MimeKit/blob/master/UnitTests/Cryptography/DummyArcSigner.cs to see how to use this new API.
Currently, the 2 methods a subclass MUST implement are the sync & async versions of GetArcAuthenticationResults/Async().
There's an Async version because a subclass MAY wish to invoke the ArcVerifier and potentially the DkimVerifier as well when generating the ARC-Authentication-Results in order to populate the authentication methods and their results.
The DummyArcSigner currently just parses and merges any existing Authentication-Results headers and uses the result.
Thanks, i have taken a look and that looks really good, i seriously appreciate the work you have put into this.
Going back to what you suggested the other day, is this what you were thinking as far as what ArcVerifier.Verify()
should return?
/// <summary>
/// An ARC validation result.
/// </summary>
/// <remarks>
/// An ARC validation result.
/// </remarks>
public enum ArcValidationResult
{
/// <summary>
/// No validation was performed.
/// </summary>
None,
/// <summary>
/// The validation passed.
/// </summary>
Pass,
/// <summary>
/// The validation failed.
/// </summary>
Fail
}
/// <summary>
/// An ARC header and its validation result.
/// </summary>
public class ArcHeader
{
public ArcValidationResult Result;
public Header Header;
}
/// <summary>
/// The results of verifying the ARC signatures of a message.
/// </summary>
class ArcVerifyResult // or maybe ArcStatus or ArcValidationStatus? or ArcSignatures?
{
public ArcHeader MessageSignature;
public ArcHeader[] Seals;
public ArcValidationResult Chain;
}
I'm not sure about the naming convention, here, but the general idea is what I think you were hinting at.
The index into the Seals
list would be the ARC instance adjusted for a 0-base (i.e. instance - 1
).
Yeah, that seems to be basically it, you can now gather all the important data (instance and signing party from the seal headers).
Few comments though:
Im not sure i see the use case for ArcHeader MessageSignature
(im assuming this is the Arc-Message-Signature header with the highest instance value?).
I would be tempted to include the last Arc-Authentication-Results but i think thats a judgement call as its not directly related to result of arc verification and it can easily be gotten once you have the instance (This might come down to do you want the method result to be full Arc status or just the verification results).
Correct, the ArcHeader MessageSignature
would be the latest ARC-Message-Signature
header which is the only ARC-Message-Signature
that the verifier checks (as per the specs). The reason I included it is because it tells you whether the message itself has been modified or not since the latest ARC headers were generated.
Each ARC-Seal
only signs the previous ARC headers (including the other ARC headers with the same instance value) and so do not tell you anything about the validity of the message itself.
The Chain
status gives you the high-level valid vs invalid state which is all ArcVerifier.Verify()
currently returns right now.
Adding the ARC-Authentication-Results
isn't unreasonable. I guess the question there is, do we want the Header
? Or the AuthenticationResults
? Consistency would dictate the Header
be supplied, but the AuthenticationResults
might be more useful.
That brings me to the question of whether or not it's really all that useful to have the Header
for the other ARC headers as well. Their signature validity is the most valuable piece of info.
Ah of course yeah.
For ARC-Authentication-Results i think adding the Header
makes the most sense as then the caller has access to the raw value which may be useful (although looking through the AuthenticationResults
no info should be lost by doing AuthenticationResults.toString()
just potentially re formatted) and furthermore it can easily be parsed by the caller to an AuthenticationResults
but personally i dont think there is much in it.
As for including headers at all, i think its a nice QOL thing but it is all extra info that can be gathered by the caller from the headers and the instance.
I think we can call this done now 😅🎉🎊
Verifying DKIM signatures against forwarded messages will fail. The forwarder (receiver of the original email) will have verified the DKIM signature and ideally re-signed the message with an ARC signature.
Feature request for MimeKit: Verifying ARC signatures
Info: http://arc-spec.org/ RFC: https://tools.ietf.org/html/draft-andersen-arc-00 (Not sure if this is the latest one)