File: src\StrongName.cs
Web Access
Project: src\src\Microsoft.DotNet.SignTool\Microsoft.DotNet.SignTool.csproj (Microsoft.DotNet.SignTool)
// 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.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
 
namespace Microsoft.DotNet.SignTool
{
    public static class StrongName
    {
        // Checksum offset in the PE header.
        internal const int ChecksumOffsetInPEHeader = 0x40;
        internal const int CheckSumSize = sizeof(uint);
 
        // Internal constants obtained from runtime's
        // src/libraries/System.Reflection.Metadata/src/System/Reflection/PortableExecutable/PEHeader.cs
        private const int PEHeaderSize32Bit = 224;
        private const int PEHeaderSize64Bit = 240;
        private const int PESectionHeaderSize = 40;
        private const int AuthenticodeDirectorySize = 2 * sizeof(int);
        private const int SnPublicKeyHeaderSize = 12;
 
        private const int BlobHeaderSize = sizeof(byte) + sizeof(byte) + sizeof(ushort) + sizeof(uint);
        private const int RsaPubKeySize = sizeof(uint) + sizeof(uint) + sizeof(uint);
 
        private const UInt32 RSA1 = 0x31415352;
        private const UInt32 RSA2 = 0x32415352;
 
        // In wincrypt.h both public and private key blobs start with a
        // PUBLICKEYSTRUC and RSAPUBKEY and then start the key data
        private const int OffsetToKeyData = BlobHeaderSize + RsaPubKeySize;
 
        // From wincrypt.h
        private const byte PublicKeyBlobId = 0x06;
        private const byte PrivateKeyBlobId = 0x07;
 
        // from winnt.h
        private const int FlagsOffsetInCorHeader = sizeof(uint) + // cb
                                                   sizeof(ushort) + // MajorRuntimeVersion
                                                   sizeof(ushort) + // MinorRuntimeVersion
                                                   sizeof(uint) * 2; // MetaData
        internal const int CorFlagsSize = sizeof(uint);
 
        /// <summary>
        /// Neutral public key indicates that the ECMA key was used to strong name the binary.
        /// </summary>
        static readonly byte[] NeutralPublicKey = { 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0 };
 
        // ECMA key
        static readonly byte[] ECMAKey =
        {
            0x00,0x24,0x00,0x00,0x04,0x80,0x00,0x00,0x94,0x00,0x00,0x00,0x06,0x02,0x00,0x00,
            0x00,0x24,0x00,0x00,0x52,0x53,0x41,0x31,0x00,0x04,0x00,0x00,0x01,0x00,0x01,0x00,
            0x07,0xd1,0xfa,0x57,0xc4,0xae,0xd9,0xf0,0xa3,0x2e,0x84,0xaa,0x0f,0xae,0xfd,0x0d,
            0xe9,0xe8,0xfd,0x6a,0xec,0x8f,0x87,0xfb,0x03,0x76,0x6c,0x83,0x4c,0x99,0x92,0x1e,
            0xb2,0x3b,0xe7,0x9a,0xd9,0xd5,0xdc,0xc1,0xdd,0x9a,0xd2,0x36,0x13,0x21,0x02,0x90,
            0x0b,0x72,0x3c,0xf9,0x80,0x95,0x7f,0xc4,0xe1,0x77,0x10,0x8f,0xc6,0x07,0x77,0x4f,
            0x29,0xe8,0x32,0x0e,0x92,0xea,0x05,0xec,0xe4,0xe8,0x21,0xc0,0xa5,0xef,0xe8,0xf1,
            0x64,0x5c,0x4c,0x0c,0x93,0xc1,0xab,0x99,0x28,0x5d,0x62,0x2c,0xaa,0x65,0x2c,0x1d,
            0xfa,0xd6,0x3d,0x74,0x5d,0x6f,0x2d,0xe5,0xf1,0x7e,0x5e,0xaf,0x0f,0xc4,0x96,0x3d,
            0x26,0x1c,0x8a,0x12,0x43,0x65,0x18,0x20,0x6d,0xc0,0x93,0x34,0x4d,0x5a,0xd2,0x93
        };
 
