File: Utils.cs
Web Access
Project: 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;
    }
}