Skip to content

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented Oct 11, 2025

  • Explore the repository and understand the issue
  • Identify the managed SHA-1 implementation in Sha1ForNonSecretPurposes.cs
  • Review BCrypt interop infrastructure and usage patterns
  • Split into separate Windows and Unix files per @stephentoub feedback
  • Create Sha1ForNonSecretPurposes.Windows.cs with BCrypt implementation
  • Create Sha1ForNonSecretPurposes.Unix.cs with managed implementation
  • Update project files to conditionally include appropriate versions
  • Address @jkotas review feedback
    • Removed Unix file from System.Security.Cryptography.csproj
    • Moved Windows file to alphabetical position in System.Private.CoreLib
  • Delete original Sha1ForNonSecretPurposes.cs file per @stephentoub feedback
  • Simplify to one-shot HashData API per @jkotas feedback
    • Added static HashData method to both implementations
    • Updated CoreLib usages to use one-shot API
    • Removed incremental hashing from Windows implementation
    • Removed unnecessary BCrypt interop files and SafeHandles
  • Fix Browser build by adding Unix file to System.Security.Cryptography.csproj
  • Optimize GenerateGuidFromName per @stephentoub feedback
    • Use GetByteCount to compute length upfront
    • Encode directly into destination buffer slice
    • Eliminated extra allocation and copy
  • Build and test the changes on Linux
  • Verify all existing tests pass

Summary

Per reviewer feedback, implemented one-shot hashing API for better performance:

  • Sha1ForNonSecretPurposes.Windows.cs - Uses BCrypt one-shot hash API (BCryptHash) with pseudo-handle
  • Sha1ForNonSecretPurposes.Unix.cs - Managed implementation with static HashData method

Both implementations expose a static HashData(ReadOnlySpan<byte>, Span<byte>) method. The Windows implementation uses the BCrypt one-shot API for optimal performance on small inputs (typical usage is ~100s of bytes). The Unix implementation wraps the existing incremental implementation and is also used for Browser builds.

The EventSource GUID generation has been optimized to reduce allocations by encoding directly into the destination buffer.

Original prompt

This section details on the original issue you should resolve

<issue_title>Consider ifdefing away Sha1ForNonSecretPurposes on Windows</issue_title>
<issue_description>We have a managed implementation of SHA-1 hanging around for use in EventSource and related APIs. We should consider ifdefing it away and relying on the OS's underlying implementation on runtimes where we know an OS implementation exists. This can help compliance, as getting an exemption for SHA-1 for compat purposes is far easier than getting an exemption for carrying our own implementation. There are also some small perf wins.

Benchmark code below the fold

public class Sha1Runner
{
    private byte[] _input;
    private byte[] _digest = new byte[20];

    [Params(0, 8, 12, 24, 32, 64, 128, 256)]
    public int InputSizeInBytes { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        _input = new byte[InputSizeInBytes];
        RandomNumberGenerator.Fill(_input);
    }

    [Benchmark(Baseline = true)]
    public byte[] UseManaged()
    {
        Sha1ForNonSecretPurposes sha1 = default;
        sha1.Start();
        sha1.Append(_input);
        sha1.Finish(_digest);
        return _digest;
    }

    [Benchmark(Baseline = false)]
    public byte[] UseBCrypt()
    {
        SHA1.HashData(_input, _digest);
        return _digest;
    }

    private struct Sha1ForNonSecretPurposes
    {
        private long length; // Total message length in bits
        private uint[] w; // Workspace
        private int pos; // Length of current chunk in bytes

        /// <summary>
        /// Call Start() to initialize the hash object.
        /// </summary>
        [SkipLocalsInit]
        public void Start()
        {
            this.w ??= new uint[85];

            this.length = 0;
            this.pos = 0;
            this.w[80] = 0x67452301;
            this.w[81] = 0xEFCDAB89;
            this.w[82] = 0x98BADCFE;
            this.w[83] = 0x10325476;
            this.w[84] = 0xC3D2E1F0;
        }

        /// <summary>
        /// Adds an input byte to the hash.
        /// </summary>
        /// <param name="input">Data to include in the hash.</param>
        [SkipLocalsInit]
        public void Append(byte input)
        {
            this.w[this.pos / 4] = (this.w[this.pos / 4] << 8) | input;
            if (64 == ++this.pos)
            {
                this.Drain();
            }
        }

        /// <summary>
        /// Adds input bytes to the hash.
        /// </summary>
        /// <param name="input">
        /// Data to include in the hash. Must not be null.
        /// </param>
        [SkipLocalsInit]
#if ES_BUILD_STANDALONE
        public void Append(byte[] input)
#else
        public void Append(ReadOnlySpan<byte> input)
#endif
        {
            foreach (byte b in input)
            {
                this.Append(b);
            }
        }

        /// <summary>
        /// Retrieves the hash value.
        /// Note that after calling this function, the hash object should
        /// be considered uninitialized. Subsequent calls to Append or
        /// Finish will produce useless results. Call Start() to
        /// reinitialize.
        /// </summary>
        /// <param name="output">
        /// Buffer to receive the hash value. Must not be null.
        /// Up to 20 bytes of hash will be written to the output buffer.
        /// If the buffer is smaller than 20 bytes, the remaining hash
        /// bytes will be lost. If the buffer is larger than 20 bytes, the
        /// rest of the buffer is left unmodified.
        /// </param>
        [SkipLocalsInit]
        public void Finish(byte[] output)
        {
            long l = this.length + 8 * this.pos;
            this.Append(0x80);
            while (this.pos != 56)
            {
                this.Append(0x00);
            }

            unchecked
            {
                this.Append((byte)(l >> 56));
                this.Append((byte)(l >> 48));
                this.Append((byte)(l >> 40));
                this.Append((byte)(l >> 32));
                this.Append((byte)(l >> 24));
                this.Append((byte)(l >> 16));
                this.Append((byte)(l >> 8));
                this.Append((byte)l);

                int end = output.Length < 20 ? output.Length : 20;
                for (int i = 0; i != end; i++)
                {
                    uint temp = this.w[80 + i / 4];
                    output[i] = (byte)(temp >> 24);
                    this.w[80 + i / 4] = temp << 8;
                }
            }
        }

