File: Verification\MachOVerifier.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.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using Microsoft.SignCheck.Logging;
using System.Security.Cryptography;
 
namespace Microsoft.SignCheck.Verification
{
    public class MachOVerifier : FileVerifier
    {
        public MachOVerifier(Log log, Exclusions exclusions, SignatureVerificationOptions options, string fileExtension) : base(log, exclusions, options, fileExtension) { }
 
        public override SignatureVerificationResult VerifySignature(string path, string parent, string virtualPath)
        {
            try
            {
                if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
                {
                    throw new PlatformNotSupportedException($"Mach-O signature verification is only supported on macOS.");
                }
 
                using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
                using (BinaryReader reader = new BinaryReader(fs))
                {
                    uint magic = reader.ReadUInt32();
                    if (magic != FileHeaders.MachO32 && magic != FileHeaders.MachO64)
                    {
                        throw new InvalidDataException($"File {path} is not a valid Mach-O file.");
                    }
                }
 
                var svr = new SignatureVerificationResult(path, parent, virtualPath);
                svr.FullPath = path;
 
                svr.IsSigned = IsSigned(svr);
 
                svr.AddDetail(DetailKeys.File, SignCheckResources.DetailSigned, svr.IsSigned);
                return svr;
            }
            catch (Exception ex) when (ex is PlatformNotSupportedException || ex is InvalidDataException)
            {
                var svr = SignatureVerificationResult.UnsupportedFileTypeResult(path, parent, virtualPath);
                svr.AddDetail(DetailKeys.File, SignCheckResources.DetailSigned, SignCheckResources.NA);
                return svr;
            }
        }
 
        /// <summary>
        /// Verifies that the Mach-O file is signed.
        /// </summary>
        /// <param name="svr"></param>
        /// <returns></returns>
        private bool IsSigned(SignatureVerificationResult svr)
        {
            // Check if the file is signed properly using codesign
            (int signExitCode, string signOutput, string signError) = Utils.RunBashCommand($"codesign --verify --verbose {svr.FullPath}");
            string signedOutput = signOutput + signError;
 
            Regex validityRegex = new Regex(@"valid on disk");
            Regex requirementRegex = new Regex(@"satisfies its Designated Requirement");
            if (signExitCode != 0 || !validityRegex.IsMatch(signedOutput) || !requirementRegex.IsMatch(signedOutput))
            {
                return false;
            }
 
            // Check that one of the authorities is Microsoft
            (int authExitCode, string authOutput, string authError) = Utils.RunBashCommand($"codesign -dvvv {svr.FullPath}");
            string authorityOutput = authOutput + authError;
            Regex authorityRegex = new Regex(@"Authority=Developer ID Application: Microsoft Corporation");
            if (authExitCode != 0 || !authorityRegex.IsMatch(authorityOutput))
            {
                return false;
            }
 
            return ValidateAndAddTimestamps(svr);
        }
 
        /// <summary>
        /// Verifies the timestamps of a Mach-O file using codesign and OpenSSL.
        /// Adds the timestamp details to the SignatureVerificationResult.
        /// </summary>
        /// <param name="svr"></param>
        private bool ValidateAndAddTimestamps(SignatureVerificationResult svr)
        {
            DateTime signedOn = ExtractSignedOnTimestamp(svr);
            IEnumerable<(DateTime effectiveOn, DateTime expiresOn, string algorithm)> certDetails = ExtractCertificateDetails(svr);
 
            // A timestamp is not valid if it doesn't contain any certificates
            if (certDetails == null || !certDetails.Any())
            {
                var ts = new Timestamp()
                {
                    SignedOn = signedOn,
                    EffectiveDate = DateTime.MaxValue,
                    ExpiryDate = DateTime.MinValue,
                    SignatureAlgorithm = SignCheckResources.NA,
                };
 
                ts.AddToSignatureVerificationResult(svr);
                return false;
            }
 
            // Validate each certificate's timestamp
            foreach (var (effectiveOn, expiresOn, algorithm) in certDetails)
            {
                var ts = new Timestamp()
                {
                    SignedOn = signedOn,
                    EffectiveDate = effectiveOn,
                    ExpiryDate = expiresOn,
                    SignatureAlgorithm = algorithm,
                };
 
                ts.AddToSignatureVerificationResult(svr);
 
                if (!ts.IsValid)
                {
                    return false;
                }
            }
 
            return true;
        }
 
        /// <summary>
        /// Extracts the signed on timestamp from the Mach-O file using codesign.
        /// </summary>
        private DateTime ExtractSignedOnTimestamp(SignatureVerificationResult svr)
        {
            (_, string output, string error) = Utils.RunBashCommand($"codesign -dvv --verbose=4 {svr.FullPath}");
            string timestampOutput = output + error;
 
            Regex timestampRegex = new Regex(@"Timestamp=(?<timestamp>.*)");
            return timestampRegex.Match(timestampOutput).GroupValueOrDefault("timestamp").DateTimeOrDefault(DateTime.MaxValue);
        }
 
        /// <summary>
        /// Extracts the certificate details from the Mach-O file using codesign and OpenSSL.
        /// </summary>
        private IEnumerable<(DateTime effectiveOn, DateTime expiresOn, string algorithm)> ExtractCertificateDetails(SignatureVerificationResult svr)
        {
            string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
            Directory.CreateDirectory(tempDir);
            try
            {
                // Extract the certificates to a temporary directory
                Utils.RunBashCommand($"codesign -d --extract-certificates {svr.FullPath}", tempDir);
 
                foreach (string cert in Directory.GetFiles(tempDir, "codesign*"))
                {
                    string pemFileName = $"{cert}.pem";
 
                    // Convert the extracted certificates to PEM format
                    Utils.RunBashCommand($"openssl x509 -inform DER -in \"{cert}\" -out \"{pemFileName}\"", tempDir);
 
                    // Get the certificate details
                    (_, string output, string error) = Utils.RunBashCommand($"openssl x509 -in \"{pemFileName}\" -noout -text", tempDir);
                    string opensslOutput = output + error;
 
                    // Extract the certificate timestamps and algorithm
                    Regex effectiveOnRegex = new Regex(@"Not Before\s*:\s*(?<effectiveOn>.*)");
                    Regex expiresOnRegex = new Regex(@"Not After\s*:\s*(?<expiresOn>.*)");
                    Regex algorithmRegex = new Regex(@"Signature Algorithm:\s*(?<algorithm>.*)");
                    DateTime effectiveOn = effectiveOnRegex.Match(opensslOutput).GroupValueOrDefault("effectiveOn").DateTimeOrDefault(DateTime.MaxValue);
                    DateTime expiresOn = expiresOnRegex.Match(opensslOutput).GroupValueOrDefault("expiresOn").DateTimeOrDefault(DateTime.MinValue);
                    string algorithm = algorithmRegex.Match(opensslOutput).GroupValueOrDefault("algorithm") ?? SignCheckResources.NA;
 
                    yield return (effectiveOn, expiresOn, algorithm);
                }
            }
            finally
            {
                Directory.Delete(tempDir, true);
            }
        }
    }
}