File: ResolveKeySource.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
 
using Microsoft.Build.Framework;
#if FEATURE_PFX_SIGNING
using System.Globalization;
using System.Security.Cryptography;
using Microsoft.Runtime.Hosting;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
#endif
 
#nullable disable
 
namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// Determine the strong name key source
    /// </summary>
    public class ResolveKeySource : TaskExtension
    {
        private const string pfxFileExtension = ".pfx";
#if !RUNTIME_TYPE_NETCORE
        private const string pfxFileContainerPrefix = "VS_KEY_";
#endif
 
        #region Properties
 
        public string KeyFile { get; set; }
 
        public string CertificateThumbprint { get; set; }
 
        public string CertificateFile { get; set; }
 
        public bool SuppressAutoClosePasswordPrompt { get; set; } = false;
 
        public bool ShowImportDialogDespitePreviousFailures { get; set; } = false;
 
        public int AutoClosePasswordPromptTimeout { get; set; } = 20;
 
        public int AutoClosePasswordPromptShow { get; set; } = 15;
 
        [Output]
        public string ResolvedThumbprint { get; set; } = String.Empty;
 
        [Output]
        public string ResolvedKeyContainer { get; set; } = String.Empty;
 
        [Output]
        public string ResolvedKeyFile { get; set; } = String.Empty;
 
        #endregion
 
        #region ITask Members
 
        public override bool Execute()
        {
            return ResolveAssemblyKey() && ResolveManifestKey();
        }
 
        // We we use hash the contens of .pfx file so we can establish relationship file <-> container name, whithout
        // need to prompt for password. Note this is not used for any security reasons. With the departure from standard MD5 algoritm
        // we need as simple hash function for replacement. The data blobs we use (.pfx files)  are
        // encrypted meaning they have high entropy, so in all practical pupose even a simpliest
        // hash would give good enough results. This code needs to be kept in sync with the code  in compsvcpkgs
        // to prevent double prompt for newly created keys. The magic numbers here are just random primes
        // in the range 10m/20m.
        private static UInt64 HashFromBlob(byte[] data)
        {
            UInt32 dw1 = 17339221;
            UInt32 dw2 = 19619429;
            UInt32 pos = 10803503;
 
            foreach (byte b in data)
            {
                UInt32 value = b ^ pos;
                pos *= 10803503;
                dw1 += ((value ^ dw2) * 15816943) + 17368321;
                dw2 ^= ((value + dw1) * 14984549) ^ 11746499;
            }
            UInt64 result = dw1;
            result <<= 32;
            result |= dw2;
            return result;
        }
 
        private bool ResolveAssemblyKey()
        {
            bool pfxSuccess = true;
            if (!string.IsNullOrEmpty(KeyFile))
            {
                string keyFileExtension = String.Empty;
                try
                {
                    keyFileExtension = Path.GetExtension(KeyFile);
                }
                catch (ArgumentException ex)
                {
                    Log.LogErrorWithCodeFromResources("ResolveKeySource.InvalidKeyName", KeyFile, ex.Message);
                    pfxSuccess = false;
                }
                if (pfxSuccess)
                {
                    if (!String.Equals(keyFileExtension, pfxFileExtension, StringComparison.OrdinalIgnoreCase))
                    {
                        ResolvedKeyFile = KeyFile;
                    }
                    else
                    {
#if FEATURE_PFX_SIGNING
                        pfxSuccess = false;
                        // it is .pfx file. It is being imported into key container with name = "VS_KEY_<MD5 check sum of the encrypted file>"
                        FileStream fs = null;
                        try
                        {
                            string currentUserName = Environment.UserDomainName + "\\" + Environment.UserName;
                            // we use the curent user name to randomize the associated container name, i.e different user on the same machine will export to different keys
                            // this is because SNAPI by default will create keys in "per-machine" crypto store (visible for all the user) but will set the permission such only
                            // creator will be able to use it. This will make imposible for other user both to sign or export the key again (since they also can not delete that key).
                            // Now different users will use different container name. We use ToLower(invariant) because this is what the native equivalent of this function (Create new key, or VC++ import-er).
                            // use as well and we want to keep the hash (and key container name the same) otherwise user could be prompt for a password twice.
                            byte[] userNameBytes = System.Text.Encoding.Unicode.GetBytes(currentUserName.ToLower(CultureInfo.InvariantCulture));
                            fs = File.OpenRead(KeyFile);
                            int fileLength = (int)fs.Length;
                            var keyBytes = new byte[fileLength];
                            fs.Read(keyBytes, 0, fileLength);
 
                            UInt64 hash = HashFromBlob(keyBytes);
                            hash ^= HashFromBlob(userNameBytes); // modify it with the username hash, so each user would get different hash for the same key
 
                            string hashedContainerName = pfxFileContainerPrefix + hash.ToString("X016", CultureInfo.InvariantCulture);
 
                            if (StrongNameHelpers.StrongNameGetPublicKey(hashedContainerName, IntPtr.Zero, 0, out IntPtr publicKeyBlob, out _) && publicKeyBlob != IntPtr.Zero)
                            {
                                StrongNameHelpers.StrongNameFreeBuffer(publicKeyBlob);
                                pfxSuccess = true;
                            }
                            else
                            {
                                Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyFileForSignAssemblyNotImported", KeyFile, hashedContainerName);
                                Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyImportError", KeyFile);
                            }
                            if (pfxSuccess)
                            {
                                ResolvedKeyContainer = hashedContainerName;
                            }
                        }
                        catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
                        {
                            Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyMD5SumError", KeyFile, e.Message);
                        }
                        finally
                        {
                            fs?.Close();
                        }
#else
                        Log.LogErrorWithCodeFromResources("ResolveKeySource.PfxUnsupported");
                        pfxSuccess = false;
#endif
                    }
                }
            }
 
            return pfxSuccess;
        }
 
        private bool ResolveManifestKey()
        {
            bool certSuccess = false;
            bool certInStore = false;
            if (!string.IsNullOrEmpty(CertificateThumbprint))
            {
                // look for cert in the cert store
                var personalStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
                try
                {
                    personalStore.Open(OpenFlags.ReadWrite);
                    X509Certificate2Collection foundCerts = personalStore.Certificates.Find(X509FindType.FindByThumbprint, CertificateThumbprint, false);
                    if (foundCerts.Count == 1)
                    {
                        certInStore = true;
                        ResolvedThumbprint = CertificateThumbprint;
                        certSuccess = true;
                    }
                }
                finally
                {
#if FEATURE_PFX_SIGNING
                    personalStore.Close();
#else
                    personalStore.Dispose();
#endif
                }
                if (!certSuccess)
                {
                    Log.LogWarningWithCodeFromResources("ResolveKeySource.ResolvedThumbprintEmpty");
                }
            }
 
            if (!string.IsNullOrEmpty(CertificateFile) && !certInStore)
            {
#if FEATURE_PFX_SIGNING
                // if the cert isn't on disk, we can't import it
                if (!FileSystems.Default.FileExists(CertificateFile))
                {
                    Log.LogErrorWithCodeFromResources("ResolveKeySource.CertificateNotInStore");
                }
                else
                {
                    // add the cert to the store optionally prompting for the password
                    if (X509Certificate2.GetCertContentType(CertificateFile) == X509ContentType.Pfx)
                    {
                        bool imported = false;
                        // first try it with no password
                        using var cert = new X509Certificate2();
                        var personalStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
                        try
                        {
                            personalStore.Open(OpenFlags.ReadWrite);
                            cert.Import(CertificateFile, (string)null, X509KeyStorageFlags.PersistKeySet);
                            personalStore.Add(cert);
                            ResolvedThumbprint = cert.Thumbprint;
                            imported = true;
                            certSuccess = true;
                        }
                        catch (CryptographicException)
                        {
                            // cert has a password, move on and prompt for it
                        }
                        finally
                        {
                            personalStore.Close();
                        }
                        if (!imported && ShowImportDialogDespitePreviousFailures)
                        {
                            Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyFileForManifestNotImported", KeyFile);
                        }
                        if (!certSuccess)
                        {
                            Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyImportError", CertificateFile);
                        }
                    }
                    else
                    {
                        var personalStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
                        try
                        {
                            var cert = new X509Certificate2(CertificateFile);
                            personalStore.Open(OpenFlags.ReadWrite);
                            personalStore.Add(cert);
                            ResolvedThumbprint = cert.Thumbprint;
                            certSuccess = true;
                        }
                        catch (CryptographicException)
                        {
                            Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyImportError", CertificateFile);
                        }
                        finally
                        {
                            personalStore.Close();
                        }
                    }
                }
#else
                Log.LogErrorWithCodeFromResources("ResolveKeySource.PfxUnsupported");
#endif
            }
            else if (!certInStore && !string.IsNullOrEmpty(CertificateFile) && !string.IsNullOrEmpty(CertificateThumbprint))
            {
                // no file and not in store, error out
                Log.LogErrorWithCodeFromResources("ResolveKeySource.CertificateNotInStore");
            }
            else
            {
                certSuccess = true;
            }
 
            return certSuccess;
        }
 
        #endregion
    }
}