File: MachO\BinaryFormat\Blobs\EmbeddedSignatureBlob.cs
Web Access
Project: src\src\runtime\src\installer\managed\Microsoft.NET.HostModel\Microsoft.NET.HostModel.csproj (Microsoft.NET.HostModel)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Collections.Immutable;
using System.IO;

namespace Microsoft.NET.HostModel.MachO;

/// <summary>
/// Format based off of https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_codesigning/lib/cscdefs.h#L23
/// Code Signature data is always big endian / network order.
/// The EmbeddedSignatureBlob is a SuperBlob that usually contains the CodeDirectoryBlob, RequirementsBlob, and CmsWrapperBlob.
/// The RequirementsBlob and CmsWrapperBlob may be null if the blob is not present in the read file (usually linker signed MachO files),
/// but will be present in newly created signatures.
/// </summary>
internal sealed class EmbeddedSignatureBlob : IBlob
{
    private SuperBlob _inner;

    public EmbeddedSignatureBlob(SuperBlob superBlob)
    {
        _inner = superBlob;
        if (superBlob.Magic != BlobMagic.EmbeddedSignature)
        {
            throw new InvalidDataException($"Invalid magic for EmbeddedSignatureBlob: {superBlob.Magic}");
        }
    }

    /// <summary>
    /// Creates a new EmbeddedSignatureBlob with the specified blobs.
    /// </summary>
    public EmbeddedSignatureBlob(
        CodeDirectoryBlob codeDirectoryBlob,
        RequirementsBlob requirementsBlob,
        CmsWrapperBlob cmsWrapperBlob)
    {
        int blobCount = 3;
        var blobs = ImmutableArray.CreateBuilder<IBlob>(blobCount);
        var blobIndices = ImmutableArray.CreateBuilder<BlobIndex>(blobCount);
        uint expectedOffset = (uint)(sizeof(uint) * 3 + (BlobIndex.Size * blobCount));
        blobs.Add(codeDirectoryBlob);
        blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.CodeDirectory, expectedOffset));
        expectedOffset += codeDirectoryBlob.Size;
        blobs.Add(requirementsBlob);
        blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.Requirements, expectedOffset));
        expectedOffset += requirementsBlob.Size;
        blobs.Add(cmsWrapperBlob);
        blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.CmsWrapper, expectedOffset));
        _inner = new SuperBlob(BlobMagic.EmbeddedSignature, blobIndices.MoveToImmutable(), blobs.MoveToImmutable());
    }

    public BlobMagic Magic => _inner.Magic;
    public uint Size => _inner.Size;
    public uint SubBlobCount => _inner.SubBlobCount;

    /// <summary>
    /// The CodeDirectoryBlob. This is always present in the signature.
    /// </summary>
    public CodeDirectoryBlob CodeDirectoryBlob => (CodeDirectoryBlob)GetBlob(BlobMagic.CodeDirectory, throwIfNotFound: true)!;

    /// <summary>
    /// The RequirementsBlob. This may be null if the blob is not present in the read file, but will be present in newly created signatures
    /// </summary>
    public RequirementsBlob? RequirementsBlob => GetBlob(BlobMagic.Requirements) as RequirementsBlob;

    /// <summary>
    /// The CmsWrapperBlob. This may be null if the blob is not present in the read file, but will be present in newly created signatures
    /// </summary>
    public CmsWrapperBlob? CmsWrapperBlob => GetBlob(BlobMagic.CmsWrapper) as CmsWrapperBlob;

    public uint GetSpecialSlotHashCount()
    {
        uint maxSlot = 0;
        foreach (var b in _inner.BlobIndices)
        {
            // Blobs that have special slots hashes have their slot value in the lower 8 bits.
            // CMSWrapperBlob has a special slot value of 0x1000 and does not have a hash.
            uint slot = 0xFF & (uint)b.Slot;
            if (slot > maxSlot)
            {
                maxSlot = slot;
            }
        }
        return maxSlot;
    }

    public int Write(IMachOFileWriter writer, long offset)
    {
        return _inner.Write(writer, offset);
    }

    /// <summary>
    /// Gets the largest size estimate for a code signature.
    /// </summary>
    public static unsafe long GetLargestSizeEstimate(uint fileSize, string identifier, byte? hashSize = null)
    {
        byte usedHashSize = hashSize ?? CodeDirectoryBlob.DefaultHashType.GetHashSize();

        long size = 0;
        // SuperBlob header
        size += sizeof(BlobMagic);
        size += sizeof(uint); // Blob size
        size += sizeof(uint); // Blob count
        size += sizeof(BlobIndex) * 3; // 3 sub-blobs: CodeDirectory, Requirements, CmsWrapper

        // CodeDirectoryBlob
        size += sizeof(BlobMagic);
        size += sizeof(uint); // Blob size
        size += sizeof(CodeDirectoryBlob.CodeDirectoryHeader); // CodeDirectory header
        size += CodeDirectoryBlob.GetIdentifierLength(identifier); // Identifier
        size += (long)CodeDirectoryBlob.GetCodeSlotCount(fileSize) * usedHashSize; // Code hashes
        size += (long)(uint)CodeDirectorySpecialSlot.Requirements * usedHashSize; // Special code hashes

        size += RequirementsBlob.Empty.Size; // Requirements is always written as an empty blob
        size += CmsWrapperBlob.Empty.Size; // CMS blob is always written as an empty blob
        return size;
    }

    /// <summary>
    /// Returns the size of a signature used to replace an existing one.
    /// If the existing signature is null, it will assume sizing using the default signature, which includes the Requirements and CMS blobs.
    /// </summary>
    internal static unsafe long GetSignatureSize(uint fileSize, string identifier, byte? hashSize = null)
    {
        byte usedHashSize = hashSize ?? CodeDirectoryBlob.DefaultHashType.GetHashSize();
        uint specialCodeSlotCount = (uint)CodeDirectorySpecialSlot.Requirements;
        uint embeddedSignatureSubBlobCount = 3; // CodeDirectory, Requirements, CMS Wrapper are always present

        // Calculate the size of the new signature
        long size = 0;
        // EmbeddedSignature
        size += sizeof(BlobMagic); // Signature blob Magic number
        size += sizeof(uint); // Size field
        size += sizeof(uint); // Blob count
        size += sizeof(BlobIndex) * embeddedSignatureSubBlobCount; // EmbeddedSignature sub-blobs
        // CodeDirectory
        size += sizeof(BlobMagic); // CD Magic number
        size += sizeof(uint); // CD Size field
        size += sizeof(CodeDirectoryBlob.CodeDirectoryHeader); // CodeDirectory header
        size += CodeDirectoryBlob.GetIdentifierLength(identifier); // Identifier
        size += specialCodeSlotCount * usedHashSize; // Special code hashes
        size += CodeDirectoryBlob.GetCodeSlotCount(fileSize) * usedHashSize; // Code hashes
        // RequirementsBlob
        size += RequirementsBlob.Empty.Size;
        // CmsWrapperBlob
        size += CmsWrapperBlob.Empty.Size;
        return size;
    }

    private IBlob? GetBlob(BlobMagic magic, bool throwIfNotFound = false)
    {
        foreach (var b in _inner.Blobs)
        {
            if (b.Magic == magic)
            {
                return b;
            }
        }
        if (throwIfNotFound)
        {
            throw new InvalidOperationException($"{magic} blob not found.");
        }
        return null;
    }

    public static void AssertEquivalent(EmbeddedSignatureBlob? a, EmbeddedSignatureBlob? b)
    {
        if (a == null && b == null)
            return;

        if (a == null || b == null)
            throw new ArgumentNullException("Both EmbeddedSignatureBlobs must be non-null for comparison.");

        if (a.GetSpecialSlotHashCount() != b.GetSpecialSlotHashCount())
            throw new ArgumentException("Special slot hash counts are not equivalent.");

        if (!a.CodeDirectoryBlob.Equals(b.CodeDirectoryBlob))
            throw new ArgumentException("CodeDirectory blobs are not equivalent");

        if (a.RequirementsBlob?.Size != b.RequirementsBlob?.Size)
            throw new ArgumentException("Requirements blobs are not equivalent");

        if (a.CmsWrapperBlob?.Size != b.CmsWrapperBlob?.Size)
            throw new ArgumentException("CMS Wrapper blobs are not equivalent");
    }
}