        /// <summary>
        /// Returns true if the file has a valid strong name signature.
        /// </summary>
        /// <param name="file">Path to file</param>
        /// <param name="log">MSBuild logger if desired</param>
        /// <param name="snPath">Path to sn.exe, if available and desired.</param>
        /// <returns>True if the file has a valid strong name signature, false otherwise.</returns>
        public static bool IsSigned(string file, string snPath = null, TaskLoggingHelper log = null)
        {
            try
            {
                using (var metadata = new FileStream(file, FileMode.Open))
                {
                    return IsSigned(metadata);
                }
            }
            catch (Exception e)
            {
                if (log != null)
                {
                    log.LogMessage(MessageImportance.High, $"Failed to determine whether PE file {file} has a valid strong name signature. {e}");
                }
 
                if (!string.IsNullOrEmpty(snPath))
                {
                    // Fall back to the old method of checking for a strong name signature, but only on Windows.
                    // Otherwise, return false:
                    return IsSigned_Legacy(file, snPath);
                }
            }
 
            return false;
        }
 
        // Internal for testing to avoid having to write a file to disk.
        internal static bool IsSigned(Stream peStream)
        {
            var peHeaders = new PEHeaders(peStream);
            peStream.Position = 0;
            using (PEReader peReader = new PEReader(peStream, PEStreamOptions.LeaveOpen))
            {
                if (!peReader.HasMetadata)
                {
                    return false;
                }
                // If the binary doesn't have metadata (e.g. crossgenned) then it's not signed.
                MetadataReader metadataReader = peReader.GetMetadataReader();
 
                var flags = peHeaders.CorHeader.Flags;
 
                // If the strong name bit isn't set, then it's not signed.
                if (CorFlags.StrongNameSigned != (flags & CorFlags.StrongNameSigned))
                {
                    return false;
                }
 
                // Reset position before creating the blob builder.
                peStream.Position = 0;
                byte[] peBuffer = ReadPEToBuffer(peStream);
 
                // Verify the checksum before verifying the strong name signature
                // PreparePEForHashing will zero out the checksum and authenticode signature.
 
                if (peHeaders.PEHeader.CheckSum != CalculateChecksum(peBuffer, peHeaders))
                {
                    return false;
                }
 
                // If the strong name signature data isn't present, then it's also not signed.
                var snDirectory = peReader.PEHeaders.CorHeader.StrongNameSignatureDirectory;
                if (!peHeaders.TryGetDirectoryOffset(snDirectory, out int snOffset))
                {
                    return false;
                }
 
                // Prepare the buffer for hashing. No need to set the strong name bit.
                PreparePEForHashing(peBuffer, peHeaders, setStrongNameBit: false);
 
                int snSize = snDirectory.Size;
                byte[] hash = ComputeSigningHash(peBuffer, peHeaders, snOffset, snSize);
 
                ImmutableArray<byte> publicKeyBlob = metadataReader.GetBlobContent(metadataReader.GetAssemblyDefinition().PublicKey);
 
                if (!IsValidPublicKey(publicKeyBlob))
                {
                    return false;
                }
 
                // It's possible that the public key blob is a neutral public key blob,
                // meaning that it's actually the ECMA key that was used to sign the assembly.
                // Verify against that.
                if (publicKeyBlob.SequenceEqual(NeutralPublicKey))
                {
                    publicKeyBlob = ECMAKey.ToImmutableArray();
                }
 
                using (RSA rsa = RSA.Create())
                {
                    // Ensure we skip over the sn key header.
                    rsa.ImportParameters(publicKeyBlob.Slice(RsaPubKeySize, publicKeyBlob.Length - RsaPubKeySize).ToRSAParameters(false));
 
                    var reversedSignature = peReader.GetSectionData(snDirectory.RelativeVirtualAddress).GetContent(0, snSize).ToArray();
 
                    // The signature bytes are in host (little-endian) byte order.
                    // Reverse the bytes to put them back into network byte order to verify the signature.
                    Array.Reverse(reversedSignature);
 
                    // CodeQL [SM02196] ECMA-335 requires us to support SHA-1 and this is testing that support
                    if (!rsa.VerifyHash(hash, reversedSignature, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1))
                    {
                        return false;
                    }
                }
            }
 
            return true;
        }
 
