File: Verification\AuthentiCode.cs
Web Access
Project: src\src\SignCheck\Microsoft.SignCheck\Microsoft.DotNet.SignCheckLibrary.csproj (Microsoft.DotNet.SignCheckLibrary)
// 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.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Pkcs;
using Microsoft.SignCheck.Interop;
#if NET
using System.Reflection.PortableExecutable;
#endif
 
namespace Microsoft.SignCheck.Verification
{
    public static class AuthentiCode
    { 
        public static bool IsSigned(string path, SignatureVerificationResult svr) =>
            string.IsNullOrEmpty(path) ? false : IsSignedInternal(path, svr);
 
        public static IEnumerable<Timestamp> GetTimestamps(string path) =>
            string.IsNullOrEmpty(path) ? Enumerable.Empty<Timestamp>() : GetTimestampsInternal(path);
 
#if NETFRAMEWORK
        private static bool IsSignedInternal(string path, SignatureVerificationResult svr)
        {
            WinTrustFileInfo fileInfo = new WinTrustFileInfo()
            {
                cbStruct = (uint)Marshal.SizeOf(typeof(WinTrustFileInfo)),
                pcwszFilePath = Path.GetFullPath(path),
                hFile = IntPtr.Zero,
                pgKnownSubject = IntPtr.Zero
            };
 
            WinTrustData data = new WinTrustData()
            {
                cbStruct = (uint)Marshal.SizeOf(typeof(WinTrustData)),
                dwProvFlags = 0,
                dwStateAction = Convert.ToUInt32(StateAction.WTD_STATEACTION_IGNORE),
                dwUIChoice = Convert.ToUInt32(UIChoice.WTD_UI_NONE),
                dwUIContext = 0,
                dwUnionChoice = Convert.ToUInt32(UnionChoice.WTD_CHOICE_FILE),
                fdwRevocationChecks = Convert.ToUInt32(RevocationChecks.WTD_REVOKE_NONE),
                hWVTStateData = IntPtr.Zero,
                pFile = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WinTrustFileInfo))),
                pPolicyCallbackData = IntPtr.Zero,
                pSIPClientData = IntPtr.Zero,
                pwszURLReference = IntPtr.Zero
            };
 
            // Potential memory leak. Need to investigate
            Marshal.StructureToPtr(fileInfo, data.pFile, false);
 
            IntPtr pGuid = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid)));
            IntPtr pData = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WinTrustData)));
            Marshal.StructureToPtr(data, pData, true);
            Marshal.StructureToPtr(WinTrust.WINTRUST_ACTION_GENERIC_VERIFY_V2, pGuid, true);
 
            uint hrresult = WinTrust.WinVerifyTrust(IntPtr.Zero, pGuid, pData);
 
            Marshal.FreeHGlobal(pGuid);
            Marshal.FreeHGlobal(pData);
 
            // Log non-zero HRESULTs
            if (hrresult != 0)
            {
                string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
                svr.AddDetail(DetailKeys.Error, String.Format(SignCheckResources.ErrorHResult, hrresult, errorMessage));
            }
 
            return hrresult == 0;
        }
 
        private static IEnumerable<Timestamp> GetTimestampsInternal(string path)
        {
            int msgAndCertEncodingType;
            int msgContentType;
            int formatType;
 
            // NULL indicates that information is unneeded
            IntPtr certStore = IntPtr.Zero;
            IntPtr msg = IntPtr.Zero;
            IntPtr context = IntPtr.Zero;
 
            if (!WinCrypt.CryptQueryObject(
                WinCrypt.CERT_QUERY_OBJECT_FILE,
                Marshal.StringToHGlobalUni(path),
                WinCrypt.CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED | WinCrypt.CERT_QUERY_CONTENT_FLAG_PKCS7_UNSIGNED | WinCrypt.CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED,
                WinCrypt.CERT_QUERY_FORMAT_FLAG_ALL,
                0,
                out msgAndCertEncodingType,
                out msgContentType,
                out formatType,
                ref certStore,
                ref msg,
                ref context))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }
 
            int cbData = 0;
 
            // Passing in NULL to pvData retrieves the size of the encoded message
            if (!WinCrypt.CryptMsgGetParam(msg, WinCrypt.CMSG_ENCODED_MESSAGE, 0, IntPtr.Zero, ref cbData))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }
 
            byte[] vData = new byte[cbData];
            if (!WinCrypt.CryptMsgGetParam(msg, WinCrypt.CMSG_ENCODED_MESSAGE, 0, vData, ref cbData))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }
 
            var signedCms = new SignedCms();
            signedCms.Decode(vData);
 
            return ExtractTimestamps(signedCms);
        }
#else
        private static bool IsSignedInternal(string path, SignatureVerificationResult svr)
        {
            try
            {
                SignedCms signedCms = ReadSecurityInfo(path);
                if (signedCms == null)
                {
                    return false;
                }
 
                if (signedCms.ContentInfo.ContentType.Value != WinCrypt.SPC_INDIRECT_DATA_OBJID)
                {
                    throw new CryptographicException($"Invalid content type: {signedCms.ContentInfo.ContentType.Value}");
                }
 
                SignerInfoCollection signerInfos = signedCms.SignerInfos;
                SignerInfo signerInfo = GetPrimarySignerInfo(signerInfos);
 
                // Check the signatures
                signerInfo.CheckSignature(signedCms.Certificates, true);
                signedCms.CheckSignature(signedCms.Certificates, true);
 
                return true;
            }
            catch (Exception ex)
            {
                svr.AddDetail(DetailKeys.Error, ex.Message);
                return false;
            }
        }
 
        private static IEnumerable<Timestamp> GetTimestampsInternal(string path)
        {
            SignedCms signedCms = ReadSecurityInfo(path);
            if (signedCms == null)
            {
                return Enumerable.Empty<Timestamp>();
            }
 
            return ExtractTimestamps(signedCms);
        }
 
        private static SignedCms ReadSecurityInfo(string path)
        {
            using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (PEReader peReader = new PEReader(fs))
            {
                var securityDirectory = peReader.PEHeaders.PEHeader.CertificateTableDirectory;
                if (securityDirectory.RelativeVirtualAddress != 0 && securityDirectory.Size != 0)
                {
                    int securityHeaderSize = 8; // 4(length of cert) + 2(cert revision) + 2(cert type)
                    if (securityDirectory.Size <= securityHeaderSize)
                    {
                        // No security entry - just the header
                        return null;
                    }
 
                    // Skip the header
                    fs.Position = securityDirectory.RelativeVirtualAddress + securityHeaderSize;
                    byte[] securityEntry = new byte[securityDirectory.Size - securityHeaderSize];
 
                    // Ensure the stream has enough data to read
                    if (fs.Length < fs.Position + securityEntry.Length)
                    {
                        throw new CryptographicException($"File '{path}' is too small to contain a valid security entry.");
                    }
 
                    // Read the security entry
                    fs.ReadExactly(securityEntry);
 
                    // Decode the security entry
                    var signedCms = new SignedCms();
                    signedCms.Decode(securityEntry);
                    return signedCms;
                }
            }
 
            return null;
        }
 
        private static SignerInfo GetPrimarySignerInfo(SignerInfoCollection signerInfos)
        {
            int signerCount = signerInfos.Count;
            if (signerCount != 1)
            {
                throw new CryptographicException($"Invalid number of signers: {signerCount}. Expected 1.");
            }
 
            return signerInfos[0];
        }
