Skip to content

Commit

Permalink
Abstract DKIM/ARC support into interfaces
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jstedfast committed Jan 27, 2024
1 parent da9f0d3 commit 7830400
Show file tree
Hide file tree
Showing 23 changed files with 1,124 additions and 237 deletions.
13 changes: 5 additions & 8 deletions MimeKit/Cryptography/ArcSigner.cs
Expand Up @@ -89,14 +89,11 @@ protected ArcSigner (string domain, string selector, DkimSignatureAlgorithm algo
/// <exception cref="System.ArgumentException">
/// <paramref name="key"/> is not a private key.
/// </exception>
protected ArcSigner (AsymmetricKeyParameter key, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : this (domain, selector, algorithm)
protected ArcSigner (IDkimPrivateKey key, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : this (domain, selector, algorithm)
{
if (key == null)
throw new ArgumentNullException (nameof (key));

if (!key.IsPrivate)
throw new ArgumentException ("The key must be a private key.", nameof (key));

PrivateKey = key;
}

Expand All @@ -109,7 +106,7 @@ protected ArcSigner (AsymmetricKeyParameter key, string domain, string selector,
/// <example>
/// <code language="c#" source="Examples\ArcSignerExample.cs" />
/// </example>
/// <param name="fileName">The file containing the private key.</param>
/// <param name="fileName">The file containing the private key in PEM format.</param>
/// <param name="domain">The domain that the signer represents.</param>
/// <param name="selector">The selector subdividing the domain.</param>
/// <param name="algorithm">The signature algorithm.</param>
Expand Down Expand Up @@ -157,7 +154,7 @@ protected ArcSigner (string fileName, string domain, string selector, DkimSignat
/// <remarks>
/// Creates a new <see cref="ArcSigner"/>.
/// </remarks>
/// <param name="stream">The stream containing the private key.</param>
/// <param name="stream">The stream containing the private key in PEM format.</param>
/// <param name="domain">The domain that the signer represents.</param>
/// <param name="selector">The selector subdividing the domain.</param>
/// <param name="algorithm">The signature algorithm.</param>
Expand Down Expand Up @@ -269,7 +266,7 @@ Header GenerateArcMessageSignature (FormatOptions options, MimeMessage message,
builder.Append ("; t=");
builder.AppendInvariant (t);

using (var stream = new DkimSignatureStream (CreateSigningContext ())) {
using (var stream = new DkimSignatureStream (PrivateKey.CreateSigningContext (SignatureAlgorithm))) {
using (var filtered = new FilteredStream (stream)) {
filtered.Add (options.CreateNewLineFilter ());

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

using (var stream = new DkimSignatureStream (CreateSigningContext ())) {
using (var stream = new DkimSignatureStream (PrivateKey.CreateSigningContext (SignatureAlgorithm))) {
using (var filtered = new FilteredStream (stream)) {
filtered.Add (options.CreateNewLineFilter ());

Expand Down
8 changes: 4 additions & 4 deletions MimeKit/Cryptography/ArcVerifier.cs
Expand Up @@ -385,8 +385,8 @@ async Task<bool> VerifyArcMessageSignatureAsync (FormatOptions options, MimeMess
{
DkimCanonicalizationAlgorithm headerAlgorithm, bodyAlgorithm;
DkimSignatureAlgorithm signatureAlgorithm;
AsymmetricKeyParameter key;
string d, s, q, bh, b;
IDkimPublicKey key;
string[] headers;
int maxLength;

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

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

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

options = options.Clone ();
options.NewLineFormat = NewLineFormat.Dos;

using (var stream = new DkimSignatureStream (CreateVerifyContext (algorithm, key))) {
using (var stream = new DkimSignatureStream (key.CreateVerifyContext (algorithm))) {
using (var filtered = new FilteredStream (stream)) {
filtered.Add (options.CreateNewLineFilter ());

Expand Down
87 changes: 87 additions & 0 deletions MimeKit/Cryptography/BouncyCastleDkimKey.cs
@@ -0,0 +1,87 @@
//
// BouncyCastleDkimKey.cs
//
// Author: Jeffrey Stedfast <jestedfa@microsoft.com>
//
// Copyright (c) 2013-2023 .NET Foundation and Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

using System;

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Signers;

namespace MimeKit.Cryptography {
/// <summary>
/// A base class for <see cref="BouncyCastleDkimPublicKey"/> and <see cref="BouncyCastleDkimPrivateKey" />.
/// </summary>
/// <remarks>
/// A base class for <see cref="BouncyCastleDkimPublicKey"/> and <see cref="BouncyCastleDkimPrivateKey" />.
/// </remarks>
public abstract class BouncyCastleDkimKey
{
/// <summary>
/// Get the private key.
/// </summary>
/// <remarks>
/// Gets the private key.
/// </remarks>
public AsymmetricKeyParameter Key {
get; protected set;
}

/// <summary>
/// Create a DKIM signature context.
/// </summary>
/// <remarks>
/// Creates a DKIM signature context.
/// </remarks>
/// <param name="algorithm">The DKIM signature algorithm.</param>
/// <param name="sign">If set to <c>true</c>, the context will be used for signing; otherwise, it will be used for verifying.</param>
/// <returns>The DKIM signature context.</returns>
/// <exception cref="NotSupportedException">
/// The specified <paramref name="algorithm"/> is not supported.
/// </exception>
protected IDkimSignatureContext CreateSignatureContext (DkimSignatureAlgorithm algorithm, bool sign)
{
ISigner signer;

switch (algorithm) {
case DkimSignatureAlgorithm.RsaSha1:
signer = new RsaDigestSigner (new Sha1Digest ());
break;
case DkimSignatureAlgorithm.RsaSha256:
signer = new RsaDigestSigner (new Sha256Digest ());
break;
case DkimSignatureAlgorithm.Ed25519Sha256:
signer = new Ed25519DigestSigner (new Sha256Digest ());
break;
default:
throw new NotSupportedException ($"{algorithm} is not supported.");
}

signer.Init (sign, Key);

return new BouncyCastleDkimSignatureContext (signer);
}
}
}
157 changes: 157 additions & 0 deletions MimeKit/Cryptography/BouncyCastleDkimPrivateKey.cs
@@ -0,0 +1,157 @@
//
// BouncyCastleDkimPrivateKey.cs
//
// Author: Jeffrey Stedfast <jestedfa@microsoft.com>
//
// Copyright (c) 2013-2023 .NET Foundation and Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

using System;
using System.IO;

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;

namespace MimeKit.Cryptography {
/// <summary>
/// A DKIM private key implemented using BouncyCastle.
/// </summary>
/// <remarks>
/// A DKIM private key implemented using BouncyCastle.
/// </remarks>
public class BouncyCastleDkimPrivateKey : BouncyCastleDkimKey, IDkimPrivateKey
{
/// <summary>
/// Initializes a new instance of the <see cref="BouncyCastleDkimPrivateKey"/> class.
/// </summary>
/// <remarks>
/// Creates a new <see cref="BouncyCastleDkimPrivateKey"/>.
/// </remarks>
/// <param name="key">The private key.</param>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="key"/> is <c>null</c>.
/// </exception>
/// <exception cref="System.ArgumentException">
/// <paramref name="key"/> is not a private key.
/// </exception>
public BouncyCastleDkimPrivateKey (AsymmetricKeyParameter key)
{
if (key is null)
throw new ArgumentNullException (nameof (key));

if (!key.IsPrivate)
throw new ArgumentException ("The key must be a private key.", nameof (key));

Key = key;
}

/// <summary>
/// Create a DKIM signature context suitable for signing.
/// </summary>
/// <remarks>
/// Creates a DKIM signature context suitable for signing.
/// </remarks>
/// <param name="algorithm">The DKIM signature algorithm.</param>
/// <returns>The DKIM signature context.</returns>
/// <exception cref="System.NotSupportedException">
/// The specified <paramref name="algorithm"/> is not supported.
/// </exception>
public IDkimSignatureContext CreateSigningContext (DkimSignatureAlgorithm algorithm)
{
return CreateSignatureContext (algorithm, true);
}

static AsymmetricKeyParameter LoadPrivateKey (Stream stream)
{
AsymmetricKeyParameter key = null;

using (var reader = new StreamReader (stream)) {
var pem = new PemReader (reader);

var keyObject = pem.ReadObject ();

if (keyObject is AsymmetricCipherKeyPair pair) {
key = pair.Private;
} else if (keyObject is AsymmetricKeyParameter param) {
key = param;
}
}

if (key == null || !key.IsPrivate)
throw new FormatException ("Private key not found.");

return key;
}

/// <summary>
/// Load a private key from the specified stream.
/// </summary>
/// <remarks>
/// Loads a private key from the specified stream.
/// </remarks>
/// <param name="stream">A stream containing the private DKIM key data.</param>
/// <returns>A <see cref="BouncyCastleDkimPrivateKey"/>.</returns>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="stream"/> is <c>null</c>.
/// </exception>
/// <exception cref="System.FormatException">
/// The stream did not contain a private key in PEM format.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
public static BouncyCastleDkimPrivateKey Load (Stream stream)
{
if (stream is null)
throw new ArgumentNullException (nameof (stream));

var key = LoadPrivateKey (stream);

return new BouncyCastleDkimPrivateKey (key);
}

/// <summary>
/// Load a private key from the specified file.
/// </summary>
/// <remarks>
/// Loads a private key from the specified file.
/// </remarks>
/// <param name="fileName">A file containing the private DKIM key data.</param>
/// <returns>A <see cref="BouncyCastleDkimPrivateKey"/>.</returns>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="fileName"/> is <c>null</c>.
/// </exception>
/// <exception cref="System.FormatException">
/// The stream did not contain a private key in PEM format.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
public static BouncyCastleDkimPrivateKey Load (string fileName)
{
if (fileName is null)
throw new ArgumentNullException (nameof (fileName));

using (var stream = File.OpenRead (fileName))
return Load (stream);
}
}
}

0 comments on commit 7830400

Please sign in to comment.