        /// <summary>
        /// Determine whether the file is strong named, using sn.exe instead
        /// of the custom implementation
        /// </summary>
        /// <param name="file">Path to file</param>
        /// <param name="snPath">sn.exe path</param>
        /// <returns>True if the file is strong named, false otherwise.</returns>
        internal static bool IsSigned_Legacy(string file, string snPath)
        {
            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                return false;
            }
 
            var process = Process.Start(new ProcessStartInfo()
            {
                FileName = snPath,
                Arguments = $@"-vf ""{file}"" > nul",
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardError = false,
                RedirectStandardOutput = false
            });
 
            process.WaitForExit();
 
            return process.ExitCode == 0;
        }
 
        /// <summary>
        /// Unset the strong name signing bit from a file. This is required for sn
        /// </summary>
        /// <param name="file"></param>
        public static void ClearStrongNameSignedBit(string file)
        {
            using (var stream = new FileStream(file, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
            using (var peReader = new PEReader(stream))
            using (var writer = new BinaryWriter(stream))
            {
                if (!ContentUtil.IsPublicSigned(peReader))
                {
                    return;
                }
 
                stream.Position = peReader.PEHeaders.CorHeaderStartOffset + FlagsOffsetInCorHeader;
                writer.Write((UInt32)(peReader.PEHeaders.CorHeader.Flags & ~CorFlags.StrongNameSigned));
            }
        }
 
        /// <summary>
        /// Strong names an existing previously signed or delay-signed binary with keyfile.
        /// Fall back to legacy signing if available and new signing fails.
        /// </summary>
        /// <param name="file">Path to file to sign</param>
        /// <param name="keyFile">Path to key pair.</param>
        /// <param name="log">Optional logger</param>
        /// <param name="snPath">Optional path to sn.exe</param>
        /// <returns>True if the file was signed successfully, false otherwise</returns>
        public static bool Sign(string file, string keyFile, string snPath = null, TaskLoggingHelper log = null)
        {
            try
            {
                using (var metadata = new FileStream(file, FileMode.Open))
                {
                    Sign(metadata, keyFile);
                }
                return true;
            }
            catch (Exception e)
            {
                if (log != null)
                {
                    log.LogMessage(MessageImportance.High, $"Failed to sign PE file {file} with strong name {keyFile}: {e}");
                }
 
                if (!string.IsNullOrEmpty(snPath))
                {
                    // Fall back to the old method of checking for a strong name signature, but only on Windows.
                    // Otherwise, return false:
                    return Sign_Legacy(file, keyFile, snPath);
                }
            }
 
            return false;
        }
 
        internal static bool Sign_Legacy(string file, string keyfile,  string snPath)
        {
            // sn -R <path_to_file> <path_to_snk>
            var process = Process.Start(new ProcessStartInfo()
            {
                FileName = snPath,
                Arguments = $@"-R ""{file}"" ""{keyfile}""",
                UseShellExecute = false
            });
 
            process.WaitForExit();
 
            if (process.ExitCode != 0)
            {
                return false;
            }
 
            return true;
        }
 
        /// <summary>
        /// Given a key file, sets the strong name in the managed binary
        /// </summary>
        /// <param name="peStream"></param>
        /// <param name="keyFile"></param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="Exception"></exception>
        internal static void Sign(Stream peStream, string keyFile)
        {
            // This process happens as follows:
            // 1. Open the PE and read into a file.
            // 2. Compute the signing hash of the binary (excluding the strong name and authenticode signatures)
            // 3. Read the key data from the provided file
            // 4. Attempt to sign the hash using the crypto service provider.
            // 5. Write the signature into the strong name signature directory.
            // 6. Compute the checksum of the binary and write it back into the PE header.
            // 7. Write the binary back to the file.
 
            byte[] signature;
            byte[] peBuffer;
            int snSignatureOffset;
            var peHeaders = new PEHeaders(peStream);
            
            // Reset the stream position before loading the PE
            peStream.Position = 0;
            using (PEReader peReader = new PEReader(peStream, PEStreamOptions.LeaveOpen))
            {
                if (!peReader.HasMetadata)
                {
                    throw new InvalidOperationException("Cannot strong name sign binary without metadata.");
                }
                // If the binary doesn't have metadata (e.g. crossgenned) then it's not signed.
                MetadataReader metadataReader = peReader.GetMetadataReader();
 
                // Parse the SNK
                if (!TryParseKey(File.ReadAllBytes(keyFile).ToImmutableArray(), out ImmutableArray<byte> snkPublicKey, out RSAParameters? privateKey) ||
                    privateKey == null)
                {
                    throw new InvalidOperationException($"Failed to parse strong name '{keyFile}'. Key must be a full public/private keypair");
                }
 
                // If the strong name signature data isn't present, then we can't sign it.
                var snDirectory = peReader.PEHeaders.CorHeader.StrongNameSignatureDirectory;
                if (!peHeaders.TryGetDirectoryOffset(snDirectory, out snSignatureOffset))
                {
                    throw new InvalidOperationException("Strong name directory is not present. Binary is not signed or delay-signed.");
                }
 
                // Verify the public key of the assembly matches the private key we're using to sign.
                // We do not support signing with the ECMA key
                ImmutableArray<byte> publicKeyBlob = metadataReader.GetBlobContent(metadataReader.GetAssemblyDefinition().PublicKey);
                if (publicKeyBlob.SequenceEqual(NeutralPublicKey))
                {
                    throw new NotImplementedException("Cannot sign with the ECMA key.");
                }
 
                // Verify that the public key of the assembly matches the public key of the provided key file.
                if (!TryParseKey(publicKeyBlob, out ImmutableArray<byte> assemblyPublicKey, out _))
                {
                    throw new InvalidOperationException("Failed to parse the public key of the assembly.");
                }
 
                if (!assemblyPublicKey.SequenceEqual(snkPublicKey))
                {
                    throw new InvalidOperationException("Public key of the assembly does not match the public key of the provided key file.");
                }
 
                // Copy the PE into a buffer
                peStream.Position = 0;
                peBuffer = ReadPEToBuffer(peStream);
 
                // Now prepare that buffer for hashing
                PreparePEForHashing(peBuffer, peHeaders, setStrongNameBit: true);
 
                byte[] hash = ComputeSigningHash(peBuffer, peHeaders, snSignatureOffset, snDirectory.Size);
                using (RSA snkRSA = RSA.Create())
                {
                    snkRSA.ImportParameters(privateKey.Value);
 
                    // CodeQL [SM02196] ECMA-335 requires us to support SHA-1 and this is testing that support
                    signature = snkRSA.SignHash(hash, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
 
                    // The signature is written in reverse order
                    Array.Reverse(signature);
                }
 
                // Write the signature into the strong name signature directory
                peBuffer.SetBytes(snSignatureOffset, signature);
 
                // Compute a new checksum and write it out.
                uint checksum = CalculateChecksum(peBuffer, peHeaders);
                var checksumBytes = BitConverter.GetBytes(checksum);
                peBuffer.SetBytes(peHeaders.PEHeaderStartOffset + ChecksumOffsetInPEHeader, checksumBytes);
            }
 
            // Write the PE stream back
            peStream.Position = 0;
            peStream.Write(peBuffer, 0, peBuffer.Length);
        }
 
        private static uint AggregateChecksum(uint checksum, ushort value)
        {
            uint sum = checksum + value;
            return (sum >> 16) + unchecked((ushort)sum);
        }
 
        private static uint CalculateChecksum(byte[] peImage, PEHeaders peHeaders)
        {
            return CalculateChecksum(GetContentWithoutChecksum(peImage, peHeaders)) + (uint)peImage.Length;
        }
 
        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;
        }
 
        private static byte[] ComputeSigningHash(
                    byte[] peImage,
                    PEHeaders peHeaders,
                    int strongNameOffset,
                    int strongNameSize)
        {
            int peHeadersSize = peHeadersSize = peHeaders.PEHeaderStartOffset
                + (peHeaders.PEHeader.Magic == PEMagic.PE32 ? PEHeaderSize32Bit : PEHeaderSize64Bit)
                + 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>
        /// 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>
        private static void PreparePEForHashing(byte[] peBuffer, PEHeaders peHeaders, bool setStrongNameBit)
        {
            bool is32bit = peHeaders.PEHeader.Magic == PEMagic.PE32;
 
            // Zero the checksum
            peBuffer.SetBytes(peHeaders.PEHeaderStartOffset + ChecksumOffsetInPEHeader, CheckSumSize, 0);
 
            // Zero the authenticode signature
            int authenticodeOffset = GetAuthenticodeOffset(peHeaders, is32bit);
            var authenticodeDir = peHeaders.PEHeader.CertificateTableDirectory;
            peBuffer.SetBytes(authenticodeOffset, AuthenticodeDirectorySize, 0);
 
            if (setStrongNameBit)
            {
                var flagBytes = BitConverter.GetBytes((uint)(peHeaders.CorHeader.Flags | CorFlags.StrongNameSigned));
                peBuffer.SetBytes(peHeaders.CorHeaderStartOffset + FlagsOffsetInCorHeader, flagBytes);
            }
        }
 
        /// <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;
            }
        }
 
        /// <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>
        private static void SetBytes(this byte[] buffer, int index, byte[] value)
        {
            for (int i = 0; i < value.Length; i++)
            {
                buffer[index + i] = value[i];
            }
        }
 
        private 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;
        }
 
