I am trying to implement delay signing of a certificate request (CSR) with ECDSA signature algorithm using Bouncy Castle in C#. So far I've managed to implement this with RSA but not ECDSA. I use Pkcs10CertificationRequestDelaySigned class from Bouncy Castle.
A snippet of my test code that fails when verifying signature (full code is available below):
[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);
}
The scenario is: There are two entities (programs running on separate machines). One has both private and public key - call it Signer and the other has additional information (like password) for extending certificate request with that information but NO access to private key - call it DelayCsrProvider.
The sequence is:
Signer creates CSR without password and signs the data, sends it to DelayCsrProvider in PKCS#10 format DER encoded.
DelayCsrProvider creates new CSR with all the information from received CSR plus adds additional attribute that contains password. Now we have to sign this new CSR but we don't have private key. Instead we calculate hash (SHA-256) of data and send the digest to Signer.
Signer receives the hash and signs the hash, sends the signature back to DelayCsrProvider.
DelayCsrProvider inserts received signature into CSR thus creating a full CSR that has valid signature.
I've created DelayCsrProvider class and unit test that is making all the steps described above. One unit-test for RSA works just fine, the other unit test for ECDSA fails when verifying the signature.
What else can be done here to resolve ECDSA hash signing ?
See main part of code below or download the whole example from GIT: https://github.com/DmitriNymi/Certificate-Enrollment.git
Note:
.Net Framework 4.6.2 Bouncy Castle nuget BouncyCastle.Crypto.dll FileVersion=1.8.15362.1
This is full code example of test that fails in 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;
}
}
}
This is the test code that signs the hash and calls DelayCsrProvider. The test fails when signing with ECDSA, see test method: 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();
}
}
}
I found the problem with the way I was calculating the signature. For some reason it doesn't work for ECDSA, so here is the solution that works both for ECDSA and RSA (with SHA-256 hash). In short when calculating signature use "NONEwithECDSA" or "NONEwithRSA" and also RSA requires adding DigestInfo instead of bair signature as in ECDSA (I am still wondering why ?) .