File: Verification\PgpVerifier.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.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Microsoft.SignCheck.Logging;
 
namespace Microsoft.SignCheck.Verification
{
    public abstract class PgpVerifier : ArchiveVerifier
    {
        protected PgpVerifier(Log log, Exclusions exclusions, SignatureVerificationOptions options, string fileExtension)
            : base(log, exclusions, options, fileExtension)
        {
        }
 
        public override SignatureVerificationResult VerifySignature(string path, string parent, string virtualPath)
            => VerifySupportedFileType(path, parent, virtualPath);
 
        /// <summary>
        /// Returns the paths to the signature document and the signable content.
        /// Used to verify the signature of the package using gpg.
        /// </summary>
        protected abstract (string signatureDocument, string signableContent) GetSignatureDocumentAndSignableContent(string path, string tempDir);
 
        /// <summary>
        /// Verifies the signature of a file using a detached .sig file.
        /// If the .sig file exists, verifies as a supported file type; otherwise, as unsupported.
        /// </summary>
        protected SignatureVerificationResult VerifyDetachedSignature(string path, string parent, string virtualPath)
        {
            if (File.Exists(path + ".sig"))
            {
                return VerifySupportedFileType(path, parent, virtualPath);
            }
            return VerifyUnsupportedFileType(path, parent, virtualPath);
        }
 
        /// <summary>
        /// Returns the paths to the detached signature document and the signable content.
        /// For use by verifiers whose signatures are stored in a separate .sig file.
        /// </summary>
        protected static (string signatureDocument, string signableContent) GetDetachedSignatureDocumentAndSignableContent(string path, string tempDir)
        {
            string signature = $"{path}.sig";
            string signatureDocument = Path.Combine(tempDir, Path.GetFileName(signature));
            File.Copy(signature, signatureDocument, overwrite: true);
 
            return (signatureDocument, path);
        }
 
        protected override bool IsSigned(string path, SignatureVerificationResult svr)
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                throw new PlatformNotSupportedException("Pgp verification is not supported on Windows.");
            }
 
            string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
            Directory.CreateDirectory(tempDir);
 
            // https://microsoft.sharepoint.com/teams/prss/esrp/info/SitePages/Linux%20GPG%20Signing.aspx
            try
            {
                Utils.DownloadAndConfigurePublicKeys(tempDir);
 
                (string signatureDocument, string signableContent) = GetSignatureDocumentAndSignableContent(path, tempDir);
 
                if (string.IsNullOrEmpty(signatureDocument) || string.IsNullOrEmpty(signableContent))
                {
                    return false;
                }
 
                (int exitCode, string output, string error) = Utils.RunBashCommand($"gpg --verify --status-fd 1 {signatureDocument} {signableContent}");
                string verificationOutput = output + error;
 
                if (!verificationOutput.Contains("Good signature"))
                {
                    if (exitCode != 0 && !verificationOutput.Contains("no signature found"))
                    {
                        // Log an error if something other than a missing
                        // signature caused the verification to fail
                        svr.AddDetail(DetailKeys.Error, error);
                    }
                    return false;
                }
 
                Timestamp ts = GetTimestamp(verificationOutput);
                ts.AddToSignatureVerificationResult(svr);
                return ts.IsValid;
            }
            finally
            {
                Directory.Delete(tempDir, true);
            }
        }
 
        /// <summary>
        /// Get the timestamp of the signature in the package.
        /// </summary>
        private Timestamp GetTimestamp(string verificationOutput)
        {
            Regex signatureTimestampsRegex = new Regex(@"VALIDSIG .+ \d+-\d+-\d+ (?<signedOn>\d+) (?<expiresOn>\d+) ");
            Match signatureTimestampsMatch = signatureTimestampsRegex.Match(verificationOutput);
 
            Regex signatureKeyInfoRegex = new Regex(@"using (?<algorithm>.+) key (?<keyId>.+)");
            Match signatureKeyInfoMatch = signatureKeyInfoRegex.Match(verificationOutput);
 
            string keyId = signatureKeyInfoMatch.GroupValueOrDefault("keyId");
            (_, string keyInfo, _) = Utils.RunBashCommand($"gpg --list-keys --with-colons {keyId} | grep '^pub:'");
            Regex keyInfoRegex = new Regex(@$"pub.+{keyId}:(?<createdOn>\d+):");
            Match keyInfoMatch = keyInfoRegex.Match(keyInfo);
 
            return new Timestamp()
            {
                SignedOn = signatureTimestampsMatch.GroupValueOrDefault("signedOn").DateTimeOrDefault(DateTime.MaxValue),
                ExpiryDate = signatureTimestampsMatch.GroupValueOrDefault("expiresOn").DateTimeOrDefault(DateTime.MaxValue),
                SignatureAlgorithm = signatureKeyInfoMatch.GroupValueOrDefault("algorithm"),
                EffectiveDate = keyInfoMatch.GroupValueOrDefault("createdOn").DateTimeOrDefault(DateTime.MaxValue)
            };
        }
    }
}