c# - 如何使用带有 ECDSA 签名的 Bouncy CaSTLe 延迟签署证书请求?

标签 c# bouncycastle csr ecdsa pkcs#10

我正在尝试使用 C# 中的 Bouncy CaSTLe 通过 ECDSA 签名算法实现证书请求 (CSR) 的延迟签名。到目前为止,我已经设法用 RSA 而不是 ECDSA 实现了这一点。我使用来自 Bouncy CaSTLe 的 Pkcs10CertificationRequestDelaySigned 类。

验证签名时我的测试代码片段失败(下面提供完整代码):

        [TestMethod]
        public void ValidCsrWithoutPassword_Ecdsa_SignatureIsAppended()
        {
            DelayCsrProvider sut = CreateSut();

            const string signAlgorithm = "ECDSA";
            var keys = new Keys(signAlgorithm);

            // Create CSR
            var signatureAlgorithm = "SHA256withECDSA";
            byte[] octetData = CreateCsr(keys.SignKeyPair, signatureAlgorithm);

            // Append password to CSR
            byte[] csrWithPass = sut.AppendPassword(octetData, "some-text-1");

            // Calculate HASH
            var hashAlgorithm = CmsSignedGenerator.DigestSha256;
            byte[] hash = sut.BuildHash(csrWithPass, hashAlgorithm);

            // Sign using HASH
            byte[] signature = Sign(hash, signAlgorithm, hashAlgorithm, keys.SignKeyPair.Private);

            // Add signature to CSR
            byte[] csrSigned = sut.AppendSignature(csrWithPass, signature);

            // Just verify the signature matches CSR's public key + data,
            // public key should match the private key
            // this is where it fails

            Verify(csrSigned);
        }

场景是: 有两个实体(在不同机器上运行的程序)。 一个同时拥有私钥和公钥 - 称之为 Signer,另一个拥有用于使用该信息扩展证书请求但无法访问私钥的附加信息(如密码) - 称之为 DelayCsrProvider。

顺序是:

  1. 签名者创建没有密码的 CSR 并对数据签名,以 PKCS#10 格式 DER 编码将其发送到 DelayCsrProvider。

  2. DelayCsrProvider 使用收到的 CSR 中的所有信息创建新的 CSR,并添加包含密码的附加属性。 现在我们必须签署这个新的 CSR,但我们没有私钥。相反,我们计算数据的哈希 (SHA-256) 并将摘要发送给签名者。

  3. Signer 接收哈希并签署哈希,将签名发送回 DelayCsrProvider。

  4. DelayCsrProvider 将收到的签名插入 CSR,从而创建具有有效签名的完整 CSR。

我已经创建了执行上述所有步骤的 DelayCsrProvider 类和单元测试。 RSA 的一个单元测试工作正常,ECDSA 的另一个单元测试在验证签名时失败。

这里还可以做什么来解决 ECDSA 哈希签名?

查看下面代码的主要部分或从 GIT 下载整个示例:https://github.com/DmitriNymi/Certificate-Enrollment.git

注意:

.Net Framework 4.6.2 充气城堡 nuget BouncyCaSTLe.Crypto.dll 文件版本=1.8.15362.1

这是在 Assert ValidCsrWithoutPassword_Ecdsa_SignatureIsAppended() 中失败的测试的完整代码示例:

using System;
using System.Collections.Generic;
using System.Linq;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;