        private static int GetAuthenticodeOffset(PEHeaders peHeaders, bool is32bit)
        {
            return peHeaders.PEHeaderStartOffset
                + 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 + ChecksumOffsetInPEHeader;
            int checksumEnd = checksumStart + 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();
        }
 
        /// <summary>
        /// Adapted from roslyn's CryptoBlobParser
        /// </summary>
        private enum AlgorithmClass
        {
            Signature = 1,
            Hash = 4,
        }
 
        /// <summary>
        /// Adapted from roslyn's CryptoBlobParser
        /// </summary>
        private enum AlgorithmSubId
        {
            Sha1Hash = 4,
            // Other possible values ommitted
        }
 
        /// <summary>
        /// Adapted from roslyn's CryptoBlobParser
        /// </summary>
        private struct AlgorithmId
        {
            // From wincrypt.h
            private const int AlgorithmClassOffset = 13;
            private const int AlgorithmClassMask = 0x7;
            private const int AlgorithmSubIdOffset = 0;
            private const int AlgorithmSubIdMask = 0x1ff;
 
            private readonly uint _flags;
 
            public const int RsaSign = 0x00002400;
            public const int Sha = 0x00008004;
 
            public bool IsSet
            {
                get { return _flags != 0; }
            }
 
            public AlgorithmClass Class
            {
                get { return (AlgorithmClass)((_flags >> AlgorithmClassOffset) & AlgorithmClassMask); }
            }
 
            public AlgorithmSubId SubId
            {
                get { return (AlgorithmSubId)((_flags >> AlgorithmSubIdOffset) & AlgorithmSubIdMask); }
            }
 
            public AlgorithmId(uint flags)
            {
                _flags = flags;
            }
        }
 
        // 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 < 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 != SnPublicKeyHeaderSize + publicKeySize)
            {
                return false;
            }
 
            // Check for the ECMA neutral public key, which does not obey the invariants checked below.
            if (blob.SequenceEqual(NeutralPublicKey))
            {
                return true;
            }
 
            // The public key must be in the wincrypto PUBLICKEYBLOB format
            if (publicKey != PublicKeyBlobId)
            {
                return false;
            }
 
            var signatureAlgorithmId = new AlgorithmId(sigAlgId);
            if (signatureAlgorithmId.IsSet && signatureAlgorithmId.Class != AlgorithmClass.Signature)
            {
                return false;
            }
 
            var hashAlgorithmId = new AlgorithmId(hashAlgId);
            if (hashAlgorithmId.IsSet && (hashAlgorithmId.Class != AlgorithmClass.Hash || hashAlgorithmId.SubId < AlgorithmSubId.Sha1Hash))
            {
                return false;
            }
 
            return true;
        }
 
        private static ImmutableArray<byte> CreateSnPublicKeyBlob(
            byte type,
            byte version,
            uint algId,
            uint magic,
            uint bitLen,
            uint pubExp,
            ReadOnlySpan<byte> pubKeyData)
        {
            var w = new BlobWriter(3 * sizeof(uint) + OffsetToKeyData + pubKeyData.Length);
            w.WriteUInt32(AlgorithmId.RsaSign);
            w.WriteUInt32(AlgorithmId.Sha);
            w.WriteUInt32((uint)(OffsetToKeyData + pubKeyData.Length));
 
            w.WriteByte(type);
            w.WriteByte(version);
            w.WriteUInt16(0 /* 16 bits of reserved space in the spec */);
            w.WriteUInt32(algId);
 
            w.WriteUInt32(magic);
            w.WriteUInt32(bitLen);
 
            // re-add padding for exponent
            w.WriteUInt32(pubExp);
 
            unsafe
            {
                fixed (byte* bytes = pubKeyData)
                {
                    w.WriteBytes(bytes, pubKeyData.Length);
                }
            }
 
            return w.ToImmutableArray();
        }
 
        /// <summary>
        /// Try to retrieve the public key from a crypto blob.
        /// </summary>
        /// <remarks>
        /// Can be either a PUBLICKEYBLOB or PRIVATEKEYBLOB. The BLOB must be unencrypted.
        /// </remarks>
        public static bool TryParseKey(ImmutableArray<byte> blob, out ImmutableArray<byte> snKey, out RSAParameters? privateKey)
        {
            privateKey = null;
            snKey = default;
 
            if (IsValidPublicKey(blob))
            {
                snKey = blob;
                return true;
            }
 
            if (blob.Length < BlobHeaderSize + RsaPubKeySize)
            {
                return false;
            }
 
            try
            {
                MemoryStream stream = new MemoryStream(blob.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
                uint algId = br.ReadUInt32();  // BLOBHEADER.aiKeyAlg
                uint magic = br.ReadUInt32();  // RSAPubKey.magic: Expected to be 0x31415352 ('RSA1') or 0x32415352 ('RSA2') 
                var bitLen = br.ReadUInt32();  // Bit Length for Modulus
                var pubExp = br.ReadUInt32();  // Exponent 
                var modulusLength = (int)(bitLen / 8);
 
                if (blob.Length - OffsetToKeyData < modulusLength)
                {
                    return false;
                }
 
                var modulus = br.ReadBytes(modulusLength);
 
                if (!(bType == PrivateKeyBlobId && magic == RSA2) && !(bType == PublicKeyBlobId && magic == RSA1))
                {
                    return false;
                }
 
                if (bType == PrivateKeyBlobId)
                {
                    privateKey = blob.ToRSAParameters(true);
                    // For snKey, rewrite some of the parameters
                    algId = AlgorithmId.RsaSign;
                    magic = RSA1;
                }
 
                snKey = CreateSnPublicKeyBlob(PublicKeyBlobId, bVersion, algId, RSA1, bitLen, pubExp, modulus);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }
 
        /// <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;
        }
 
        private static byte[] ReadReversed(this BinaryReader reader, int count)
        {
            byte[] data = reader.ReadBytes(count);
            Array.Reverse(data);
            return data;
        }
 
        /// <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)
                };
            }
        }
    }
}