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