Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 54 additions & 17 deletions src/CoderPatros.Jss/Crypto/Algorithms/EcdsaAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
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<byte> 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<byte> hash, ReadOnlySpan<byte> 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<byte> 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.");
}
}
51 changes: 32 additions & 19 deletions src/CoderPatros.Jss/Crypto/Algorithms/RsaPkcs1Algorithm.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
internal sealed class RsaPkcs1Algorithm : ISignatureAlgorithm
{
Expand All @@ -23,33 +26,43 @@ public RsaPkcs1Algorithm(string algorithmId)

public byte[] Sign(ReadOnlySpan<byte> 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<byte> hash, ReadOnlySpan<byte> 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.");
}
}
137 changes: 118 additions & 19 deletions src/CoderPatros.Jss/Crypto/Algorithms/RsaPssAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
internal sealed class RsaPssAlgorithm : ISignatureAlgorithm
{
Expand All @@ -23,33 +27,128 @@ public RsaPssAlgorithm(string algorithmId)

public byte[] Sign(ReadOnlySpan<byte> 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<byte> hash, ReadOnlySpan<byte> 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.");
}

/// <summary>
/// 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.
/// </summary>
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<byte> input)
{
if (_firstDoFinalDone)
_realDigest.BlockUpdate(input);
else
_buffer.Write(input);
}

public int DoFinal(Span<byte> output)
{
if (!_firstDoFinalDone)
{
_firstDoFinalDone = true;
var data = _buffer.ToArray();
_buffer.SetLength(0);
data.CopyTo(output);
return data.Length;
}
return _realDigest.DoFinal(output);
}
#endif
}
}
14 changes: 7 additions & 7 deletions src/CoderPatros.Jss/Keys/CertificateHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,25 @@ public static VerificationKey ExtractPublicKey(IReadOnlyList<string> 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"),
Expand Down
Loading