File: Utils.cs
Web Access
Project: src\src\Microsoft.DotNet.StrongName\Microsoft.DotNet.StrongName.csproj (Microsoft.DotNet.StrongName)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;
 
namespace Microsoft.DotNet.StrongName
{
    internal static class Utils
    {
        // From StrongNameInternal.cpp
        // Checks to see if a public key is a valid instance of a PublicKeyBlob as
        // defined in StongName.h
        internal static bool IsValidPublicKey(ImmutableArray<byte> blob)
        {
            // The number of public key bytes must be at least large enough for the header and one byte of data.
            if (blob.IsDefault || blob.Length < Constants.SnPublicKeyHeaderSize + 1)
            {
                return false;
            }
 
            var blobStream = new MemoryStream(blob.ToArray());
            var blobReader = new BinaryReader(blobStream);
 
            // Signature algorithm ID
            var sigAlgId = blobReader.ReadUInt32();
            // Hash algorithm ID
            var hashAlgId = blobReader.ReadUInt32();
            // Size of public key data in bytes, not including the header
            var publicKeySize = blobReader.ReadUInt32();
            // publicKeySize bytes of public key data
            var publicKey = blobReader.ReadByte();
 
            // The number of public key bytes must be the same as the size of the header plus the size of the public key data.
            if (blob.Length != Constants.SnPublicKeyHeaderSize + publicKeySize)
            {
                return false;
            }
 
            // Check for the ECMA neutral public key, which does not obey the invariants checked below.
            if (blob.SequenceEqual(Constants.NeutralPublicKey))
            {
                return true;
            }
 
            // The public key must be in the wincrypto PUBLICKEYBLOB format
            if (publicKey != Constants.PublicKeyBlobId)
            {
                return false;
            }
 
            var signatureAlgorithmId = new Algorithm.AlgorithmId(sigAlgId);
            if (signatureAlgorithmId.IsSet && signatureAlgorithmId.Class != Algorithm.AlgorithmClass.Signature)
            {
                return false;
            }
 
            var hashAlgorithmId = new Algorithm.AlgorithmId(hashAlgId);
            if (hashAlgorithmId.IsSet && (hashAlgorithmId.Class != Algorithm.AlgorithmClass.Hash || hashAlgorithmId.SubId < Algorithm.AlgorithmSubId.Sha1Hash))
            {
                return false;
            }
 
            return true;
        }
 
        /// <summary>
        /// Prepare a PE buffer for hashing by zeroing out the checksum and authenticode signature, and
        /// potentially setting the strong name bit.
        /// </summary>
        /// <param name="peBuffer">PE buffer</param>
        /// <param name="peHeaders">Headers</param>
        /// <param name="setStrongNameBit">If true, strong name bit is set.</param>
        internal static void PreparePEForHashing(byte[] peBuffer, PEHeaders peHeaders, bool setStrongNameBit)
        {
            bool is32bit = peHeaders.PEHeader.Magic == PEMagic.PE32;
 
            // Zero the checksum
            peBuffer.SetBytes(peHeaders.PEHeaderStartOffset + Constants.ChecksumOffsetInPEHeader, Constants.CheckSumSize, 0);
 
            // Zero the authenticode signature
            int authenticodeOffset = GetAuthenticodeOffset(peHeaders, is32bit);
            var authenticodeDir = peHeaders.PEHeader.CertificateTableDirectory;
            peBuffer.SetBytes(authenticodeOffset, Constants.AuthenticodeDirectorySize, 0);
 
            if (setStrongNameBit)
            {
                var flagBytes = BitConverter.GetBytes((uint)(peHeaders.CorHeader.Flags | CorFlags.StrongNameSigned));
                peBuffer.SetBytes(peHeaders.CorHeaderStartOffset + Constants.FlagsOffsetInCorHeader, flagBytes);
            }
        }
 
        /// <summary>
        /// Sets the bytes in the buffer starting at <paramref name="index"/> to the bytes in <paramref name="value"/>
        /// </summary>
        /// <param name="buffer">Buffer to alter</param>
        /// <param name="index">Starting index</param>
        /// <param name="value">Value</param>
        internal static void SetBytes(this byte[] buffer, int index, byte[] value)
        {
            for (int i = 0; i < value.Length; i++)
            {
                buffer[index + i] = value[i];
            }
        }
 
        internal static byte[] ReadPEToBuffer(Stream peStream)
        {
            byte[] peImage = new byte[checked((int)peStream.Length)];
 
            peStream.Position = 0;
            if (peStream.Read(peImage, 0, peImage.Length) != peImage.Length)
            {
                throw new InvalidOperationException("Failed to read the full PE file.");
            }
 
            return peImage;
        }
 
