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),