Skip to content

Commit 351e3b7

Browse files
committed
Abstract DKIM/ARC support into interfaces
The idea here is to provide the DKIM interfaces in the core MimeKit package, but provide the BouncyCastle implementation of said interfaces in a future MimeKit.Cryptography package. Partial fix for issue #820
1 parent 8a57bc9 commit 351e3b7

25 files changed

+1367
-240
lines changed

MimeKit/Cryptography/ArcSigner.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,11 @@ protected ArcSigner (string domain, string selector, DkimSignatureAlgorithm algo
8989
/// <exception cref="System.ArgumentException">
9090
/// <paramref name="key"/> is not a private key.
9191
/// </exception>
92-
protected ArcSigner (AsymmetricKeyParameter key, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : this (domain, selector, algorithm)
92+
protected ArcSigner (IDkimPrivateKey key, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : this (domain, selector, algorithm)
9393
{
9494
if (key == null)
9595
throw new ArgumentNullException (nameof (key));
9696

97-
if (!key.IsPrivate)
98-
throw new ArgumentException ("The key must be a private key.", nameof (key));
99-
10097
PrivateKey = key;
10198
}
10299

@@ -109,7 +106,7 @@ protected ArcSigner (AsymmetricKeyParameter key, string domain, string selector,
109106
/// <example>
110107
/// <code language="c#" source="Examples\ArcSignerExample.cs" />
111108
/// </example>
112-
/// <param name="fileName">The file containing the private key.</param>
109+
/// <param name="fileName">The file containing the private key in PEM format.</param>
113110
/// <param name="domain">The domain that the signer represents.</param>
114111
/// <param name="selector">The selector subdividing the domain.</param>
115112
/// <param name="algorithm">The signature algorithm.</param>
@@ -157,7 +154,7 @@ protected ArcSigner (string fileName, string domain, string selector, DkimSignat
157154
/// <remarks>
158155
/// Creates a new <see cref="ArcSigner"/>.
159156
/// </remarks>
160-
/// <param name="stream">The stream containing the private key.</param>
157+
/// <param name="stream">The stream containing the private key in PEM format.</param>
161158
/// <param name="domain">The domain that the signer represents.</param>
162159
/// <param name="selector">The selector subdividing the domain.</param>
163160
/// <param name="algorithm">The signature algorithm.</param>
@@ -269,7 +266,7 @@ Header GenerateArcMessageSignature (FormatOptions options, MimeMessage message,
269266
builder.Append ("; t=");
270267
builder.AppendInvariant (t);
271268

272-
using (var stream = new DkimSignatureStream (CreateSigningContext ())) {
269+
using (var stream = new DkimSignatureStream (PrivateKey.CreateSigningContext (SignatureAlgorithm))) {
273270
using (var filtered = new FilteredStream (stream)) {
274271
filtered.Add (options.CreateNewLineFilter ());
275272

@@ -323,7 +320,7 @@ Header GenerateArcSeal (FormatOptions options, int instance, string cv, long t,
323320
builder.Append ("; t=");
324321
builder.AppendInvariant (t);
325322

326-
using (var stream = new DkimSignatureStream (CreateSigningContext ())) {
323+
using (var stream = new DkimSignatureStream (PrivateKey.CreateSigningContext (SignatureAlgorithm))) {
327324
using (var filtered = new FilteredStream (stream)) {
328325
filtered.Add (options.CreateNewLineFilter ());
329326

MimeKit/Cryptography/ArcVerifier.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,8 @@ async Task<bool> VerifyArcMessageSignatureAsync (FormatOptions options, MimeMess
385385
{
386386
DkimCanonicalizationAlgorithm headerAlgorithm, bodyAlgorithm;
387387
DkimSignatureAlgorithm signatureAlgorithm;
388-
AsymmetricKeyParameter key;
389388
string d, s, q, bh, b;
389+
IDkimPublicKey key;
390390
string[] headers;
391391
int maxLength;
392392

@@ -417,7 +417,7 @@ async Task<bool> VerifyArcMessageSignatureAsync (FormatOptions options, MimeMess
417417
async Task<bool> VerifyArcSealAsync (FormatOptions options, ArcHeaderSet[] sets, int i, bool doAsync, CancellationToken cancellationToken)
418418
{
419419
DkimSignatureAlgorithm algorithm;
420-
AsymmetricKeyParameter key;
420+
IDkimPublicKey key;
421421
string d, s, q, b;
422422

423423
ValidateArcSealParameters (sets[i].ArcSealParameters, out algorithm, out d, out s, out q, out b);
@@ -430,13 +430,13 @@ async Task<bool> VerifyArcSealAsync (FormatOptions options, ArcHeaderSet[] sets,
430430
else
431431
key = PublicKeyLocator.LocatePublicKey (q, d, s, cancellationToken);
432432

433-
if ((key is RsaKeyParameters rsa) && rsa.Modulus.BitLength < MinimumRsaKeyLength)
433+
if (key.Algorithm == DkimPublicKeyAlgorithm.Rsa && key.KeySize < MinimumRsaKeyLength)
434434
return false;
435435

436436
options = options.Clone ();
437437
options.NewLineFormat = NewLineFormat.Dos;
438438

439-
using (var stream = new DkimSignatureStream (CreateVerifyContext (algorithm, key))) {
439+
using (var stream = new DkimSignatureStream (key.CreateVerifyContext (algorithm))) {
440440
using (var filtered = new FilteredStream (stream)) {
441441
filtered.Add (options.CreateNewLineFilter ());
442442

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// BouncyCastleDkimKey.cs
3+
//
4+
// Author: Jeffrey Stedfast <[email protected]>
5+
//
6+
// Copyright (c) 2013-2025 .NET Foundation and Contributors
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files (the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included in
16+
// all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
// THE SOFTWARE.
25+
//
26+
27+
using System;
28+
29+
using Org.BouncyCastle.Crypto;
30+
using Org.BouncyCastle.Crypto.Digests;
31+
using Org.BouncyCastle.Crypto.Signers;
32+
33+
namespace MimeKit.Cryptography {
34+
/// <summary>
35+
/// A base class for <see cref="BouncyCastleDkimPublicKey"/> and <see cref="BouncyCastleDkimPrivateKey" />.
36+
/// </summary>
37+
/// <remarks>
38+
/// A base class for <see cref="BouncyCastleDkimPublicKey"/> and <see cref="BouncyCastleDkimPrivateKey" />.
39+
/// </remarks>
40+
public abstract class BouncyCastleDkimKey
41+
{
42+
/// <summary>
43+
/// Get the private key.
44+
/// </summary>
45+
/// <remarks>
46+
/// Gets the private key.
47+
/// </remarks>
48+
public AsymmetricKeyParameter Key {
49+
get; protected set;
50+
}
51+
52+
/// <summary>
53+
/// Create a DKIM signature context.
54+
/// </summary>
55+
/// <remarks>
56+
/// Creates a DKIM signature context.
57+
/// </remarks>
58+
/// <param name="algorithm">The DKIM signature algorithm.</param>
59+
/// <param name="sign">If set to <c>true</c>, the context will be used for signing; otherwise, it will be used for verifying.</param>
60+
/// <returns>The DKIM signature context.</returns>
61+
/// <exception cref="NotSupportedException">
62+
/// The specified <paramref name="algorithm"/> is not supported.
63+
/// </exception>
64+
protected IDkimSignatureContext CreateSignatureContext (DkimSignatureAlgorithm algorithm, bool sign)
65+
{
66+
ISigner signer;
67+
68+
switch (algorithm) {
69+
case DkimSignatureAlgorithm.RsaSha1:
70+
signer = new RsaDigestSigner (new Sha1Digest ());
71+
break;
72+
case DkimSignatureAlgorithm.RsaSha256:
73+
signer = new RsaDigestSigner (new Sha256Digest ());
74+
break;
75+
case DkimSignatureAlgorithm.Ed25519Sha256:
76+
signer = new Ed25519DigestSigner (new Sha256Digest ());
77+
break;
78+
default:
79+
throw new NotSupportedException ($"{algorithm} is not supported.");
80+
}
81+
82+
signer.Init (sign, Key);
83+
84+
return new BouncyCastleDkimSignatureContext (signer);
85+
}
86+
}
87+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
//
2+
// BouncyCastleDkimPrivateKey.cs
3+
//
4+
// Author: Jeffrey Stedfast <[email protected]>
5+
//
6+
// Copyright (c) 2013-2025 .NET Foundation and Contributors
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files (the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included in
16+
// all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
// THE SOFTWARE.
25+
//
26+
27+
using System;
28+
using System.IO;
29+
30+
using Org.BouncyCastle.Crypto;
31+
using Org.BouncyCastle.OpenSsl;
32+
33+
namespace MimeKit.Cryptography {
34+
/// <summary>
35+
/// A DKIM private key implemented using BouncyCastle.
36+
/// </summary>
37+
/// <remarks>
38+
/// A DKIM private key implemented using BouncyCastle.
39+
/// </remarks>
40+
public class BouncyCastleDkimPrivateKey : BouncyCastleDkimKey, IDkimPrivateKey
41+
{
42+
/// <summary>
43+
/// Initializes a new instance of the <see cref="BouncyCastleDkimPrivateKey"/> class.
44+
/// </summary>
45+
/// <remarks>
46+
/// Creates a new <see cref="BouncyCastleDkimPrivateKey"/>.
47+
/// </remarks>
48+
/// <param name="key">The private key.</param>
49+
/// <exception cref="System.ArgumentNullException">
50+
/// <paramref name="key"/> is <c>null</c>.
51+
/// </exception>
52+
/// <exception cref="System.ArgumentException">
53+
/// <paramref name="key"/> is not a private key.
54+
/// </exception>
55+
public BouncyCastleDkimPrivateKey (AsymmetricKeyParameter key)
56+
{
57+
if (key is null)
58+
throw new ArgumentNullException (nameof (key));
59+
60+
if (!key.IsPrivate)
61+
throw new ArgumentException ("The key must be a private key.", nameof (key));
62+
63+
Key = key;
64+
}
65+
66+
/// <summary>
67+
/// Create a DKIM signature context suitable for signing.
68+
/// </summary>
69+
/// <remarks>
70+
/// Creates a DKIM signature context suitable for signing.
71+
/// </remarks>
72+
/// <param name="algorithm">The DKIM signature algorithm.</param>
73+
/// <returns>The DKIM signature context.</returns>
74+
/// <exception cref="System.NotSupportedException">
75+
/// The specified <paramref name="algorithm"/> is not supported.
76+
/// </exception>
77+
public IDkimSignatureContext CreateSigningContext (DkimSignatureAlgorithm algorithm)
78+
{
79+
return CreateSignatureContext (algorithm, true);
80+
}
81+
82+
static AsymmetricKeyParameter LoadPrivateKey (Stream stream)
83+
{
84+
AsymmetricKeyParameter key = null;
85+
86+
using (var reader = new StreamReader (stream)) {
87+
var pem = new PemReader (reader);
88+
89+
var keyObject = pem.ReadObject ();
90+
91+
if (keyObject is AsymmetricCipherKeyPair pair) {
92+
key = pair.Private;
93+
} else if (keyObject is AsymmetricKeyParameter param) {
94+
key = param;
95+
}
96+
}
97+
98+
if (key == null || !key.IsPrivate)
99+
throw new FormatException ("Private key not found.");
100+
101+
return key;
102+
}
103+
104+
/// <summary>
105+
/// Load a private key from the specified stream.
106+
/// </summary>
107+
/// <remarks>
108+
/// Loads a private key from the specified stream.
109+
/// </remarks>
110+
/// <param name="stream">A stream containing the private DKIM key data.</param>
111+
/// <returns>A <see cref="BouncyCastleDkimPrivateKey"/>.</returns>
112+
/// <exception cref="System.ArgumentNullException">
113+
/// <paramref name="stream"/> is <c>null</c>.
114+
/// </exception>
115+
/// <exception cref="System.FormatException">
116+
/// The stream did not contain a private key in PEM format.
117+
/// </exception>
118+
/// <exception cref="System.IO.IOException">
119+
/// An I/O error occurred.
120+
/// </exception>
121+
public static BouncyCastleDkimPrivateKey Load (Stream stream)
122+
{
123+
if (stream is null)
124+
throw new ArgumentNullException (nameof (stream));
125+
126+
var key = LoadPrivateKey (stream);
127+
128+
return new BouncyCastleDkimPrivateKey (key);
129+
}
130+
131+
/// <summary>
132+
/// Load a private key from the specified file.
133+
/// </summary>
134+
/// <remarks>
135+
/// Loads a private key from the specified file.
136+
/// </remarks>
137+
/// <param name="fileName">A file containing the private DKIM key data.</param>
138+
/// <returns>A <see cref="BouncyCastleDkimPrivateKey"/>.</returns>
139+
/// <exception cref="System.ArgumentNullException">
140+
/// <paramref name="fileName"/> is <c>null</c>.
141+
/// </exception>
142+
/// <exception cref="System.FormatException">
143+
/// The stream did not contain a private key in PEM format.
144+
/// </exception>
145+
/// <exception cref="System.IO.IOException">
146+
/// An I/O error occurred.
147+
/// </exception>
148+
public static BouncyCastleDkimPrivateKey Load (string fileName)
149+
{
150+
if (fileName is null)
151+
throw new ArgumentNullException (nameof (fileName));
152+
153+
using (var stream = File.OpenRead (fileName))
154+
return Load (stream);
155+
}
156+
}
157+
}

0 commit comments

Comments
 (0)