namespace NEnrollment.Services.DelaySigning
{
public class DelayCsrProvider
{
    /// <summary>
    /// append password to CSR: csrWithPassword = (csr, password)
    /// </summary>
    /// <param name="csr"></param>
    /// <param name="password"></param>
    /// <returns>CSR that  contains password</returns>
    public byte[] AppendPassword(byte[] csr, string password)
    {
        if (csr == null) throw new ArgumentNullException(nameof(csr));
        if (string.IsNullOrEmpty(password)) throw new ArgumentNullException(nameof(password));

        var originalCsr = new Pkcs10CertificationRequest(csr);

        CertificationRequestInfo cri = originalCsr.GetCertificationRequestInfo();

        DerSet attributesSet = AddPasswordAttribute(password, cri.Attributes);

        AsymmetricKeyParameter publicKey = PublicKeyFactory.CreateKey(cri.SubjectPublicKeyInfo);

        string signatureAlgorithm = originalCsr.SignatureAlgorithm.Algorithm.Id;

        // build new CSR from original + password attribute
        var csrWithPassword =
            new Pkcs10CertificationRequestDelaySigned(signatureAlgorithm, cri.Subject, publicKey, attributesSet);

        // this signing key is not used for signing but here only to suppress exception thrown in ctor
        csrWithPassword.SignRequest(new byte[] { });

        var csrWithPasswordBytes = csrWithPassword.GetDerEncoded();

        return csrWithPasswordBytes;
    }

    private DerSet AddPasswordAttribute(string password, Asn1Set attributes)
    {
        if (attributes == null) attributes = new DerSet();

        List<AttributePkcs> attributesPkcs = attributes
            .OfType<DerSequence>()
            .Select(AttributePkcs.GetInstance)
            .ToList();

        bool hasPassword = attributesPkcs.Any(x => x.AttrType.Equals(PkcsObjectIdentifiers.Pkcs9AtChallengePassword));
        if (hasPassword) throw new Exception("Cannot append password, already has password attribute in CSR.");

        AttributePkcs passwordAttribute = ChallengePasswordAttribute(password);

        attributesPkcs.Add(passwordAttribute);

        // ReSharper disable once CoVariantArrayConversion
        DerSet attributesSet = new DerSet(attributesPkcs.ToArray());
        return attributesSet;
    }

    private AttributePkcs ChallengePasswordAttribute(string password)
    {
        if (password == null) return null;

        Asn1EncodableVector attributeValues = new Asn1EncodableVector { new DerPrintableString(password) };

        return new AttributePkcs(PkcsObjectIdentifiers.Pkcs9AtChallengePassword, new DerSet(attributeValues));
    }

    /// <summary>
    /// Calculates hash (digest) of the given CSR using the specified hash algorithm OID
    /// </summary>
    /// <param name="csr">CSR without password</param>
    /// <param name="algorithm">digest algorithm OID, for example for SHA256 use: "2.16.840.1.101.3.4.2.1"</param>
    /// <returns>Hash of csr</returns>
    public byte[] BuildHash(byte[] csr, string algorithm)
    {
        var originalCsr = new Pkcs10CertificationRequestDelaySigned(csr);

        // parse CSR to Org.BouncyCastle.Pkcs.Pkcs10CertificationRequestDelaySigned
        //  requires CSR to have:
        // 1. Subject
        //      a. X509Name
        //      b. subject public key
        //      c. attributes
        //          c1. password - should be empty
        //          c2. extensions - should contain ... doesn't matter - don't touch
        // 2. SignatureAlgorithmId - keep as it is defined by user request
        // 3. SignBits of user for the given CSR

        // hash = function(csrWithPassword without signature/signature algorithm)
        // for some hash algorithms Hash may depend on a random number, 
        // thus giving different Hash every time it is calculated even for the same Data, PrivateKey

        byte[] dataToSign = originalCsr.GetDataToSign();

        //byte[] digest = DigestUtilities.CalculateDigest(CmsSignedGenerator.DigestSha256, dataToSign);
        byte[] digest = DigestUtilities.CalculateDigest(algorithm, dataToSign);

        return digest;
    }

