diff --git a/src/CoderPatros.Jss/Crypto/Algorithms/EcdsaAlgorithm.cs b/src/CoderPatros.Jss/Crypto/Algorithms/EcdsaAlgorithm.cs
index 577769a..8ceb253 100644
--- a/src/CoderPatros.Jss/Crypto/Algorithms/EcdsaAlgorithm.cs
+++ b/src/CoderPatros.Jss/Crypto/Algorithms/EcdsaAlgorithm.cs
@@ -1,52 +1,89 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) Patrick Dwyer. All Rights Reserved.
-using System.Security.Cryptography;
using CoderPatros.Jss.Keys;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Crypto.Signers;
+using Org.BouncyCastle.Math;
namespace CoderPatros.Jss.Crypto.Algorithms;
///
-/// ECDSA signature algorithm. Uses SignHash/VerifyHash since JSS pre-hashes the data.
-/// ECDSA SignHash does not require knowing the hash algorithm name.
+/// ECDSA signature algorithm using BouncyCastle.
+/// Uses ECDsaSigner with IEEE P1363 encoding since JSS pre-hashes the data.
///
internal sealed class EcdsaAlgorithm : ISignatureAlgorithm
{
public string AlgorithmId { get; }
private readonly string _expectedCurveOid;
+ private readonly int _fieldSize;
public EcdsaAlgorithm(string algorithmId)
{
AlgorithmId = algorithmId;
- _expectedCurveOid = algorithmId switch
+ (_expectedCurveOid, _fieldSize) = algorithmId switch
{
- "ES256" => "1.2.840.10045.3.1.7", // P-256
- "ES384" => "1.3.132.0.34", // P-384
- "ES512" => "1.3.132.0.35", // P-521
+ "ES256" => ("1.2.840.10045.3.1.7", 32), // P-256
+ "ES384" => ("1.3.132.0.34", 48), // P-384
+ "ES512" => ("1.3.132.0.35", 66), // P-521
_ => throw new ArgumentException($"Unknown ECDSA algorithm: {algorithmId}")
};
}
public byte[] Sign(ReadOnlySpan hash, SigningKey key)
{
- if (key.KeyMaterial is not ECDsa ecdsa)
+ if (key.KeyMaterial is not ECPrivateKeyParameters ecKey)
throw new JssException($"Algorithm {AlgorithmId} requires an ECDsa key.");
- ValidateCurve(ecdsa);
- return ecdsa.SignHash(hash.ToArray(), DSASignatureFormat.IeeeP1363FixedFieldConcatenation);
+ ValidateCurve(ecKey.Parameters);
+
+ var signer = new ECDsaSigner();
+ signer.Init(true, ecKey);
+ var components = signer.GenerateSignature(hash.ToArray());
+ return EncodeIeeeP1363(components[0], components[1]);
}
public bool Verify(ReadOnlySpan hash, ReadOnlySpan signature, VerificationKey key)
{
- if (key.KeyMaterial is not ECDsa ecdsa)
+ if (key.KeyMaterial is not ECPublicKeyParameters ecKey)
throw new JssException("Invalid key type for ECDSA verification.");
- ValidateCurve(ecdsa);
- return ecdsa.VerifyHash(hash.ToArray(), signature.ToArray(), DSASignatureFormat.IeeeP1363FixedFieldConcatenation);
+ ValidateCurve(ecKey.Parameters);
+
+ var (r, s) = DecodeIeeeP1363(signature);
+ var signer = new ECDsaSigner();
+ signer.Init(false, ecKey);
+ return signer.VerifySignature(hash.ToArray(), r, s);
+ }
+
+ private byte[] EncodeIeeeP1363(BigInteger r, BigInteger s)
+ {
+ var result = new byte[_fieldSize * 2];
+ PadBigInteger(r, result, 0, _fieldSize);
+ PadBigInteger(s, result, _fieldSize, _fieldSize);
+ return result;
+ }
+
+ private static void PadBigInteger(BigInteger value, byte[] dest, int offset, int length)
+ {
+ var bytes = value.ToByteArrayUnsigned();
+ var copyLen = Math.Min(bytes.Length, length);
+ Array.Copy(bytes, 0, dest, offset + length - copyLen, copyLen);
+ }
+
+ private (BigInteger R, BigInteger S) DecodeIeeeP1363(ReadOnlySpan signature)
+ {
+ if (signature.Length != _fieldSize * 2)
+ throw new JssException($"Invalid ECDSA signature length for {AlgorithmId}: expected {_fieldSize * 2}, got {signature.Length}.");
+ var r = new BigInteger(1, signature[.._fieldSize].ToArray());
+ var s = new BigInteger(1, signature[_fieldSize..].ToArray());
+ return (r, s);
}
- private void ValidateCurve(ECDsa ecdsa)
+ private void ValidateCurve(ECDomainParameters parameters)
{
- var curveOid = ecdsa.ExportParameters(false).Curve.Oid?.Value;
- if (curveOid != _expectedCurveOid)
- throw new JssException($"Algorithm {AlgorithmId} requires curve OID {_expectedCurveOid}, but key uses {curveOid}.");
+ // Look up expected curve and compare domain parameters
+ var expectedCurve = Org.BouncyCastle.Asn1.X9.ECNamedCurveTable.GetByOid(
+ new Org.BouncyCastle.Asn1.DerObjectIdentifier(_expectedCurveOid));
+ if (expectedCurve == null || !parameters.Curve.Equals(expectedCurve.Curve) || !parameters.G.Equals(expectedCurve.G))
+ throw new JssException($"Algorithm {AlgorithmId} requires curve OID {_expectedCurveOid}, but key uses a different curve.");
}
}
diff --git a/src/CoderPatros.Jss/Crypto/Algorithms/RsaPkcs1Algorithm.cs b/src/CoderPatros.Jss/Crypto/Algorithms/RsaPkcs1Algorithm.cs
index a5bc5cc..a20d9b5 100644
--- a/src/CoderPatros.Jss/Crypto/Algorithms/RsaPkcs1Algorithm.cs
+++ b/src/CoderPatros.Jss/Crypto/Algorithms/RsaPkcs1Algorithm.cs
@@ -1,15 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) Patrick Dwyer. All Rights Reserved.
-using System.Security.Cryptography;
using CoderPatros.Jss.Keys;
+using Org.BouncyCastle.Asn1;
+using Org.BouncyCastle.Asn1.Nist;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Crypto.Digests;
+using Org.BouncyCastle.Crypto.Signers;
namespace CoderPatros.Jss.Crypto.Algorithms;
///
-/// RSA PKCS#1 v1.5 signature algorithm. Uses SignHash/VerifyHash since JSS pre-hashes the data.
-/// The hash algorithm name for RSA is inferred from the hash length since JSS separates
-/// the hash algorithm from the signing algorithm.
+/// RSA PKCS#1 v1.5 signature algorithm using BouncyCastle.
+/// Uses RsaDigestSigner with NullDigest since JSS pre-hashes the data.
///
internal sealed class RsaPkcs1Algorithm : ISignatureAlgorithm
{
@@ -23,33 +26,43 @@ public RsaPkcs1Algorithm(string algorithmId)
public byte[] Sign(ReadOnlySpan hash, SigningKey key)
{
- if (key.KeyMaterial is not RSA rsa)
+ if (key.KeyMaterial is not RsaPrivateCrtKeyParameters rsaKey)
throw new JssException($"Algorithm {AlgorithmId} requires an RSA key.");
- ValidateKeySize(rsa);
- var hashAlgorithmName = InferHashAlgorithm(hash.Length);
- return rsa.SignHash(hash.ToArray(), hashAlgorithmName, RSASignaturePadding.Pkcs1);
+ ValidateKeySize(rsaKey.Modulus.BitLength);
+
+ var oid = InferHashOid(hash.Length);
+ var signer = new RsaDigestSigner(new NullDigest(), oid);
+ signer.Init(true, rsaKey);
+ var hashArray = hash.ToArray();
+ signer.BlockUpdate(hashArray, 0, hashArray.Length);
+ return signer.GenerateSignature();
}
public bool Verify(ReadOnlySpan hash, ReadOnlySpan signature, VerificationKey key)
{
- if (key.KeyMaterial is not RSA rsa)
+ if (key.KeyMaterial is not RsaKeyParameters rsaKey)
throw new JssException("Invalid key type for RSA verification.");
- ValidateKeySize(rsa);
- var hashAlgorithmName = InferHashAlgorithm(hash.Length);
- return rsa.VerifyHash(hash.ToArray(), signature.ToArray(), hashAlgorithmName, RSASignaturePadding.Pkcs1);
+ ValidateKeySize(rsaKey.Modulus.BitLength);
+
+ var oid = InferHashOid(hash.Length);
+ var signer = new RsaDigestSigner(new NullDigest(), oid);
+ signer.Init(false, rsaKey);
+ var hashArray = hash.ToArray();
+ signer.BlockUpdate(hashArray, 0, hashArray.Length);
+ return signer.VerifySignature(signature.ToArray());
}
- private static HashAlgorithmName InferHashAlgorithm(int hashLength) => hashLength switch
+ private static DerObjectIdentifier InferHashOid(int hashLength) => hashLength switch
{
- 32 => HashAlgorithmName.SHA256,
- 48 => HashAlgorithmName.SHA384,
- 64 => HashAlgorithmName.SHA512,
+ 32 => NistObjectIdentifiers.IdSha256,
+ 48 => NistObjectIdentifiers.IdSha384,
+ 64 => NistObjectIdentifiers.IdSha512,
_ => throw new JssException($"Unsupported hash length: {hashLength} bytes")
};
- private static void ValidateKeySize(RSA rsa)
+ private static void ValidateKeySize(int keySizeBits)
{
- if (rsa.KeySize < MinimumRsaKeySizeBits)
- throw new JssException($"RSA key size {rsa.KeySize} bits is below the minimum of {MinimumRsaKeySizeBits} bits.");
+ if (keySizeBits < MinimumRsaKeySizeBits)
+ throw new JssException($"RSA key size {keySizeBits} bits is below the minimum of {MinimumRsaKeySizeBits} bits.");
}
}
diff --git a/src/CoderPatros.Jss/Crypto/Algorithms/RsaPssAlgorithm.cs b/src/CoderPatros.Jss/Crypto/Algorithms/RsaPssAlgorithm.cs
index 350c9d9..2f0a334 100644
--- a/src/CoderPatros.Jss/Crypto/Algorithms/RsaPssAlgorithm.cs
+++ b/src/CoderPatros.Jss/Crypto/Algorithms/RsaPssAlgorithm.cs
@@ -1,15 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) Patrick Dwyer. All Rights Reserved.
-using System.Security.Cryptography;
using CoderPatros.Jss.Keys;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Digests;
+using Org.BouncyCastle.Crypto.Engines;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Crypto.Signers;
namespace CoderPatros.Jss.Crypto.Algorithms;
///
-/// RSA-PSS signature algorithm. Uses SignHash/VerifyHash since JSS pre-hashes the data.
-/// The hash algorithm name for RSA is inferred from the hash length since JSS separates
-/// the hash algorithm from the signing algorithm.
+/// RSA-PSS signature algorithm using BouncyCastle.
+/// Uses PssSigner with a special digest for content (since JSS pre-hashes)
+/// and the appropriate SHA digest for MGF1.
///
internal sealed class RsaPssAlgorithm : ISignatureAlgorithm
{
@@ -23,33 +27,128 @@ public RsaPssAlgorithm(string algorithmId)
public byte[] Sign(ReadOnlySpan hash, SigningKey key)
{
- if (key.KeyMaterial is not RSA rsa)
+ if (key.KeyMaterial is not RsaPrivateCrtKeyParameters rsaKey)
throw new JssException($"Algorithm {AlgorithmId} requires an RSA key.");
- ValidateKeySize(rsa);
- var hashAlgorithmName = InferHashAlgorithm(hash.Length);
- return rsa.SignHash(hash.ToArray(), hashAlgorithmName, RSASignaturePadding.Pss);
+ ValidateKeySize(rsaKey.Modulus.BitLength);
+
+ var signer = CreatePssSigner(hash.Length);
+ signer.Init(true, rsaKey);
+ var hashArray = hash.ToArray();
+ signer.BlockUpdate(hashArray, 0, hashArray.Length);
+ return signer.GenerateSignature();
}
public bool Verify(ReadOnlySpan hash, ReadOnlySpan signature, VerificationKey key)
{
- if (key.KeyMaterial is not RSA rsa)
+ if (key.KeyMaterial is not RsaKeyParameters rsaKey)
throw new JssException("Invalid key type for RSA-PSS verification.");
- ValidateKeySize(rsa);
- var hashAlgorithmName = InferHashAlgorithm(hash.Length);
- return rsa.VerifyHash(hash.ToArray(), signature.ToArray(), hashAlgorithmName, RSASignaturePadding.Pss);
+ ValidateKeySize(rsaKey.Modulus.BitLength);
+
+ var signer = CreatePssSigner(hash.Length);
+ signer.Init(false, rsaKey);
+ var hashArray = hash.ToArray();
+ signer.BlockUpdate(hashArray, 0, hashArray.Length);
+ return signer.VerifySignature(signature.ToArray());
+ }
+
+ private static PssSigner CreatePssSigner(int hashLength)
+ {
+ var (realDigest, saltLen) = CreateDigest(hashLength);
+ return new PssSigner(new RsaBlindedEngine(), new PreHashDigest(realDigest), realDigest, saltLen);
}
- private static HashAlgorithmName InferHashAlgorithm(int hashLength) => hashLength switch
+ private static (IDigest Digest, int SaltLength) CreateDigest(int hashLength) => hashLength switch
{
- 32 => HashAlgorithmName.SHA256,
- 48 => HashAlgorithmName.SHA384,
- 64 => HashAlgorithmName.SHA512,
+ 32 => (new Sha256Digest(), 32),
+ 48 => (new Sha384Digest(), 48),
+ 64 => (new Sha512Digest(), 64),
_ => throw new JssException($"Unsupported hash length: {hashLength} bytes")
};
- private static void ValidateKeySize(RSA rsa)
+ private static void ValidateKeySize(int keySizeBits)
+ {
+ if (keySizeBits < MinimumRsaKeySizeBits)
+ throw new JssException($"RSA key size {keySizeBits} bits is below the minimum of {MinimumRsaKeySizeBits} bits.");
+ }
+
+ ///
+ /// A digest wrapper for pre-hashed data with PssSigner.
+ /// PssSigner reuses the content digest: first DoFinal extracts mHash from user input,
+ /// then subsequent calls compute H = Hash(0x00^8 || mHash || salt).
+ /// This wrapper passes through on the first DoFinal, then delegates to a real hash.
+ ///
+ private sealed class PreHashDigest : IDigest
{
- if (rsa.KeySize < MinimumRsaKeySizeBits)
- throw new JssException($"RSA key size {rsa.KeySize} bits is below the minimum of {MinimumRsaKeySizeBits} bits.");
+ private readonly IDigest _realDigest;
+ private readonly MemoryStream _buffer = new();
+ private bool _firstDoFinalDone;
+
+ public PreHashDigest(IDigest realDigest)
+ {
+ _realDigest = realDigest;
+ }
+
+ public string AlgorithmName => _realDigest.AlgorithmName;
+ public int GetDigestSize() => _realDigest.GetDigestSize();
+ public int GetByteLength() => _realDigest.GetByteLength();
+
+ public void Update(byte input)
+ {
+ if (_firstDoFinalDone)
+ _realDigest.Update(input);
+ else
+ _buffer.WriteByte(input);
+ }
+
+ public void BlockUpdate(byte[] input, int inOff, int inLen)
+ {
+ if (_firstDoFinalDone)
+ _realDigest.BlockUpdate(input, inOff, inLen);
+ else
+ _buffer.Write(input, inOff, inLen);
+ }
+
+ public int DoFinal(byte[] output, int outOff)
+ {
+ if (!_firstDoFinalDone)
+ {
+ _firstDoFinalDone = true;
+ var data = _buffer.ToArray();
+ _buffer.SetLength(0);
+ Array.Copy(data, 0, output, outOff, data.Length);
+ return data.Length;
+ }
+ return _realDigest.DoFinal(output, outOff);
+ }
+
+ public void Reset()
+ {
+ _firstDoFinalDone = false;
+ _buffer.SetLength(0);
+ _realDigest.Reset();
+ }
+
+#if NET6_0_OR_GREATER
+ public void BlockUpdate(ReadOnlySpan input)
+ {
+ if (_firstDoFinalDone)
+ _realDigest.BlockUpdate(input);
+ else
+ _buffer.Write(input);
+ }
+
+ public int DoFinal(Span output)
+ {
+ if (!_firstDoFinalDone)
+ {
+ _firstDoFinalDone = true;
+ var data = _buffer.ToArray();
+ _buffer.SetLength(0);
+ data.CopyTo(output);
+ return data.Length;
+ }
+ return _realDigest.DoFinal(output);
+ }
+#endif
}
}
diff --git a/src/CoderPatros.Jss/Keys/CertificateHelper.cs b/src/CoderPatros.Jss/Keys/CertificateHelper.cs
index 59d897e..ede7f2f 100644
--- a/src/CoderPatros.Jss/Keys/CertificateHelper.cs
+++ b/src/CoderPatros.Jss/Keys/CertificateHelper.cs
@@ -32,25 +32,25 @@ public static VerificationKey ExtractPublicKey(IReadOnlyList certChain,
throw new JssException("Certificate chain is empty.");
var cert = ParseCertificate(certChain[0]);
+ var bcKey = PublicKeyFactory.CreateKey(cert.PublicKey.ExportSubjectPublicKeyInfo());
if (algorithm.StartsWith("ES", StringComparison.Ordinal))
{
- var ecdsa = cert.GetECDsaPublicKey()
- ?? throw new JssException("Certificate does not contain an ECDSA public key.");
- return VerificationKey.FromECDsa(ecdsa);
+ if (bcKey is not ECPublicKeyParameters ecKey)
+ throw new JssException("Certificate does not contain an ECDSA public key.");
+ return VerificationKey.FromECDsa(ecKey);
}
if (algorithm.StartsWith("RS", StringComparison.Ordinal) ||
algorithm.StartsWith("PS", StringComparison.Ordinal))
{
- var rsa = cert.GetRSAPublicKey()
- ?? throw new JssException("Certificate does not contain an RSA public key.");
- return VerificationKey.FromRsa(rsa);
+ if (bcKey is not RsaKeyParameters rsaKey)
+ throw new JssException("Certificate does not contain an RSA public key.");
+ return VerificationKey.FromRsa(rsaKey);
}
if (algorithm is "Ed25519" or "Ed448")
{
- var bcKey = PublicKeyFactory.CreateKey(cert.PublicKey.ExportSubjectPublicKeyInfo());
return bcKey switch
{
Ed25519PublicKeyParameters ed25519 => VerificationKey.FromEdDsa(ed25519.GetEncoded(), "Ed25519"),
diff --git a/src/CoderPatros.Jss/Keys/PemKeyHelper.cs b/src/CoderPatros.Jss/Keys/PemKeyHelper.cs
index ee3a563..81b766a 100644
--- a/src/CoderPatros.Jss/Keys/PemKeyHelper.cs
+++ b/src/CoderPatros.Jss/Keys/PemKeyHelper.cs
@@ -1,15 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) Patrick Dwyer. All Rights Reserved.
-using System.Security.Cryptography;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
+using Org.BouncyCastle.X509;
namespace CoderPatros.Jss.Keys;
///
-/// Helpers for parsing PEM body (base64 DER SubjectPublicKeyInfo) to .NET key objects,
-/// and for exporting .NET key objects to PEM body format.
+/// Helpers for parsing PEM body (base64 DER SubjectPublicKeyInfo) to BouncyCastle key objects,
+/// and for exporting BouncyCastle key objects to PEM body format.
/// JSS uses PEM body without -----BEGIN/END----- lines.
///
public static class PemKeyHelper
@@ -29,25 +32,26 @@ public static VerificationKey ParsePublicKey(string pemBody, string algorithm)
}
var der = Convert.FromBase64String(padded);
+ var bcKey = PublicKeyFactory.CreateKey(der);
+
if (algorithm.StartsWith("ES", StringComparison.Ordinal))
{
- var ecdsa = ECDsa.Create();
- ecdsa.ImportSubjectPublicKeyInfo(der, out _);
- return VerificationKey.FromECDsa(ecdsa);
+ if (bcKey is not ECPublicKeyParameters ecKey)
+ throw new JssException($"Expected ECDSA public key for algorithm {algorithm}.");
+ return VerificationKey.FromECDsa(ecKey);
}
if (algorithm.StartsWith("RS", StringComparison.Ordinal) ||
algorithm.StartsWith("PS", StringComparison.Ordinal))
{
- var rsa = RSA.Create();
- rsa.ImportSubjectPublicKeyInfo(der, out _);
- return VerificationKey.FromRsa(rsa);
+ if (bcKey is not RsaKeyParameters rsaKey)
+ throw new JssException($"Expected RSA public key for algorithm {algorithm}.");
+ return VerificationKey.FromRsa(rsaKey);
}
if (algorithm is "Ed25519" or "Ed448")
{
- var bcPublicKey = PublicKeyFactory.CreateKey(der);
- return bcPublicKey switch
+ return bcKey switch
{
Ed25519PublicKeyParameters ed25519 => VerificationKey.FromEdDsa(ed25519.GetEncoded(), "Ed25519"),
Ed448PublicKeyParameters ed448 => VerificationKey.FromEdDsa(ed448.GetEncoded(), "Ed448"),
@@ -59,59 +63,37 @@ public static VerificationKey ParsePublicKey(string pemBody, string algorithm)
}
///
- /// Export the SubjectPublicKeyInfo of an ECDsa key as a PEM body (base64, no header/footer).
- ///
- public static string ExportPublicKeyPemBody(ECDsa key)
- {
- var spki = key.ExportSubjectPublicKeyInfo();
- return Convert.ToBase64String(spki);
- }
-
- ///
- /// Export the SubjectPublicKeyInfo of an RSA key as a PEM body.
- ///
- public static string ExportPublicKeyPemBody(RSA key)
- {
- var spki = key.ExportSubjectPublicKeyInfo();
- return Convert.ToBase64String(spki);
- }
-
- ///
- /// Export an EdDSA public key as a PEM body (SubjectPublicKeyInfo format).
+ /// Export the SubjectPublicKeyInfo of a BouncyCastle public key as a PEM body (base64, no header/footer).
///
- public static string ExportEdDsaPublicKeyPemBody(byte[] publicKey, string curve)
+ public static string ExportPublicKeyPemBody(AsymmetricKeyParameter publicKey)
{
- Org.BouncyCastle.Crypto.AsymmetricKeyParameter bcKey = curve switch
- {
- "Ed25519" => new Ed25519PublicKeyParameters(publicKey, 0),
- "Ed448" => new Ed448PublicKeyParameters(publicKey, 0),
- _ => throw new JssException($"Unsupported EdDSA curve: {curve}")
- };
-
- var info = Org.BouncyCastle.X509.SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(bcKey);
- return Convert.ToBase64String(info.GetEncoded());
+ var spki = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(publicKey);
+ return Convert.ToBase64String(spki.GetEncoded());
}
///
- /// Generate a key pair for the given algorithm and return (SigningKey, PEM body of public key).
+ /// Generate a key pair for the given algorithm and return (SigningKey, VerificationKey, PEM body of public key).
///
public static (SigningKey Signing, VerificationKey Verification, string PublicKeyPemBody) GenerateKeyPair(string algorithm, int rsaKeySize = 2048)
{
if (algorithm.StartsWith("ES", StringComparison.Ordinal))
{
- var curve = algorithm switch
+ var curveName = algorithm switch
{
- "ES256" => ECCurve.NamedCurves.nistP256,
- "ES384" => ECCurve.NamedCurves.nistP384,
- "ES512" => ECCurve.NamedCurves.nistP521,
+ "ES256" => "P-256",
+ "ES384" => "P-384",
+ "ES512" => "P-521",
_ => throw new JssException($"Unsupported ECDSA algorithm: {algorithm}")
};
- var ecdsa = ECDsa.Create(curve);
- var pemBody = ExportPublicKeyPemBody(ecdsa);
- // Create a second ECDsa for the verification key (same key material)
- var ecdsa2 = ECDsa.Create();
- ecdsa2.ImportSubjectPublicKeyInfo(ecdsa.ExportSubjectPublicKeyInfo(), out _);
- return (SigningKey.FromECDsa(ecdsa), VerificationKey.FromECDsa(ecdsa2), pemBody);
+ var ecParams = Org.BouncyCastle.Asn1.X9.ECNamedCurveTable.GetByName(curveName);
+ var domainParams = new ECDomainParameters(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed());
+ var gen = new ECKeyPairGenerator();
+ gen.Init(new ECKeyGenerationParameters(domainParams, new SecureRandom()));
+ var kp = gen.GenerateKeyPair();
+ var privateKey = (ECPrivateKeyParameters)kp.Private;
+ var publicKey = (ECPublicKeyParameters)kp.Public;
+ var pemBody = ExportPublicKeyPemBody(publicKey);
+ return (SigningKey.FromECDsa(privateKey), VerificationKey.FromECDsa(publicKey), pemBody);
}
if (algorithm.StartsWith("RS", StringComparison.Ordinal) ||
@@ -119,32 +101,38 @@ public static (SigningKey Signing, VerificationKey Verification, string PublicKe
{
if (rsaKeySize < 2048)
throw new JssException($"RSA key size {rsaKeySize} bits is below the minimum of 2048 bits.");
- var rsa = RSA.Create(rsaKeySize);
- var pemBody = ExportPublicKeyPemBody(rsa);
- var rsa2 = RSA.Create();
- rsa2.ImportSubjectPublicKeyInfo(rsa.ExportSubjectPublicKeyInfo(), out _);
- return (SigningKey.FromRsa(rsa), VerificationKey.FromRsa(rsa2), pemBody);
+ var gen = new RsaKeyPairGenerator();
+ gen.Init(new RsaKeyGenerationParameters(
+ Org.BouncyCastle.Math.BigInteger.ValueOf(0x10001),
+ new SecureRandom(),
+ rsaKeySize,
+ 256));
+ var kp = gen.GenerateKeyPair();
+ var privateKey = (RsaPrivateCrtKeyParameters)kp.Private;
+ var publicKey = (RsaKeyParameters)kp.Public;
+ var pemBody = ExportPublicKeyPemBody(publicKey);
+ return (SigningKey.FromRsa(privateKey), VerificationKey.FromRsa(publicKey), pemBody);
}
if (algorithm is "Ed25519")
{
- var gen = new Org.BouncyCastle.Crypto.Generators.Ed25519KeyPairGenerator();
- gen.Init(new Ed25519KeyGenerationParameters(new Org.BouncyCastle.Security.SecureRandom()));
+ var gen = new Ed25519KeyPairGenerator();
+ gen.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
var kp = gen.GenerateKeyPair();
var privBytes = ((Ed25519PrivateKeyParameters)kp.Private).GetEncoded();
var pubBytes = ((Ed25519PublicKeyParameters)kp.Public).GetEncoded();
- var pemBody = ExportEdDsaPublicKeyPemBody(pubBytes, "Ed25519");
+ var pemBody = ExportPublicKeyPemBody((Ed25519PublicKeyParameters)kp.Public);
return (SigningKey.FromEdDsa(privBytes, "Ed25519"), VerificationKey.FromEdDsa(pubBytes, "Ed25519"), pemBody);
}
if (algorithm is "Ed448")
{
- var gen = new Org.BouncyCastle.Crypto.Generators.Ed448KeyPairGenerator();
- gen.Init(new Ed448KeyGenerationParameters(new Org.BouncyCastle.Security.SecureRandom()));
+ var gen = new Ed448KeyPairGenerator();
+ gen.Init(new Ed448KeyGenerationParameters(new SecureRandom()));
var kp = gen.GenerateKeyPair();
var privBytes = ((Ed448PrivateKeyParameters)kp.Private).GetEncoded();
var pubBytes = ((Ed448PublicKeyParameters)kp.Public).GetEncoded();
- var pemBody = ExportEdDsaPublicKeyPemBody(pubBytes, "Ed448");
+ var pemBody = ExportPublicKeyPemBody((Ed448PublicKeyParameters)kp.Public);
return (SigningKey.FromEdDsa(privBytes, "Ed448"), VerificationKey.FromEdDsa(pubBytes, "Ed448"), pemBody);
}
@@ -159,20 +147,27 @@ public static (SigningKey Signing, VerificationKey Verification, string PublicKe
{
return key.KeyMaterial switch
{
- ECDsa ecdsa => ExportPublicKeyPemBody(ecdsa),
- RSA rsa => ExportPublicKeyPemBody(rsa),
+ ECPrivateKeyParameters ecKey => ExportPublicKeyPemBody(GetEcPublicKey(ecKey)),
+ RsaPrivateCrtKeyParameters rsaKey => ExportPublicKeyPemBody(
+ new RsaKeyParameters(false, rsaKey.Modulus, rsaKey.PublicExponent)),
SigningKey.EdDsaKeyMaterial edDsa =>
- ExportEdDsaPublicKeyPemBody(GetEdDsaPublicKeyFromPrivate(edDsa.PrivateKey, edDsa.Curve), edDsa.Curve),
+ ExportPublicKeyPemBody(GetEdDsaPublicKeyParam(edDsa.PrivateKey, edDsa.Curve)),
_ => null
};
}
- private static byte[] GetEdDsaPublicKeyFromPrivate(byte[] privateKey, string curve)
+ private static ECPublicKeyParameters GetEcPublicKey(ECPrivateKeyParameters privateKey)
+ {
+ var q = privateKey.Parameters.G.Multiply(privateKey.D).Normalize();
+ return new ECPublicKeyParameters(q, privateKey.Parameters);
+ }
+
+ private static AsymmetricKeyParameter GetEdDsaPublicKeyParam(byte[] privateKey, string curve)
{
return curve switch
{
- "Ed25519" => new Ed25519PrivateKeyParameters(privateKey, 0).GeneratePublicKey().GetEncoded(),
- "Ed448" => new Ed448PrivateKeyParameters(privateKey, 0).GeneratePublicKey().GetEncoded(),
+ "Ed25519" => new Ed25519PrivateKeyParameters(privateKey, 0).GeneratePublicKey(),
+ "Ed448" => new Ed448PrivateKeyParameters(privateKey, 0).GeneratePublicKey(),
_ => throw new JssException($"Unsupported EdDSA curve: {curve}")
};
}
@@ -182,13 +177,23 @@ private static byte[] GetEdDsaPublicKeyFromPrivate(byte[] privateKey, string cur
///
public static string ExportPrivateKeyPem(SigningKey key, string algorithm)
{
- return key.KeyMaterial switch
+ AsymmetricKeyParameter bcKey = key.KeyMaterial switch
{
- ECDsa ecdsa => ecdsa.ExportPkcs8PrivateKeyPem(),
- RSA rsa => rsa.ExportPkcs8PrivateKeyPem(),
- SigningKey.EdDsaKeyMaterial edDsa => ExportEdDsaPrivateKeyPem(edDsa.PrivateKey, edDsa.Curve),
+ ECPrivateKeyParameters ecKey => ecKey,
+ RsaPrivateCrtKeyParameters rsaKey => rsaKey,
+ SigningKey.EdDsaKeyMaterial edDsa => edDsa.Curve switch
+ {
+ "Ed25519" => new Ed25519PrivateKeyParameters(edDsa.PrivateKey, 0),
+ "Ed448" => new Ed448PrivateKeyParameters(edDsa.PrivateKey, 0),
+ _ => throw new JssException($"Unsupported EdDSA curve: {edDsa.Curve}")
+ },
_ => throw new JssException("Unsupported key type for PEM export.")
};
+
+ var info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(bcKey);
+ var der = info.GetEncoded();
+ var base64 = Convert.ToBase64String(der);
+ return $"-----BEGIN PRIVATE KEY-----\n{base64}\n-----END PRIVATE KEY-----";
}
///
@@ -204,27 +209,27 @@ public static string ExportPublicKeyPem(string pemBody)
///
public static SigningKey ImportPrivateKeyPem(string pem, string algorithm)
{
+ var base64 = ExtractPemBody(pem);
+ var der = Convert.FromBase64String(base64);
+ var bcKey = PrivateKeyFactory.CreateKey(der);
+
if (algorithm.StartsWith("ES", StringComparison.Ordinal))
{
- var ecdsa = ECDsa.Create();
- ecdsa.ImportFromPem(pem);
- return SigningKey.FromECDsa(ecdsa);
+ if (bcKey is not ECPrivateKeyParameters ecKey)
+ throw new JssException($"Expected ECDSA private key for algorithm {algorithm}.");
+ return SigningKey.FromECDsa(ecKey);
}
if (algorithm.StartsWith("RS", StringComparison.Ordinal) ||
algorithm.StartsWith("PS", StringComparison.Ordinal))
{
- var rsa = RSA.Create();
- rsa.ImportFromPem(pem);
- return SigningKey.FromRsa(rsa);
+ if (bcKey is not RsaPrivateCrtKeyParameters rsaKey)
+ throw new JssException($"Expected RSA private key for algorithm {algorithm}.");
+ return SigningKey.FromRsa(rsaKey);
}
if (algorithm is "Ed25519" or "Ed448")
{
- // Extract the base64 body from PEM
- var base64 = ExtractPemBody(pem);
- var der = Convert.FromBase64String(base64);
- var bcKey = PrivateKeyFactory.CreateKey(der);
return bcKey switch
{
Ed25519PrivateKeyParameters ed25519 => SigningKey.FromEdDsa(ed25519.GetEncoded(), "Ed25519"),
@@ -245,21 +250,6 @@ public static VerificationKey ImportPublicKeyPem(string pem, string algorithm)
return ParsePublicKey(base64, algorithm);
}
- private static string ExportEdDsaPrivateKeyPem(byte[] privateKey, string curve)
- {
- Org.BouncyCastle.Crypto.AsymmetricKeyParameter bcKey = curve switch
- {
- "Ed25519" => new Ed25519PrivateKeyParameters(privateKey, 0),
- "Ed448" => new Ed448PrivateKeyParameters(privateKey, 0),
- _ => throw new JssException($"Unsupported EdDSA curve: {curve}")
- };
-
- var info = Org.BouncyCastle.Pkcs.PrivateKeyInfoFactory.CreatePrivateKeyInfo(bcKey);
- var der = info.GetEncoded();
- var base64 = Convert.ToBase64String(der);
- return $"-----BEGIN PRIVATE KEY-----\n{base64}\n-----END PRIVATE KEY-----";
- }
-
private static string ExtractPemBody(string pem)
{
var lines = pem.Split('\n', StringSplitOptions.RemoveEmptyEntries);
diff --git a/src/CoderPatros.Jss/Keys/SigningKey.cs b/src/CoderPatros.Jss/Keys/SigningKey.cs
index 92b2e52..fd97333 100644
--- a/src/CoderPatros.Jss/Keys/SigningKey.cs
+++ b/src/CoderPatros.Jss/Keys/SigningKey.cs
@@ -2,6 +2,7 @@
// Copyright (c) Patrick Dwyer. All Rights Reserved.
using System.Security.Cryptography;
+using Org.BouncyCastle.Crypto.Parameters;
namespace CoderPatros.Jss.Keys;
@@ -20,8 +21,8 @@ private SigningKey(object keyMaterial)
KeyMaterial = keyMaterial;
}
- public static SigningKey FromECDsa(ECDsa key) => new(key);
- public static SigningKey FromRsa(RSA key) => new(key);
+ public static SigningKey FromECDsa(ECPrivateKeyParameters key) => new(key);
+ public static SigningKey FromRsa(RsaPrivateCrtKeyParameters key) => new(key);
public static SigningKey FromEdDsa(byte[] privateKey, string curve) =>
new(new EdDsaKeyMaterial(privateKey, curve));
@@ -31,12 +32,6 @@ public void Dispose()
switch (KeyMaterial)
{
- case ECDsa ecdsa:
- ecdsa.Dispose();
- break;
- case RSA rsa:
- rsa.Dispose();
- break;
case EdDsaKeyMaterial edDsa:
CryptographicOperations.ZeroMemory(edDsa.PrivateKey);
break;
diff --git a/src/CoderPatros.Jss/Keys/VerificationKey.cs b/src/CoderPatros.Jss/Keys/VerificationKey.cs
index 15341bd..825f695 100644
--- a/src/CoderPatros.Jss/Keys/VerificationKey.cs
+++ b/src/CoderPatros.Jss/Keys/VerificationKey.cs
@@ -3,6 +3,7 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
+using Org.BouncyCastle.Crypto.Parameters;
namespace CoderPatros.Jss.Keys;
@@ -20,8 +21,8 @@ private VerificationKey(object keyMaterial)
KeyMaterial = keyMaterial;
}
- public static VerificationKey FromECDsa(ECDsa key) => new(key);
- public static VerificationKey FromRsa(RSA key) => new(key);
+ public static VerificationKey FromECDsa(ECPublicKeyParameters key) => new(key);
+ public static VerificationKey FromRsa(RsaKeyParameters key) => new(key);
public static VerificationKey FromEdDsa(byte[] publicKey, string curve) =>
new(new EdDsaKeyMaterial(publicKey, curve));
public static VerificationKey FromCertificate(X509Certificate2 cert) =>
@@ -33,12 +34,6 @@ public void Dispose()
switch (KeyMaterial)
{
- case ECDsa ecdsa:
- ecdsa.Dispose();
- break;
- case RSA rsa:
- rsa.Dispose();
- break;
case EdDsaKeyMaterial edDsa:
CryptographicOperations.ZeroMemory(edDsa.PublicKey);
break;
diff --git a/tests/CoderPatros.Jss.Tests/TestFixtures/KeyFixtures.cs b/tests/CoderPatros.Jss.Tests/TestFixtures/KeyFixtures.cs
index 3040bb1..c074d90 100644
--- a/tests/CoderPatros.Jss.Tests/TestFixtures/KeyFixtures.cs
+++ b/tests/CoderPatros.Jss.Tests/TestFixtures/KeyFixtures.cs
@@ -1,4 +1,3 @@
-using System.Security.Cryptography;
using CoderPatros.Jss.Keys;
using CoderPatros.Jss.Models;
using Org.BouncyCastle.Crypto.Generators;
@@ -10,12 +9,28 @@ namespace CoderPatros.Jss.Tests.TestFixtures;
internal static class KeyFixtures
{
// ECDSA keys
- public static ECDsa CreateEcdsaP256() => ECDsa.Create(ECCurve.NamedCurves.nistP256);
- public static ECDsa CreateEcdsaP384() => ECDsa.Create(ECCurve.NamedCurves.nistP384);
- public static ECDsa CreateEcdsaP521() => ECDsa.Create(ECCurve.NamedCurves.nistP521);
+ public static (ECPrivateKeyParameters Private, ECPublicKeyParameters Public) CreateEcdsaKeyPair(string curveName)
+ {
+ var ecParams = Org.BouncyCastle.Asn1.X9.ECNamedCurveTable.GetByName(curveName);
+ var domainParams = new ECDomainParameters(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed());
+ var gen = new ECKeyPairGenerator();
+ gen.Init(new ECKeyGenerationParameters(domainParams, new SecureRandom()));
+ var kp = gen.GenerateKeyPair();
+ return ((ECPrivateKeyParameters)kp.Private, (ECPublicKeyParameters)kp.Public);
+ }
// RSA keys
- public static RSA CreateRsa2048() => RSA.Create(2048);
+ public static (RsaPrivateCrtKeyParameters Private, RsaKeyParameters Public) CreateRsa2048()
+ {
+ var gen = new RsaKeyPairGenerator();
+ gen.Init(new RsaKeyGenerationParameters(
+ Org.BouncyCastle.Math.BigInteger.ValueOf(0x10001),
+ new SecureRandom(),
+ 2048,
+ 256));
+ var kp = gen.GenerateKeyPair();
+ return ((RsaPrivateCrtKeyParameters)kp.Private, (RsaKeyParameters)kp.Public);
+ }
// EdDSA Ed25519
public static (byte[] PrivateKey, byte[] PublicKey) CreateEd25519KeyPair()
@@ -42,32 +57,29 @@ public static (byte[] PrivateKey, byte[] PublicKey) CreateEd448KeyPair()
// Helper: create signing/verification key pairs with PEM body
public static (SigningKey Signing, VerificationKey Verification, string PublicKeyPemBody) CreateEcdsaKeySet(string algorithm)
{
- var ecdsa = algorithm switch
+ var curveName = algorithm switch
{
- JssAlgorithm.ES256 => CreateEcdsaP256(),
- JssAlgorithm.ES384 => CreateEcdsaP384(),
- JssAlgorithm.ES512 => CreateEcdsaP521(),
+ JssAlgorithm.ES256 => "P-256",
+ JssAlgorithm.ES384 => "P-384",
+ JssAlgorithm.ES512 => "P-521",
_ => throw new ArgumentException($"Unsupported: {algorithm}")
};
- var pemBody = PemKeyHelper.ExportPublicKeyPemBody(ecdsa);
- var ecdsa2 = ECDsa.Create();
- ecdsa2.ImportSubjectPublicKeyInfo(ecdsa.ExportSubjectPublicKeyInfo(), out _);
+ var (privateKey, publicKey) = CreateEcdsaKeyPair(curveName);
+ var pemBody = PemKeyHelper.ExportPublicKeyPemBody(publicKey);
return (
- SigningKey.FromECDsa(ecdsa),
- VerificationKey.FromECDsa(ecdsa2),
+ SigningKey.FromECDsa(privateKey),
+ VerificationKey.FromECDsa(publicKey),
pemBody
);
}
public static (SigningKey Signing, VerificationKey Verification, string PublicKeyPemBody) CreateRsaKeySet()
{
- var rsa = CreateRsa2048();
- var pemBody = PemKeyHelper.ExportPublicKeyPemBody(rsa);
- var rsa2 = RSA.Create();
- rsa2.ImportSubjectPublicKeyInfo(rsa.ExportSubjectPublicKeyInfo(), out _);
+ var (privateKey, publicKey) = CreateRsa2048();
+ var pemBody = PemKeyHelper.ExportPublicKeyPemBody(publicKey);
return (
- SigningKey.FromRsa(rsa),
- VerificationKey.FromRsa(rsa2),
+ SigningKey.FromRsa(privateKey),
+ VerificationKey.FromRsa(publicKey),
pemBody
);
}
@@ -80,7 +92,13 @@ public static (SigningKey Signing, VerificationKey Verification, string PublicKe
"Ed448" => CreateEd448KeyPair(),
_ => throw new ArgumentException($"Unsupported: {curve}")
};
- var pemBody = PemKeyHelper.ExportEdDsaPublicKeyPemBody(publicKey, curve);
+ var bcPubKey = curve switch
+ {
+ "Ed25519" => (Org.BouncyCastle.Crypto.AsymmetricKeyParameter)new Ed25519PublicKeyParameters(publicKey, 0),
+ "Ed448" => new Ed448PublicKeyParameters(publicKey, 0),
+ _ => throw new ArgumentException($"Unsupported: {curve}")
+ };
+ var pemBody = PemKeyHelper.ExportPublicKeyPemBody(bcPubKey);
return (
SigningKey.FromEdDsa(privateKey, curve),
VerificationKey.FromEdDsa(publicKey, curve),