#endif
 
        private static IEnumerable<Timestamp> ExtractTimestamps(SignedCms signedCms)
        {
            var timestamps = new List<Timestamp>();
            // Timestamp information can be stored in multiple sections.
            // A single SHA1 stores the timestamp as a counter sign in the unsigned attributes
            // Multiple authenticode signatures will store additional information as a nested signature
            // In the case of SHA2 signatures, we need to find and decode the timestamp token (RFC3161).
            // Luckily NuGet implemented a proper TST and DER parser to decode this
            foreach (SignerInfo signerInfo in signedCms.SignerInfos)
            {
                foreach (CryptographicAttributeObject unsignedAttribute in signerInfo.UnsignedAttributes)
                {
                    if (String.Equals(unsignedAttribute.Oid.Value, WinCrypt.szOID_RSA_counterSign, StringComparison.OrdinalIgnoreCase))
                    {
                        foreach (SignerInfo counterSign in signerInfo.CounterSignerInfos)
                        {
                            foreach (CryptographicAttributeObject signedAttribute in counterSign.SignedAttributes)
                            {
                                if (String.Equals(signedAttribute.Oid.Value, WinCrypt.szOID_RSA_signingTime, StringComparison.OrdinalIgnoreCase))
                                {
                                    var st = (Pkcs9SigningTime)signedAttribute.Values[0];
                                    X509Certificate2 cert = counterSign.Certificate;
 
                                    var timeStamp = new Timestamp
                                    {
                                        SignedOn = st.SigningTime.ToLocalTime(),
                                        EffectiveDate = Convert.ToDateTime(cert.GetEffectiveDateString()).ToLocalTime(),
                                        ExpiryDate = Convert.ToDateTime(cert.GetExpirationDateString()).ToLocalTime(),
                                        SignatureAlgorithm = cert.SignatureAlgorithm.FriendlyName
                                    };
 
                                    timestamps.Add(timeStamp);
                                }
                            }
                        }
                    }
                    else if (String.Equals(unsignedAttribute.Oid.Value, WinCrypt.szOID_RFC3161_counterSign, StringComparison.OrdinalIgnoreCase))
                    {
                        timestamps.AddRange(GetTimestampsFromCounterSignature(unsignedAttribute.Values[0]));
                    }
                    else if (String.Equals(unsignedAttribute.Oid.Value, WinCrypt.szOID_NESTED_SIGNATURE, StringComparison.OrdinalIgnoreCase))
                    {
                        var nestedSignature = new Pkcs9AttributeObject(unsignedAttribute.Values[0]);
                        SignedCms nestedSignatureMessage = new SignedCms();
                        nestedSignatureMessage.Decode(nestedSignature.RawData);
 
                        foreach (SignerInfo nestedSignerInfo in nestedSignatureMessage.SignerInfos)
                        {
                            foreach (CryptographicAttributeObject nestedUnsignedAttribute in nestedSignerInfo.UnsignedAttributes)
                            {
                                if (String.Equals(nestedUnsignedAttribute.Oid.Value, WinCrypt.szOID_RFC3161_counterSign, StringComparison.OrdinalIgnoreCase))
                                {
                                    timestamps.AddRange(GetTimestampsFromCounterSignature(nestedUnsignedAttribute.Values[0]));
                                }
                            }
                        }
                    }
                }
            }
 
            return timestamps;
        }
 
        private static IEnumerable<Timestamp> GetTimestampsFromCounterSignature(AsnEncodedData unsignedAttribute)
        {
            var timestamps = new List<Timestamp>();
            var rfc3161CounterSignature = new Pkcs9AttributeObject(unsignedAttribute);
            SignedCms rfc3161Message = new SignedCms();
            rfc3161Message.Decode(rfc3161CounterSignature.RawData);
 
            foreach (SignerInfo rfc3161SignerInfo in rfc3161Message.SignerInfos)
            {
                if (String.Equals(rfc3161Message.ContentInfo.ContentType.Value, WinCrypt.szOID_TIMESTAMP_TOKEN, StringComparison.OrdinalIgnoreCase))
                {
                    var timestampToken = NuGet.Packaging.Signing.TstInfo.Read(rfc3161Message.ContentInfo.Content);
 
                    var timeStamp = new Timestamp
                    {
                        SignedOn = timestampToken.GenTime.LocalDateTime,
                        EffectiveDate = Convert.ToDateTime(rfc3161SignerInfo.Certificate.GetEffectiveDateString()).ToLocalTime(),
                        ExpiryDate = Convert.ToDateTime(rfc3161SignerInfo.Certificate.GetExpirationDateString()).ToLocalTime(),
                        SignatureAlgorithm = rfc3161SignerInfo.Certificate.SignatureAlgorithm.FriendlyName
                    };
 
                    timestamps.Add(timeStamp);
                }
            }
 
            return timestamps;
        }
    }
}