        /// <summary>
        /// Called when this.pos reach...

</details>

Fixes dotnet/runtime#45237

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

@Copilot Copilot AI changed the title [WIP] Consider ifdefing away Sha1ForNonSecretPurposes on Windows Use Windows BCrypt for Sha1ForNonSecretPurposes instead of managed implementation Oct 11, 2025
@Copilot Copilot AI requested a review from stephentoub October 11, 2025 18:18
Copilot finished work on behalf of stephentoub October 11, 2025 18:18
@stephentoub stephentoub marked this pull request as ready for review October 11, 2025 20:23
@Copilot Copilot AI review requested due to automatic review settings October 11, 2025 20:23
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR replaces the managed SHA-1 implementation in Sha1ForNonSecretPurposes with Windows BCrypt API calls on Windows platforms while preserving the managed implementation for non-Windows platforms. The change is motivated by compliance requirements (easier security exemptions for OS-provided implementations), performance improvements (20-58% faster), and reduced binary size on Windows.

Key changes:

  • Added conditional compilation to use BCrypt on Windows (TARGET_WINDOWS) and managed implementation elsewhere
  • Windows implementation buffers input data and uses BCrypt's one-shot hash API in Finish()
  • Added required BCrypt interop declarations to the project file

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
System.Private.CoreLib.Shared.projitems Adds BCrypt interop file references for Windows builds
Sha1ForNonSecretPurposes.cs Implements Windows-specific BCrypt implementation with conditional compilation directives

Copilot finished work on behalf of stephentoub October 11, 2025 20:53
@Copilot Copilot AI requested a review from stephentoub October 11, 2025 20:53
Copilot finished work on behalf of jkotas October 11, 2025 22:03
@Copilot Copilot AI requested a review from jkotas October 11, 2025 22:03
@jkotas jkotas added the tenet-performance Performance related issue label Oct 11, 2025
@stephentoub
Copy link
Member

@copilot, what happened to the original runtime/src/libraries/Common/src/System/Sha1ForNonSecretPurposes.cs file? I no longer see that being modified or deleted. Shouldn't it have just been renamed with the .Unix.cs suffix, rather than duplicating it and leaving the original?

const uint k = 0x5A827999;
uint f = (b & c) | ((~b) & d);
uint temp = BitOperations.RotateLeft(a, 5) + f + e + k + _w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp;
uint temp = BitOperations.RotateLeft(a, 5) + f + e + k + w[i]; e = d; d = c; c = BitOperations.RotateLeft(b, 30); b = a; a = temp;
Copy link
Member

@jkotas jkotas Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.NET Framewwork managed implementation of SHA1 hash had these loops manually unrolled: https://github.com/microsoft/referencesource/blob/a48449cb48a9a693903668a71449ac719b76867c/mscorlib/system/security/cryptography/sha1managed.cs#L227-L231 . Doing so gets the performance close to bcrypt, but I do not think the extra code bloat is worth it for Sha1ForNonSecretPurposes

@jkotas
Copy link
Member

jkotas commented Oct 13, 2025

@EgorBot -intel

using System.Reflection;
using BenchmarkDotNet.Attributes;

public class Bench
{
    static AssemblyName an = typeof(object).Assembly.GetName();

    [Benchmark]
    public byte[] GetPublicKeyToken() => ((AssemblyName)an.Clone()).GetPublicKeyToken();
}

@jkotas
Copy link
Member

jkotas commented Oct 13, 2025

According to MichalStrehovsky/rt-sz#191, this is introducing 5% size regression in minimal NAOT app on Windows. I guess this introduced unconditional dependency on more interop infrastructure.

@jkotas jkotas added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Oct 13, 2025
@jkotas
Copy link
Member

jkotas commented Oct 13, 2025

@EgorBot -windows_x64

using System.Reflection;
using BenchmarkDotNet.Attributes;

public class Bench
{
    static AssemblyName an = typeof(object).Assembly.GetName();

    [Benchmark]
    public byte[] GetPublicKeyToken() => ((AssemblyName)an.Clone()).GetPublicKeyToken();
}

#
# Windows 8+
#
bcrypt!BCryptHash
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my edification, what does this do?

Copy link
Member

@jkotas jkotas Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file lists Windows APIs that are expected to be available on all Windows OS that we support. APIs in this file are opted into https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/interop#direct-pinvoke-calls . NAOT binary can have hard dependency on these APIs, and it will fail to launch if the API is missing.

APIs not on the list are imported lazily using LoadLibrary/GetProcAddress when called for the first time. EntryPointNotFoundException is thrown when the API is not available. (This is the default behavior for runtime with the JIT.)

BCryptHash is available on Windows 10+ only, thus it was missing from this list. We plan to raise the minimum supported version to Windows 10 in .NET 11, and so it should be ok to add it. This should fix the binary size regression. The binary size regression was introduced by BCryptHash introducing dependency on the lazy loading infrastructure.

@jkotas
Copy link
Member

jkotas commented Oct 13, 2025

Adding BCryptHash to WindowsAPIs.txt does not seem to be enough to fix the size regression. This will need more investigation.

In the meantime, I have split the platform neutral refactoring and optimization to #120674 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Runtime NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) tenet-performance Performance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants