Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: ObtainRFC3161Timestamp occasionally fails with a CryptographicException and HRESULT TRUST_E_TIME_STAMP #9505

Closed
e455a81e-d3ba-41a2-bc6d-7aafb1d9a5cd opened this issue Dec 7, 2023 · 1 comment

Comments

@e455a81e-d3ba-41a2-bc6d-7aafb1d9a5cd
Copy link

e455a81e-d3ba-41a2-bc6d-7aafb1d9a5cd commented Dec 7, 2023

Issue Description

Hi, I am using SecurityUtilities.SignFile to sign a ClickOnce manifest files but it occasionally fails with a System.Security.Cryptography.CryptographicException that stems from a call to ObtainRFC3161Timestamp.
I am directly calling the API using the Microsoft.Build.Tasks.Core nuget package.

Steps to Reproduce

This behaviour depends on the result of the random number generator in ObtainRFC3161Timestamp so the issues manifests on average once every couple of hundreds attempts. The following is a minimal example that reproduces the issue using code from SignedCmiManifest2.

void Main()
{

    for (int i = 1; i <= 1000; i++)
    {
        Console.Write(". ");

        try
        {
            ObtainRFC3161Timestamp(
                "http://timestamp.digicert.com",
                Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes("I'm a signature")),
                true);
        }
        catch (CryptographicException ex)
        {
            Console.WriteLine();
            Console.WriteLine($"i={i}, {ex.Message}, HResult: {ex.HResult:x}");
        }
    }

}

private static string ObtainRFC3161Timestamp(string timeStampUrl, string signatureValue, bool useSha256)
{
    byte[] sigValueBytes = Convert.FromBase64String(signatureValue);
    string timestamp = String.Empty;

    string algId = useSha256 ? Win32.szOID_NIST_sha256 : Win32.szOID_OIWSEC_sha1;

    unsafe
    {
        IntPtr ppTsContext = IntPtr.Zero;
        IntPtr ppTsSigner = IntPtr.Zero;
        IntPtr phStore = IntPtr.Zero;

        try
        {
            byte[] nonce = new byte[24];

            using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
            {
                rng.GetBytes(nonce);
            }

            Win32.CRYPT_TIMESTAMP_PARA para = new Win32.CRYPT_TIMESTAMP_PARA()
            {
                fRequestCerts = true,
                pszTSAPolicyId = IntPtr.Zero,
            };

            fixed (byte* pbNonce = nonce)
            {
                para.Nonce.cbData = (uint)nonce.Length;
                para.Nonce.pbData = (IntPtr)pbNonce;

                if (!Win32.CryptRetrieveTimeStamp(
                        timeStampUrl,
                        0,
                        60 * 1000, // 1 minute timeout
                        algId,
                        ref para,
                        sigValueBytes,
                        sigValueBytes.Length,
                        ref ppTsContext,
                        ref ppTsSigner,
                        ref phStore))
                {
                    throw new CryptographicException(Marshal.GetLastWin32Error());
                }
            }

            var timestampContext = (Win32.CRYPT_TIMESTAMP_CONTEXT)Marshal.PtrToStructure(ppTsContext, typeof(Win32.CRYPT_TIMESTAMP_CONTEXT));
            byte[] encodedBytes = new byte[(int)timestampContext.cbEncoded];
            Marshal.Copy(timestampContext.pbEncoded, encodedBytes, 0, (int)timestampContext.cbEncoded);
            timestamp = Convert.ToBase64String(encodedBytes);
        }
        finally
        {
            if (ppTsContext != IntPtr.Zero)
                Win32.CryptMemFree(ppTsContext);

            if (ppTsSigner != IntPtr.Zero)
                Win32.CertFreeCertificateContext(ppTsSigner);

            if (phStore != IntPtr.Zero)
                Win32.CertCloseStore(phStore, 0);
        }
    }

    return timestamp;
}

internal static class Win32
{
    //
    // PInvoke dll's.
    //
    internal const String CRYPT32 = "crypt32.dll";
    internal const String KERNEL32 = "kernel32.dll";