    /// <summary>
    /// Creates new csr from given CSR + signature
    /// </summary>
    /// <param name="csr">CSR to be used for appending signature</param>
    /// <param name="signature">signature to be appended to CSR</param>
    /// <returns>new CSR with signature appended inside</returns>
    public byte[] AppendSignature(byte[] csr, byte[] signature)
    {
        if (csr == null) throw new ArgumentNullException(nameof(csr));

        var originalCsr = new Pkcs10CertificationRequestDelaySigned(csr);

        originalCsr.SignRequest(signature);

        byte[] csrBytes = originalCsr.GetDerEncoded();

        return csrBytes;
    }
}
}

这是对哈希进行签名并调用 DelayCsrProvider 的测试代码。 使用ECDSA签名时测试失败,参见测试方法:ValidCsrWithoutPassword_Ecdsa_SignatureIsAppended

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using NEnrollment.Services.DelaySigning;
    using Org.BouncyCastle.Asn1;
    using Org.BouncyCastle.Asn1.Sec;
    using Org.BouncyCastle.Asn1.X509;
    using Org.BouncyCastle.Cms;
    using Org.BouncyCastle.Crypto;
    using Org.BouncyCastle.Crypto.Operators;
    using Org.BouncyCastle.Crypto.Parameters;
    using Org.BouncyCastle.Math;
    using Org.BouncyCastle.Pkcs;
    using Org.BouncyCastle.Security;

    namespace NEnrollment.Tests
    {
    [TestClass]
    public class DelayCsrProviderTest
    {
        private readonly bool _enableWritingToFile = false;

        DelayCsrProvider CreateSut()
        {
            return new DelayCsrProvider();
        }

        [TestMethod]
        public void ValidCsrWithoutPassword_Rsa_SignatureIsAppended()
        {
            var sut = CreateSut();

            const string signAlgorithm = "RSA";
            var keys = new Keys(signAlgorithm);

            // Create CSR
            var signatureAlgorithm = "SHA256withRSA";
            byte[] octetData = CreateCsr(keys.SignKeyPair, signatureAlgorithm);
            ByteArrayToFile(@"Rsa\csrWithoutPass.csr", octetData);

            // Append password to CSR
            byte[] csrWithPass = sut.AppendPassword(octetData, "some-text-1");
            ByteArrayToFile(@"Rsa\csrWithPass.csr", csrWithPass);

            // Calculate HASH
            var hashAlgorithm = CmsSignedGenerator.DigestSha256;
            byte[] hash = sut.BuildHash(csrWithPass, hashAlgorithm);

            // Sign using HASH
            byte[] signature = Sign(hash, signAlgorithm, hashAlgorithm, keys.SignKeyPair.Private);

            // Add signature to CSR
            byte[] csrSigned = sut.AppendSignature(csrWithPass, signature);
            ByteArrayToFile(@"Rsa\csrSigned.csr", csrSigned);

            // Just verify the signature matches CSR's public key + data,
            // public key should match the private key
            Verify(csrSigned);
            Verify2(csrSigned);
        }

        [TestMethod]
        public void ValidCsrWithoutPassword_Ecdsa_SignatureIsAppended()
        {
            var sut = CreateSut();

            const string signAlgorithm = "ECDSA";
            var keys = new Keys(signAlgorithm);

            // Create CSR
            var signatureAlgorithm = "SHA256withECDSA";
            byte[] octetData = CreateCsr(keys.SignKeyPair, signatureAlgorithm);
            ByteArrayToFile(@"Ecdsa\csrWithoutPass.csr", octetData);
            Verify(octetData);

            // Append password to CSR
            byte[] csrWithPass = sut.AppendPassword(octetData, "some-text-1");
            ByteArrayToFile(@"Ecdsa\csrWithPass.csr", csrWithPass);

            // Calculate HASH
            var hashAlgorithm = CmsSignedGenerator.DigestSha256;
            byte[] hash = sut.BuildHash(csrWithPass, hashAlgorithm);

            // Sign using HASH
            byte[] signature = Sign(hash, signAlgorithm, hashAlgorithm, keys.SignKeyPair.Private);

            // Add signature to CSR
            byte[] csrSigned = sut.AppendSignature(csrWithPass, signature);
            ByteArrayToFile(@"Ecdsa\csrSigned.csr", csrSigned);

            // Just verify the signature matches CSR's public key + data,
            // public key should match the private key

            //Verify2(csrSigned);
            Verify(csrSigned);
        }

        private byte[] CreateCsr(AsymmetricCipherKeyPair signingKeyPair, string signatureAlgorithm)
        {
            var key = signingKeyPair;

            Dictionary<DerObjectIdentifier, string> values = CreateSubjectValues("my common name");

            var subject = new X509Name(values.Keys.Reverse().ToList(), values);

            DerSet attributes = null;

            var signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, key.Private);

            var pkcs10Csr = new Pkcs10CertificationRequest(
                signatureFactory,
                subject,
                key.Public,
                attributes,
                key.Private);

            byte[] derEncoded = pkcs10Csr.GetDerEncoded();

            //string stringEncoded = Convert.ToBase64String(derEncoded);
            //return stringEncoded;
            return derEncoded;
        }

        private Dictionary<DerObjectIdentifier, string> CreateSubjectValues(string commonName)
        {
            var values = new Dictionary<DerObjectIdentifier, string>
            {
                {X509Name.CN, commonName}, //domain name inside the quotes
                /*
                {X509Name.CN, csrSubject.CommonName}, //domain name inside the quotes
                {X509Name.OU, csrSubject.OrganizationalUnit},
                {X509Name.O, csrSubject.Organization}, //Organisation's Legal name inside the quotes
                {X509Name.L, csrSubject.City},
                {X509Name.ST, csrSubject.Country},
                {X509Name.C, csrSubject.State},
                */
            };

            // remove empty values
            var emptyKeys = values.Keys.Where(key => string.IsNullOrEmpty(values[key])).ToList();

            emptyKeys.ForEach(key => values.Remove(key));

            return values;
        }

        /// <summary>
        /// Calculate signature using signer algorithm for the defined has algorithm
        /// </summary>
        /// <param name="hash"></param>
        /// <param name="signerAlgorithm"></param>
        /// <param name="hashAlgorithmOid">
        /// hash Algorithm Oid, for example:
        /// "2.16.840.1.101.3.4.2.1"
        /// </param>
        /// <param name="privateSigningKey">private key for signing</param>
        /// <returns></returns>
        public static byte[] Sign(byte[] hash, string signerAlgorithm, string hashAlgorithmOid, AsymmetricKeyParameter privateSigningKey)
        {

            var digestAlgorithm = new AlgorithmIdentifier(new DerObjectIdentifier(hashAlgorithmOid), DerNull.Instance);
            var dInfo = new DigestInfo(digestAlgorithm, hash);
            byte[] digest = dInfo.GetDerEncoded();

            ISigner signer = SignerUtilities.GetSigner(signerAlgorithm);
            signer.Init(true, privateSigningKey);
            signer.BlockUpdate(digest, 0, digest.Length);
            byte[] signature = signer.GenerateSignature();
            return signature;

/*  // Another way of signing
            if (signerAlgorithm == "RSA")
            {
                // convert private key from BouncyCastle to System.Security :
                RSA key = DotNetUtilities.ToRSA((RsaPrivateCrtKeyParameters)privateSigningKey);
                using (var cryptoServiceProvider = new RSACryptoServiceProvider())
                {
                    cryptoServiceProvider.ImportParameters(key.ExportParameters(true));

                    //
                    // Hash and sign the data. Pass a new instance of SHA1CryptoServiceProvider
                    // to specify the use of SHA1 for hashing.
                    byte[] signedData = cryptoServiceProvider.SignHash(hash, hashAlgorithmOid);
                    return signedData;
                }
            }

            if (signerAlgorithm == "ECDSA")
            {
                // convert private key from BouncyCastle to System.Security :
                var bcKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateSigningKey);
                var pkcs8Blob = bcKeyInfo.GetDerEncoded();
                var key = CngKey.Import(pkcs8Blob, CngKeyBlobFormat.Pkcs8PrivateBlob);

                using (ECDsaCng cryptoServiceProvider = new ECDsaCng(key))
                {
                    cryptoServiceProvider.HashAlgorithm = CngAlgorithm.Sha256; //, hashAlgorithmOid);

                    byte[] signature = cryptoServiceProvider.SignHash(hash);
                    return signature;
                }
            }

            throw new NotImplementedException(signerAlgorithm);
*/
        }

        /// <summary>
        /// Verify signature using self verification of Pkcs10CertificationRequest
        /// </summary>
        /// <param name="csrSigned"></param>
        private void Verify(byte[] csrSigned)
        {
            Assert.IsNotNull(csrSigned);

            var csr = new Pkcs10CertificationRequest(csrSigned);

            bool isValid = csr.Verify();

            Assert.IsTrue(isValid, "Verification failed");
        }

        /// <summary>
        /// Verify signature using specified signer
        /// </summary>
        /// <param name="csrSigned"></param>
        private void Verify2(byte[] csrSigned)
        {
            var csr = new Pkcs10CertificationRequestDelaySigned(csrSigned);
            var sigBytes = csr.Signature.GetBytes();//.GetDerEncoded();
            var data = csr.GetDataToSign();
            AsymmetricKeyParameter publicSigningKey = csr.GetPublicKey();
            var signerAlgorithm = csr.SignatureAlgorithm.Algorithm.Id;

            var s = SignerUtilities.GetSigner(signerAlgorithm);
            s.Init(false, publicSigningKey);
            s.BlockUpdate(data, 0, data.Length);
            bool isValidSignature = s.VerifySignature(sigBytes);

            Assert.IsTrue(isValidSignature, "ECDSA verification failed");
        }

        private void ByteArrayToFile(string fileName, byte[] byteArray)
        {
            if (!_enableWritingToFile) return;

            try
            {
                fileName = @"C:\temp\delayCsrTest\" + fileName;
                new FileInfo(fileName).Directory?.Create();
                File.WriteAllBytes(fileName, byteArray);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception caught in process: {0}", ex);
                throw;
            }
        }
    }

    /// <summary>
    /// Helper that stores private and public key-pair as required for signing and verification of signature
    /// </summary>
    class Keys
    {
        private static readonly SecureRandom Rand;

        private readonly string _keyAlgorithm;

        private readonly KeyGenerationParameters _keyGenerationParameters;

        private readonly IAsymmetricCipherKeyPairGenerator _keyPairGenerator;

        private AsymmetricCipherKeyPair _signKeyPair;
        public AsymmetricCipherKeyPair SignKeyPair => _signKeyPair ?? (_signKeyPair = MakeKeyPair());

        static Keys()
        {
            try
            {
                Rand = new SecureRandom();
            }
            catch (Exception ex)
            {
                throw new Exception(ex.ToString());
            }
        }

        public Keys(string keyAlgorithm)
        {
            _keyAlgorithm = keyAlgorithm;
            _keyGenerationParameters = CreateKeyGenerationParameters();
            _keyPairGenerator = CreateKeyPairGenerator();
        }

        private KeyGenerationParameters CreateKeyGenerationParameters()
        {
            SecureRandom random = Rand;
            //SecureRandom random = SecureRandom.GetInstance("SHA256PRNG");

            if (_keyAlgorithm == "RSA")
            {
                return new RsaKeyGenerationParameters(BigInteger.ValueOf(65537), random, 2048, 25);
            }

            if (_keyAlgorithm == "ECDSA")
            {
                return new ECKeyGenerationParameters(SecObjectIdentifiers.SecP256r1, random);
            }

            throw new NotSupportedException(_keyAlgorithm);
        }

        private IAsymmetricCipherKeyPairGenerator CreateKeyPairGenerator()
        {
            var keyPairGenerator = GeneratorUtilities.GetKeyPairGenerator(_keyAlgorithm);
            keyPairGenerator.Init(_keyGenerationParameters);

            return keyPairGenerator;
        }

        public AsymmetricCipherKeyPair MakeKeyPair()
        {
            return _keyPairGenerator.GenerateKeyPair();
        }
    }
    }

最佳答案

我发现我计算签名的方式有问题。由于某种原因,它不适用于 ECDSA,因此这里是适用于 ECDSA 和 RSA(使用 SHA-256 哈希)的解决方案。 简而言之,在计算签名时使用“NONEwithECDSA”或“NONEwithRSA”,而且 RSA 需要添加 DigestInfo 而不是 ECDSA 中的 bair 签名(我仍然想知道为什么?)。

 [TestMethod]
 public void TestDelaySigning()
 {
            const string hashAlgorithm = "SHA256";
            const string signAlgorithm = "ECDSA"; // or "RSA" 
            bool isRsa =false; // or true for RSA

           // Create CSR
            var signatureAlgorithm = hashAlgorithm + "with" + signAlgorithm; // "SHA256withECDSA" or "SHA256withRSA"
            byte[] octetData = CreateCsr(keys.SignKeyPair, signatureAlgorithm);
            byte[] csrWithPass = AppendPassword(octetData, "some-password");

            byte[] hash = BuildHash(csrWithPass, hashAlgorithm);

            // Sign the hash
            string singingAlgorithm2 = "NONEwith" + signAlgorithm;
            byte[] signature = Sign(hash, singingAlgorithm2, hashAlgorithm, keys.SignKeyPair.Private, isRsa );

            byte[] csrSigned = AppendSignature(csrWithPass, signature);

            Verify(csrSigned);
 }

        public byte[] BuildHash(byte[] csr, string algorithm)
        {
            var originalCsr = new Pkcs10CertificationRequestDelaySigned(csr);
            byte[] dataToSign = originalCsr.GetDataToSign();
            byte[] digest = DigestUtilities.CalculateDigest(algorithm, dataToSign);
            return digest;
        }

        public static byte[] Sign(byte[] hash, string signerAlgorithm, string hashAlgorithm, 
                                                     AsymmetricKeyParameter privateSigningKey, bool isRsa)
        {
            if (isRsa)
        {
            var hashAlgorithmOid = DigestUtilities.GetObjectIdentifier(hashAlgorithm).Id;

            var digestAlgorithm = new AlgorithmIdentifier(new DerObjectIdentifier(hashAlgorithmOid), DerNull.Instance);
            var dInfo = new DigestInfo(digestAlgorithm, hash);
            byte[] digest = dInfo.GetDerEncoded();
            hash = digest;
        }

        ISigner signer = SignerUtilities.GetSigner(signerAlgorithm);
        signer.Init(true, privateSigningKey);
        signer.BlockUpdate(hash, 0, hash.Length);
        byte[] signature = signer.GenerateSignature();
        return signature;
      }
 }

关于c# - 如何使用带有 ECDSA 签名的 Bouncy CaSTLe 延迟签署证书请求?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46182659/

相关文章:

c# - Bouncy CaSTLe 的 X509V3CertificateGenerator.SetSignatureAlgorithm 标记为已过时。我该怎么办?

certificate - 使用 certreq 工具创建证书,其中我使用 openssl 生成了 csr

c# - "is { }"是什么意思?

c# - 如何在 C# 中指定 RGB 值的范围

java - 使用公钥/私钥恢复签名消息

encryption - 安全的随机种子和 derby 数据库创建

发送到服务器后删除 C# HttpClient 授权 header

c# - 检索类型的叶接口(interface)

ssl - 当域作为域 B 的 CNAME 时,SSL 证书签名请求的 FQDN 是什么

certificate - 如何将 .csr 转换为 .cer(或任何可在 Windows 上使用的文件)