2

Introduction

I have a digital code signing logic in my application. I use custom signing tool which signs dll files and create .sig text files with signed hash values. This tool runs on .NET 6, gets assembly list, path to pfx certificate file and a passphrase for it.

Signing code part

        var cryptoProvider = new RSACryptoServiceProvider();
        var rsaParameters = GetRsaPrivateKey(certificate.FullName, passphrase);
        cryptoProvider.ImportParameters(rsaParameters);

        foreach (var assembly in assemblies)
        {
          
            var assemblyBytes = File.ReadAllBytes(assembly.FullName);
            var signedBytes = cryptoProvider.SignData(
                    assemblyBytes,
                    HashAlgorithmName.SHA256);
            var hash = Convert.ToBase64String(signedBytes);
            var sigFile = new FileInfo(Path.ChangeExtension(assembly.FullName, "sig"));
            File.WriteAllText(sigFile.FullName, hash);
        }

        private static RSAParameters GetRsaPrivateKey(string certificatePath, string certificatePassphrase)
    {
        var certificate = new X509Certificate2(
            certificatePath,
            certificatePassphrase,
            X509KeyStorageFlags.Exportable);
        var privateKey = certificate.GetRSAPrivateKey();
        if (privateKey != null)
        {
            using var exportRewriter = RSA.Create();
            exportRewriter.ImportEncryptedPkcs8PrivateKey(
                certificatePassphrase,
                privateKey.ExportEncryptedPkcs8PrivateKey(
                    certificatePassphrase,
                    new PbeParameters(
                        PbeEncryptionAlgorithm.Aes128Cbc,
                        HashAlgorithmName.SHA256,
                        1)), out _);

            return exportRewriter.ExportParameters(true);
        }

After creating .sig file my console tool verifies digital signature importing only public key from pfx certificate

Verifying code signing part

 var cryptoProvider = new RSACryptoServiceProvider();
        var rsaParameters = GetRsaPublicKey(certificate.FullName, passphrase);
        cryptoProvider.ImportParameters(rsaParameters);

        foreach (var assembly in assemblies)
        {
            try
            {
                var sigFile = new FileInfo(Path.ChangeExtension(assembly.FullName, "sig"));
                var assemblyBytes = File.ReadAllBytes(assembly.FullName);
                var assemblySignedBytes = Convert.FromBase64String(File.ReadAllText(sigFile.FullName));
                var verificationResult = cryptoProvider.VerifyData(
                    assemblyBytes,
                    HashAlgorithmName.SHA256,
                    assemblySignedBytes);

                if (verificationResult)
                {
                    Console.WriteLine($"Verification for assembly: {assembly} is successful.{Environment.NewLine}");
                }
                else
                {
                    throw new SignToolException($"Verification for assembly: {assembly} " +
                                                   $"with sig file: {sigFile} is failed.");
                }
            }
            catch
            {
                Console.WriteLine($"Assembly: {assembly} verification failed.{Environment.NewLine}");
                throw;
            }
        }
 private static RSAParameters GetRsaPublicKey(string certificatePath, string certificatePassPhrase)
    {
        var certificate = new X509Certificate2(
            certificatePath,
            certificatePassPhrase);
        var publicKey = certificate.GetRSAPublicKey();
        if (publicKey != null)
        {
            return publicKey.ExportParameters(false);
        }

        throw new SignToolException(
            "Can't get public key from certificate." +
            " Please check your certificate file and a passphrase");
    }

Everything works great on .NET 6 I don't have any problems.

Also I have .NET 4.8 application which must verify signed dll's by reading signed hash value from corresponding .sig files and verify digital signature.

.NET 4.8 application digital signature verification part

var publicKeyBytes = GetPublicKeyFromResource(PublicKeyResourceKey);
CryptoServiceProvider = new RSACryptoServiceProvider();
CryptoServiceProvider.ImportCspBlob(publicKeyBytes);

public static bool VerifyAssemblyCodeSign(Assembly assembly)
        { 
            var assemblyPath = new UriBuilder(assembly.CodeBase).Path;
            var assemblySigLocation = Path.ChangeExtension(assemblyPath, SignatureFileExtension);
            
            if (File.Exists(assemblySigLocation))
            {
                var assemblyBytes = File.ReadAllBytes(assemblyPath);
                var assemblySignedBytes = Convert.FromBase64String(File.ReadAllText(assemblySigLocation));
                return CryptoServiceProvider.VerifyData(
                    assemblyBytes,
                    assemblySignedBytes,
                    HashAlgorithmName.SHA256, 
                    RSASignaturePadding.Pkcs1);
            }
            
            return false;
        }

private static byte[] GetPublicKeyFromResource(string resourceKey)
        {
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceKey))
            using (var memoryStream = new MemoryStream())
            {
                // ReSharper disable once PossibleNullReferenceException
                stream.CopyTo(memoryStream);
                return memoryStream.ToArray();
            }
        }

This code reads exported from pfx certificate public key in binary format and imports it to CryptoServiceProvider. For exporting public key from pfx certificate I use this command

openssl rsa -in my.key -passin pass:mySecret -RSAPublicKey_out -outform "MS PUBLICKEYBLOB" -out my_rsa.pem

Problem

Code represented above works great on my local machine (Windows 11) and on my build server (Windows Server 2019 with latest patches) but it doesn't work on other Windows Server machines and on some other Windows machines. VerifyAssemblyCodeSign method returns false without any other output. I've tried different ways to import/export public key, tried RSACng object, but it didn't help. I have no clue why it works on random Windows machines. I'll appreciate any help/suggestion.

  • The `GetRsaPrivateKey` code generates a completely new RSA key pair if there is no private key associated with the certificate (which makes no cryptographic nor logical sense and definitely doesn't comply to the principle of least surprise). Do you know which branch is taken on which machine? – Maarten Bodewes Aug 22 '22 at 18:53
  • @MaartenBodewes I use this method to export certificate private key to RSA object and this part work well. It uses the same private key each time (from my pfx certificate). The problem with .net 4.8 application not .net 6. – Dmitry Grebennikov Aug 22 '22 at 18:57
  • 1
    The problem is that we cannot really debug. I would myself possibly blame a base 64 encoding / decoding bug when transferring the files from one system to another. If the `+` or `/` files are missing or if the transfer is not complete then the signature won't verify. This may result in random errors, which makes sense given your error description. Calculate a hash over the assemblies beforehand using one of the many `sha256sum` variants. – Maarten Bodewes Aug 22 '22 at 19:44
  • 1
    Yes, we can't debug. About base64. I checked hash and it's identical for both (working and not working) machines. I also changed base64 to utf8 encoding reading, but it didn't help. I think the problem with win32 implementation of Verify method. Maybe analyzing with WinDbg will help. – Dmitry Grebennikov Aug 22 '22 at 19:49
  • Quick update. I removed base64 part and store bytes in file directly. But nothing changed. The problem not in string to byte converting. – Dmitry Grebennikov Aug 23 '22 at 12:22
  • Can you maybe compare the keys using some method or another? For RSA the moduli of the public and private key pair are identical and unique, so often a SHA-1 over the (big endian, unsigned, static sized) modulus is calculated as a key identifier. – Maarten Bodewes Aug 23 '22 at 15:50

1 Answers1

0

I found a problem. It was with UriBuilder. For paths with spaces it replaces them to %20 symbols and File.Exists didn't find a file with signed hash. For fixing this you need to wrap you need to use Uri.UnescapeDataString. So, the working code:

var uri = new UriBuilder(assembly.CodeBase).Path;
var assemblyPath = Uri.UnescapeDataString(uri);

Now everything works on any Windows version!