    //
    // Structures.
    //
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    internal struct CRYPT_DATA_BLOB
    {
        internal uint cbData;
        internal IntPtr pbData;
    }
    //
    [DllImport(KERNEL32, CharSet = CharSet.Auto, SetLastError = true)]
    internal extern static
        IntPtr GetProcessHeap();

    [DllImport(KERNEL32, CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal extern static
        bool HeapFree(
        [In] IntPtr hHeap,
        [In] uint dwFlags,
        [In] IntPtr lpMem);

    // hash algorithm OIDs
    internal const string szOID_OIWSEC_sha1 = "1.3.14.3.2.26";
    internal const string szOID_NIST_sha256 = "2.16.840.1.101.3.4.2.1";

    [StructLayout(LayoutKind.Sequential)]
    internal struct CRYPT_TIMESTAMP_CONTEXT
    {
        internal uint cbEncoded; // DWORD->unsigned int
        internal IntPtr pbEncoded; // BYTE*
        internal IntPtr pTimeStamp; // PCRYPT_TIMESTAMP_INFO->_CRYPT_TIMESTAMP_INFO*
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct CRYPTOAPI_BLOB
    {
        internal uint cbData;
        internal IntPtr pbData;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct CRYPT_TIMESTAMP_PARA
    {
        internal IntPtr pszTSAPolicyId;
        internal bool fRequestCerts;
        internal CRYPTOAPI_BLOB Nonce;
        internal int cExtension;
        internal IntPtr rgExtension;
    }

    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    [DllImport(CRYPT32, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal extern static
        bool CryptRetrieveTimeStamp(
        [In][MarshalAs(UnmanagedType.LPWStr)] string wszUrl,
        [In] uint dwRetrievalFlags,
        [In] int dwTimeout,
        [In][MarshalAs(UnmanagedType.LPStr)] string pszHashId,
        [In, Out] ref CRYPT_TIMESTAMP_PARA pPara,
        [In] byte[] pbData,
        [In] int cbData,
        [In, Out] ref IntPtr ppTsContext,
        [In, Out] ref IntPtr ppTsSigner,
        [In, Out] ref IntPtr phStore);

    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    [DllImport(CRYPT32, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
    internal static extern bool CertFreeCertificateContext(IntPtr pCertContext);

    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    [DllImport(CRYPT32, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
    internal static extern bool CertCloseStore(IntPtr pCertContext, int dwFlags);

    [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
    [DllImport(CRYPT32, CallingConvention = CallingConvention.Winapi)]
    internal static extern void CryptMemFree(IntPtr pv);
}

Expected Behavior

Signing the ClickOnce manifest file always succeeds.

Actual Behavior

Signing fails occasionally with a CryptographicException that has a HResult of TRUST_E_TIME_STAMP.

Analysis

The problem is that the Win32.CryptRetrieveTimeStamp() call within ObtainRFC3161Timestamp() sometimes returns a TRUST_E_TIME_STAMP result. The reason is because the generated nonce in ObtainRFC3161Timestamp() - just a evenly distributed random 192 bit number - is in an unexpected range.
When using the default ObtainRFC3161Timestamp() implementation of the 1000 calls about two TRUST_E_TIME_STAMP errors occur.
To test this can be made worse by doing nonce[^1] = 0xFF in ObtainRFC3161Timestamp() which lets 50% of calls fail.

The team responsible for nuget signing seems to have encountered a similiar issue and implemented a fix.
When implementing their fix nonce[^1] &= 0x7f; nonce[^1] |= 0x01; 100% of calls succeed.

Versions & Configurations

MSBuild version 17.7.2+d6990bcfa for .NET Framework
17.7.2.37605
dotnet --version
7.0.401

@e455a81e-d3ba-41a2-bc6d-7aafb1d9a5cd e455a81e-d3ba-41a2-bc6d-7aafb1d9a5cd added bug needs-triage Have yet to determine what bucket this goes in. labels Dec 7, 2023
@AR-May AR-May added backlog and removed needs-triage Have yet to determine what bucket this goes in. labels Dec 12, 2023
@AR-May AR-May added the triaged label Feb 7, 2024
@AR-May
Copy link
Member

AR-May commented Jul 31, 2024

The issue should be fixed by #9579. Please feel free to re-open the issue otherwise.

@AR-May AR-May closed this as completed Jul 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants