diff --git a/src/ScottBrady.IdentityModel/Assembly.cs b/src/ScottBrady.IdentityModel/Assembly.cs
new file mode 100644
index 0000000..cfb61cd
--- /dev/null
+++ b/src/ScottBrady.IdentityModel/Assembly.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly:InternalsVisibleTo("ScottBrady.IdentityModel.Tests")]
\ No newline at end of file
diff --git a/src/ScottBrady.IdentityModel/Crypto/ExtendedSecurityAlgorithms.cs b/src/ScottBrady.IdentityModel/Crypto/ExtendedSecurityAlgorithms.cs
index bad714d..f420d06 100644
--- a/src/ScottBrady.IdentityModel/Crypto/ExtendedSecurityAlgorithms.cs
+++ b/src/ScottBrady.IdentityModel/Crypto/ExtendedSecurityAlgorithms.cs
@@ -12,5 +12,14 @@ public static class ExtendedSecurityAlgorithms
// https://tools.ietf.org/html/rfc8037#section-5
public const string EdDsa = "EdDSA";
+
+ public class Curves
+ {
+ // https://tools.ietf.org/html/rfc8037#section-5
+ public const string Ed25519 = "Ed25519";
+ public const string Ed448 = "Ed448";
+ public const string X25519 = "X25519";
+ public const string X448 = "X448";
+ }
}
}
\ No newline at end of file
diff --git a/src/ScottBrady.IdentityModel/ScottBrady.IdentityModel.csproj b/src/ScottBrady.IdentityModel/ScottBrady.IdentityModel.csproj
index 6fc1c0d..7af1ba7 100644
--- a/src/ScottBrady.IdentityModel/ScottBrady.IdentityModel.csproj
+++ b/src/ScottBrady.IdentityModel/ScottBrady.IdentityModel.csproj
@@ -3,13 +3,13 @@
netstandard2.0
Scott Brady
- Token helpers for Branca and PASETO.
+ Token helpers for Branca, PASETO, and Ed25519.
icon.png
https://github.com/scottbrady91/IdentityModel
Copyright 2020 (c) Scott Brady
Branca PASETO Base62
true
- 1.0.0
+ 1.1.0
Apache-2.0
diff --git a/src/ScottBrady.IdentityModel/Tokens/EdDsaSecurityKey.cs b/src/ScottBrady.IdentityModel/Tokens/EdDsaSecurityKey.cs
index 5d15b29..9da8f73 100644
--- a/src/ScottBrady.IdentityModel/Tokens/EdDsaSecurityKey.cs
+++ b/src/ScottBrady.IdentityModel/Tokens/EdDsaSecurityKey.cs
@@ -2,22 +2,32 @@
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
+using ScottBrady.IdentityModel.Crypto;
namespace ScottBrady.IdentityModel.Tokens
{
public class EdDsaSecurityKey : AsymmetricSecurityKey
{
- public EdDsaSecurityKey(Ed25519PrivateKeyParameters keyParameters)
+ private EdDsaSecurityKey()
+ {
+ CryptoProviderFactory.CustomCryptoProvider = new ExtendedCryptoProvider();
+ }
+
+ public EdDsaSecurityKey(Ed25519PrivateKeyParameters keyParameters) : this()
{
KeyParameters = keyParameters ?? throw new ArgumentNullException(nameof(keyParameters));
+ Curve = ExtendedSecurityAlgorithms.Curves.Ed25519;
}
-
- public EdDsaSecurityKey(Ed25519PublicKeyParameters keyParameters)
+
+ public EdDsaSecurityKey(Ed25519PublicKeyParameters keyParameters) : this()
{
KeyParameters = keyParameters ?? throw new ArgumentNullException(nameof(keyParameters));
+ Curve = ExtendedSecurityAlgorithms.Curves.Ed25519;
}
public virtual AsymmetricKeyParameter KeyParameters { get; }
+ public string Curve { get; }
+
public override int KeySize => throw new NotImplementedException();
[Obsolete("HasPrivateKey method is deprecated, please use PrivateKeyStatus.")]
diff --git a/src/ScottBrady.IdentityModel/Tokens/EdDsaSignatureProvider.cs b/src/ScottBrady.IdentityModel/Tokens/EdDsaSignatureProvider.cs
new file mode 100644
index 0000000..5799cbc
--- /dev/null
+++ b/src/ScottBrady.IdentityModel/Tokens/EdDsaSignatureProvider.cs
@@ -0,0 +1,36 @@
+using Microsoft.IdentityModel.Tokens;
+using Org.BouncyCastle.Crypto.Signers;
+
+namespace ScottBrady.IdentityModel.Tokens
+{
+ internal class EdDsaSignatureProvider : SignatureProvider
+ {
+ private readonly EdDsaSecurityKey edDsaKey;
+
+ public EdDsaSignatureProvider(EdDsaSecurityKey key, string algorithm)
+ : base(key, algorithm)
+ {
+ edDsaKey = key;
+ }
+
+ protected override void Dispose(bool disposing) { }
+
+ public override byte[] Sign(byte[] input)
+ {
+ var signer = new Ed25519Signer();
+ signer.Init(true, edDsaKey.KeyParameters);
+ signer.BlockUpdate(input, 0, input.Length);
+
+ return signer.GenerateSignature();
+ }
+
+ public override bool Verify(byte[] input, byte[] signature)
+ {
+ var validator = new Ed25519Signer();
+ validator.Init(false, edDsaKey.KeyParameters);
+ validator.BlockUpdate(input, 0, input.Length);
+
+ return validator.VerifySignature(signature);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ScottBrady.IdentityModel/Tokens/ExtendedCryptoProvider.cs b/src/ScottBrady.IdentityModel/Tokens/ExtendedCryptoProvider.cs
new file mode 100644
index 0000000..d8820fa
--- /dev/null
+++ b/src/ScottBrady.IdentityModel/Tokens/ExtendedCryptoProvider.cs
@@ -0,0 +1,28 @@
+using System;
+using Microsoft.IdentityModel.Tokens;
+using ScottBrady.IdentityModel.Tokens;
+
+namespace ScottBrady.IdentityModel.Crypto
+{
+ internal class ExtendedCryptoProvider : ICryptoProvider
+ {
+ public bool IsSupportedAlgorithm(string algorithm, params object[] args)
+ => algorithm == ExtendedSecurityAlgorithms.EdDsa;
+
+ public object Create(string algorithm, params object[] args)
+ {
+ if (algorithm == ExtendedSecurityAlgorithms.EdDsa && args[0] is EdDsaSecurityKey key)
+ {
+ return new EdDsaSignatureProvider(key, algorithm);
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public void Release(object cryptoInstance)
+ {
+ if (cryptoInstance is IDisposable disposableObject)
+ disposableObject.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/EdDsaSecurityKeyTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/EdDsaSecurityKeyTests.cs
new file mode 100644
index 0000000..2863130
--- /dev/null
+++ b/test/ScottBrady.IdentityModel.Tests/Tokens/EdDsaSecurityKeyTests.cs
@@ -0,0 +1,61 @@
+using System;
+using FluentAssertions;
+using Microsoft.IdentityModel.Tokens;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Security;
+using ScottBrady.IdentityModel.Crypto;
+using ScottBrady.IdentityModel.Tokens;
+using Xunit;
+
+namespace ScottBrady.IdentityModel.Tests.Tokens
+{
+ public class EdDsaSecurityKeyTests
+ {
+ [Fact]
+ public void ctor_WhenKeyParametersAreNull_ExpectArgumentNullException()
+ => Assert.Throws(() => new EdDsaSecurityKey((Ed25519PublicKeyParameters) null));
+
+ [Fact]
+ public void ctor_WhenEd25519PrivateKey_ExpectKeySetAndCorrectCurve()
+ {
+ var keyPair = GenerateEd25519KeyPair();
+
+ var securityKey = new EdDsaSecurityKey((Ed25519PrivateKeyParameters) keyPair.Private);
+
+ securityKey.CryptoProviderFactory.CustomCryptoProvider.Should().BeOfType();
+ securityKey.KeyParameters.Should().Be(keyPair.Private);
+ securityKey.Curve.Should().Be(ExtendedSecurityAlgorithms.Curves.Ed25519);
+ securityKey.PrivateKeyStatus.Should().Be(PrivateKeyStatus.Exists);
+
+#pragma warning disable 618
+ securityKey.HasPrivateKey.Should().BeTrue();
+#pragma warning restore 618
+ }
+
+ [Fact]
+ public void ctor_WhenEd25519PublicKey_ExpectKeySetAndCorrectCurve()
+ {
+ var keyPair = GenerateEd25519KeyPair();
+
+ var securityKey = new EdDsaSecurityKey((Ed25519PublicKeyParameters) keyPair.Public);
+
+ securityKey.CryptoProviderFactory.CustomCryptoProvider.Should().BeOfType();
+ securityKey.KeyParameters.Should().Be(keyPair.Public);
+ securityKey.Curve.Should().Be(ExtendedSecurityAlgorithms.Curves.Ed25519);
+ securityKey.PrivateKeyStatus.Should().Be(PrivateKeyStatus.DoesNotExist);
+
+#pragma warning disable 618
+ securityKey.HasPrivateKey.Should().BeFalse();
+#pragma warning restore 618
+ }
+
+ private static AsymmetricCipherKeyPair GenerateEd25519KeyPair()
+ {
+ var keyPairGenerator = new Ed25519KeyPairGenerator();
+ keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
+ return keyPairGenerator.GenerateKeyPair();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/EdDsaSignatureProviderTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/EdDsaSignatureProviderTests.cs
new file mode 100644
index 0000000..6598fa7
--- /dev/null
+++ b/test/ScottBrady.IdentityModel.Tests/Tokens/EdDsaSignatureProviderTests.cs
@@ -0,0 +1,70 @@
+using FluentAssertions;
+using Microsoft.IdentityModel.Tokens;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Security;
+using ScottBrady.IdentityModel.Crypto;
+using ScottBrady.IdentityModel.Tokens;
+using Xunit;
+
+namespace ScottBrady.IdentityModel.Tests.Tokens
+{
+ public class EdDsaSignatureProviderTests
+ {
+ // privateKey = "FU1F1QTjYwfB-xkO6aknnBifE_Ywa94U04xpd-XJfBs"
+
+ [Fact]
+ public void ctor_ExpectPropertiesSet()
+ {
+ var keyPairGenerator = new Ed25519KeyPairGenerator();
+ keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
+ var keyPair = keyPairGenerator.GenerateKeyPair();
+
+ var expectedSecurityKey = new EdDsaSecurityKey((Ed25519PublicKeyParameters) keyPair.Public);
+ var expectedAlgorithm = ExtendedSecurityAlgorithms.EdDsa;
+
+ var provider = new EdDsaSignatureProvider(expectedSecurityKey, expectedAlgorithm);
+
+ provider.Key.Should().Be(expectedSecurityKey);
+ provider.Algorithm.Should().Be(expectedAlgorithm);
+ }
+
+ [Fact]
+ public void Sign_WhenSigningWithEd25519Curve_ExpectCorrectSignature()
+ {
+ const string plaintext =
+ "eyJraWQiOiIxMjMiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJhdWQiOiJ5b3UiLCJzdWIiOiJib2IiLCJpc3MiOiJtZSIsImV4cCI6MTU5MDg0MTg4N30";
+ const string expectedSignature =
+ "OyBxBr344Ny-0vRCeEMLSnuEO1IecybvJBivrjum4d-dgN5WLnEAGAO43MlZeRGn1F3fRXO_xlYot68PtDuiAA";
+
+ const string privateKey = "FU1F1QTjYwfB-xkO6aknnBifE_Ywa94U04xpd-XJfBs";
+ var edDsaSecurityKey = new EdDsaSecurityKey(new Ed25519PrivateKeyParameters(Base64UrlEncoder.DecodeBytes(privateKey), 0));
+
+ var signatureProvider = new EdDsaSignatureProvider(edDsaSecurityKey, ExtendedSecurityAlgorithms.EdDsa);
+
+ var signature = signatureProvider.Sign(System.Text.Encoding.UTF8.GetBytes(plaintext));
+
+ signature.Should().BeEquivalentTo(Base64UrlEncoder.DecodeBytes(expectedSignature));
+ }
+
+ [Fact]
+ public void Verify_WhenJwtSignedWithEd25519Curve_ExpectTrue()
+ {
+ const string plaintext =
+ "eyJraWQiOiIxMjMiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJhdWQiOiJ5b3UiLCJzdWIiOiJib2IiLCJpc3MiOiJtZSIsImV4cCI6MTU5MDg0MTg4N30";
+ const string signature =
+ "OyBxBr344Ny-0vRCeEMLSnuEO1IecybvJBivrjum4d-dgN5WLnEAGAO43MlZeRGn1F3fRXO_xlYot68PtDuiAA";
+
+ const string publicKey = "60mR98SQlHUSeLeIu7TeJBTLRG10qlcDLU4AJjQdqMQ";
+ var edDsaSecurityKey = new EdDsaSecurityKey(new Ed25519PublicKeyParameters(Base64UrlEncoder.DecodeBytes(publicKey), 0));
+
+ var signatureProvider = new EdDsaSignatureProvider(edDsaSecurityKey, ExtendedSecurityAlgorithms.EdDsa);
+
+ var isValidSignature = signatureProvider.Verify(
+ System.Text.Encoding.UTF8.GetBytes(plaintext),
+ Base64UrlEncoder.DecodeBytes(signature));
+
+ isValidSignature.Should().BeTrue();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/ExtendedCryptoProviderTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/ExtendedCryptoProviderTests.cs
new file mode 100644
index 0000000..9fc7a5a
--- /dev/null
+++ b/test/ScottBrady.IdentityModel.Tests/Tokens/ExtendedCryptoProviderTests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using FluentAssertions;
+using Microsoft.IdentityModel.Tokens;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Security;
+using ScottBrady.IdentityModel.Crypto;
+using ScottBrady.IdentityModel.Tokens;
+using Xunit;
+
+namespace ScottBrady.IdentityModel.Tests.Tokens
+{
+ public class ExtendedCryptoProviderTests
+ {
+ private ExtendedCryptoProvider sut = new ExtendedCryptoProvider();
+
+ [Theory]
+ [InlineData("eddsa")]
+ [InlineData("RS256")]
+ [InlineData("EDDSA")]
+ public void IsSupportedAlgorithm_WhenNotSupportedAlgorithm_ExpectFalse(string algorithm)
+ => sut.IsSupportedAlgorithm(algorithm);
+
+ [Theory]
+ [InlineData(ExtendedSecurityAlgorithms.EdDsa)]
+ public void IsSupportedAlgorithm_WhenSupportedAlgorithm_ExpectTrue(string algorithm)
+ => sut.IsSupportedAlgorithm(algorithm);
+
+ [Fact]
+ public void Release_WhenObjectImplementsIDisposable_ExpectObjectDisposed()
+ {
+ var memoryStream = new MemoryStream();
+ sut.Release(memoryStream);
+ Assert.Throws(() => memoryStream.Read(Span.Empty));
+ }
+
+ [Fact]
+ public void Release_WhenObjectDoesNotImplementIDisposable_ExpectNoOp()
+ {
+ var uri = new Uri("urn:test");
+ sut.Release(uri);
+ }
+
+ [Fact]
+ public void Create_WhenAlgorithmIsNotEdDsaButHasEdDsaSecurityKey_ExpectNotSupportedException()
+ {
+ var keyPairGenerator = new Ed25519KeyPairGenerator();
+ keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
+ var keyPair = keyPairGenerator.GenerateKeyPair();
+
+ var securityKey = new EdDsaSecurityKey((Ed25519PublicKeyParameters) keyPair.Public);
+
+ Assert.Throws(() => sut.Create(SecurityAlgorithms.RsaSha256, securityKey));
+ }
+
+ [Fact]
+ public void Create_WhenAlgorithmIsEdDsaButIsNotEdDsaSecurityKey_ExpectNotSupportedException()
+ {
+ var securityKey = new RsaSecurityKey(RSA.Create());
+
+ Assert.Throws(() => sut.Create(ExtendedSecurityAlgorithms.EdDsa, securityKey));
+ }
+
+ [Fact]
+ public void Create_WhenAlgorithmIsEdDsaWithEdDsaSecurityKey_ExpectEdDsaSignatureProvider()
+ {
+ var keyPairGenerator = new Ed25519KeyPairGenerator();
+ keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
+ var keyPair = keyPairGenerator.GenerateKeyPair();
+
+ var securityKey = new EdDsaSecurityKey((Ed25519PublicKeyParameters) keyPair.Public);
+
+ var signatureProvider = sut.Create(ExtendedSecurityAlgorithms.EdDsa, securityKey);
+
+ var edDsaSignatureProvider = Assert.IsType(signatureProvider);
+ edDsaSignatureProvider.Algorithm.Should().Be(ExtendedSecurityAlgorithms.EdDsa);
+ edDsaSignatureProvider.Key.Should().Be(securityKey);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/ScottBrady.IdentityModel.Tests/Tokens/JsonWebTokenHandlerTests.cs b/test/ScottBrady.IdentityModel.Tests/Tokens/JsonWebTokenHandlerTests.cs
new file mode 100644
index 0000000..3ba74f6
--- /dev/null
+++ b/test/ScottBrady.IdentityModel.Tests/Tokens/JsonWebTokenHandlerTests.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Security.Claims;
+using FluentAssertions;
+using Microsoft.IdentityModel.JsonWebTokens;
+using Microsoft.IdentityModel.Tokens;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Security;
+using ScottBrady.IdentityModel.Crypto;
+using ScottBrady.IdentityModel.Tokens;
+using Xunit;
+
+namespace ScottBrady.IdentityModel.Tests.Tokens
+{
+ public class JsonWebTokenHandlerTests
+ {
+ [Fact]
+ public void WhenEdDsaTokenGenerated_ExpectEdDsaTokenVerifiable()
+ {
+ const string issuer = "me";
+ const string audience = "you";
+ const string subject = "123";
+
+ var keyPairGenerator = new Ed25519KeyPairGenerator();
+ keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
+ var keyPair = keyPairGenerator.GenerateKeyPair();
+
+ var handler = new JsonWebTokenHandler();
+
+ var jwt = handler.CreateToken(new SecurityTokenDescriptor
+ {
+ Issuer = issuer,
+ Audience = audience,
+ Expires = DateTime.UtcNow.AddMinutes(30),
+ Subject = new ClaimsIdentity(new[] {new Claim("sub", subject)}),
+ SigningCredentials = new SigningCredentials(
+ new EdDsaSecurityKey((Ed25519PrivateKeyParameters) keyPair.Private),
+ ExtendedSecurityAlgorithms.EdDsa)
+ });
+
+ var validationResult = handler.ValidateToken(jwt, new TokenValidationParameters
+ {
+ ValidIssuer = issuer,
+ ValidAudience = audience,
+ ValidateLifetime = true,
+ RequireExpirationTime = true,
+ IssuerSigningKey = new EdDsaSecurityKey((Ed25519PublicKeyParameters) keyPair.Public)
+ });
+
+ validationResult.IsValid.Should().BeTrue();
+ validationResult.ClaimsIdentity.Claims.Should().Contain(x => x.Type == "sub" && x.Value == subject);
+ }
+ }
+}
\ No newline at end of file