        internal static uint CalculateChecksum(byte[] peImage, PEHeaders peHeaders)
        {
            return CalculateChecksum(GetContentWithoutChecksum(peImage, peHeaders)) + (uint)peImage.Length;
        }
 
        internal static byte[] ComputeSigningHash(
                    byte[] peImage,
                    PEHeaders peHeaders,
                    int strongNameOffset,
                    int strongNameSize)
        {
            int peHeadersSize = peHeadersSize = peHeaders.PEHeaderStartOffset
                + (peHeaders.PEHeader.Magic == PEMagic.PE32 ? Constants.PEHeaderSize32Bit : Constants.PEHeaderSize64Bit)
                + Constants.PESectionHeaderSize * peHeaders.SectionHeaders.Length;
 
            // CodeQL [SM02196] ECMA-335 requires us to support SHA-1 and this is testing that support
            using (var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA1))
            {
                // First hash the DOS header and PE headers
                hash.AppendData(peImage, 0, peHeadersSize);
 
                // Now each section, skipping the strong name signature if present
                foreach (var sectionHeader in peHeaders.SectionHeaders)
                {
                    int sectionOffset = sectionHeader.PointerToRawData;
                    int sectionSize = sectionHeader.SizeOfRawData;
 
                    if ((strongNameOffset + strongNameSize) < sectionOffset ||
                        strongNameOffset >= (sectionOffset + sectionSize))
                    {
                        // No signature overlap, hash the whole section
                        hash.AppendData(peImage, sectionOffset, sectionSize);
                    }
                    else
                    {
                        // There is overlap. Hash either side of signature
                        hash.AppendData(peImage, sectionOffset, strongNameOffset - sectionOffset);
                        var strongNameEndOffset = strongNameOffset + strongNameSize;
                        hash.AppendData(peImage, strongNameEndOffset, sectionSize - (strongNameEndOffset - sectionOffset));
                    }
                }
 
                return hash.GetHashAndReset();
            }
        }
 
        /// <summary>
        /// Helper for RsaCryptoServiceProvider.ExportParameters()
        /// Adapted from https://github.com/dotnet/roslyn/blob/2f0aa43abd019143ae4662b5ccca11d4d666a61f/src/Compilers/Core/Portable/StrongName/CryptoBlobParser.cs#L257
        /// </summary>
        internal static RSAParameters ToRSAParameters(this ImmutableArray<byte> cspBlob, bool includePrivateParameters)
        {
            MemoryStream stream = new MemoryStream(cspBlob.ToArray());
            var br = new BinaryReader(stream);
 
            byte bType = br.ReadByte();    // BLOBHEADER.bType: Expected to be 0x6 (PUBLICKEYBLOB) or 0x7 (PRIVATEKEYBLOB), though there's no check for backward compat reasons. 
            byte bVersion = br.ReadByte(); // BLOBHEADER.bVersion: Expected to be 0x2, though there's no check for backward compat reasons.
            br.ReadUInt16();               // BLOBHEADER.wReserved
            int algId = br.ReadInt32();    // BLOBHEADER.aiKeyAlg
 
            int magic = br.ReadInt32();    // RSAPubKey.magic: Expected to be 0x31415352 ('RSA1') or 0x32415352 ('RSA2') 
            int bitLen = br.ReadInt32();   // RSAPubKey.bitLen
 
            int modulusLength = bitLen / 8;
            int halfModulusLength = (modulusLength + 1) / 2;
 
            uint expAsDword = br.ReadUInt32();
 
            RSAParameters rsaParameters = new RSAParameters();
            rsaParameters.Exponent = ExponentAsBytes(expAsDword);
            rsaParameters.Modulus = br.ReadReversed(modulusLength);
 
            if (includePrivateParameters)
            {
                rsaParameters.P = br.ReadReversed(halfModulusLength);
                rsaParameters.Q = br.ReadReversed(halfModulusLength);
                rsaParameters.DP = br.ReadReversed(halfModulusLength);
                rsaParameters.DQ = br.ReadReversed(halfModulusLength);
                rsaParameters.InverseQ = br.ReadReversed(halfModulusLength);
                rsaParameters.D = br.ReadReversed(modulusLength);
            }
 
            return rsaParameters;
        }
 
        /// <summary>
        /// Gets the public key blob from the assembly definition.
        /// </summary>
        /// <param name="metadataReader">Metadata reader</param>
        /// <returns>Public key blob</returns>
        internal static ImmutableArray<byte> GetPublicKeyBlob(this MetadataReader metadataReader)
        {
            var publicKey = metadataReader.GetAssemblyDefinition().PublicKey;
            if (publicKey.IsNil)
            {
                return ImmutableArray<byte>.Empty;
            }
 
            return metadataReader.GetBlobContent(publicKey);
        }
 
        /// <summary>
        /// Sets <paramref name="count"/> bytes starting at <paramref name="index"/> in buffer to value
        /// </summary>
        /// <param name="buffer">Buffer to alter</param>
        /// <param name="index">Start index</param>
        /// <param name="count">count</param>
        /// <param name="value">Value to set</param>
        private static void SetBytes(this byte[] buffer, int index, int count, byte value)
        {
            for (int i = 0; i < count; i++)
            {
                buffer[index + i] = value;
            }
        }
 
        private static int GetAuthenticodeOffset(PEHeaders peHeaders, bool is32bit)
        {
            return peHeaders.PEHeaderStartOffset
                + Constants.ChecksumOffsetInPEHeader
                + sizeof(int)                                  // Checksum
                + sizeof(short)                                // Subsystem
                + sizeof(short)                                // DllCharacteristics
                + 4 * (is32bit ? sizeof(int) : sizeof(long))   // SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve, SizeOfHeapCommit
                + sizeof(int)                                  // LoaderFlags
                + sizeof(int)                                  // NumberOfRvaAndSizes
                + 4 * sizeof(long);                            // directory entries before Authenticode
        }
 
        private static IEnumerable<Blob> GetContentWithoutChecksum(byte[] peImage, PEHeaders peHeaders)
        {
            BlobBuilder imageWithoutChecksum = new BlobBuilder();
            int checksumStart = peHeaders.PEHeaderStartOffset + Constants.ChecksumOffsetInPEHeader;
            int checksumEnd = checksumStart + Constants.CheckSumSize;
            // Content up to the checksum
            imageWithoutChecksum.WriteBytes(peImage, 0, checksumStart);
            // Content after the checksum
            imageWithoutChecksum.WriteBytes(peImage, checksumEnd, peImage.Length - checksumEnd);
            return imageWithoutChecksum.GetBlobs();
        }
 
        private static uint AggregateChecksum(uint checksum, ushort value)
        {
            uint sum = checksum + value;
            return (sum >> 16) + unchecked((ushort)sum);
        }
 
        private static uint CalculateChecksum(IEnumerable<Blob> blobs)
        {
            uint checksum = 0;
            int pendingByte = -1;
 
            // Iterates over the blobs in the PE image.
            // For each pair of bytes in the blob, compute an aggregate checksum
            // If the blob has an odd number of bytes, save the value of the last byte
            // and pair it with the first byte of the next blob. If there is no next blob, aggregate it.
            foreach (var blob in blobs)
            {
                var segment = blob.GetBytes().ToList();
                Debug.Assert(segment.Count > 0);
 
                int currIndex = 0;
                int count = segment.Count;
                if (pendingByte >= 0)
                {
                    checksum = AggregateChecksum(checksum, (ushort)(segment[currIndex] << 8 | pendingByte));
                    currIndex++;
                }
 
                if ((count - currIndex) % 2 != 0)
                {
                    // Save last byte for later
                    pendingByte = segment[count - 1];
                    count--;
                }
                else
                {
                    pendingByte = -1;
                }
 
                while (currIndex < count)
                {
                    checksum = AggregateChecksum(checksum, (ushort)(segment[currIndex + 1] << 8 | segment[currIndex]));
                    currIndex += 2;
                }
            }
 
            if (pendingByte >= 0)
            {
                checksum = AggregateChecksum(checksum, (ushort)pendingByte);
            }
 
            return checksum;
        }
 
        /// <summary>
        /// Convert a UInt32 into the minimal number of bytes (in big-endian order) to represent the value.
        /// Copied from https://github.com/dotnet/corefx/blob/5fe5f9aae7b2987adc7082f90712b265bee5eefc/src/System.Security.Cryptography.Csp/src/System/Security/Cryptography/CapiHelper.Shared.cs
        /// </summary>
        private static byte[] ExponentAsBytes(uint exponent)
        {
            if (exponent <= 0xFF)
            {
                return new[] { (byte)exponent };
            }
            else if (exponent <= 0xFFFF)
            {
                unchecked
                {
                    return new[]
                    {
                        (byte)(exponent >> 8),
                        (byte)(exponent)
                    };
                }
            }
            else if (exponent <= 0xFFFFFF)
            {
                unchecked
                {
                    return new[]
                    {
                        (byte)(exponent >> 16),
                        (byte)(exponent >> 8),
                        (byte)(exponent)
                    };
                }
            }
            else
            {
                return new[]
                {
                    (byte)(exponent >> 24),
                    (byte)(exponent >> 16),
                    (byte)(exponent >> 8),
                    (byte)(exponent)
                };
            }
        }
 
        private static byte[] ReadReversed(this BinaryReader reader, int count)
        {
            byte[] data = reader.ReadBytes(count);
            Array.Reverse(data);
            return data;
        }
    }
}