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 AI and others added 2 commits October 11, 2025 18:05
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
@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

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
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
…dows file alphabetically

Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
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?


input.CopyTo(_buffer.AsSpan(_bufferPos));
_bufferPos += input.Length;
}
Copy link
Member

Choose a reason for hiding this comment

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

@copilot, this implementation looks problematic from a performance perspective. It's buffering up all input and then only hashing it when done. In contrast, the existing implementation was streaming; it didn't need to buffer the whole input, it just kept updating the hash. Is there no way to do the same incremental hashing with BCrypt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 4bcdc5f. Changed to use BCrypt's streaming hash APIs (BCryptCreateHash, BCryptHashData, BCryptFinishHash) instead of buffering all input. The implementation now maintains a SafeBCryptHashHandle and incrementally hashes data as it's appended, matching the performance characteristics of the original managed implementation.

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.

Outside browser, Sha1ForNonSecretPurposes is used for very small amount of data (100s bytes). The savings were demonstrated on one-shot bcrypt hash and they are <1 microsecond. If we switch to incremental hashing, I do not expect that we will see any savings anymore (more like regressions).

  • Sha1ForNonSecretPurposes should expose new one-shot static hashing method
  • The two uses of Sha1ForNonSecretPurposes in CoreLib should be switched to use this one-shot static hashing method
  • BCrypt-based implementation of Sha1ForNonSecretPurposes should expose the one-shot static hashing method only. It should not expose the incremental hashing at all.

@copilot Could you please implement these changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in commit 7e52c0b. Implemented the requested changes:

  • Added static HashData(ReadOnlySpan<byte>, Span<byte>) method to both implementations
  • Updated the two CoreLib usages (EventSource and AssemblyNameHelpers) to use the one-shot API
  • Windows implementation now only exposes the one-shot static method using BCryptHash with pseudo-handle
  • Removed incremental hashing support, BCrypt streaming APIs, and SafeHandle types from Windows implementation

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 11, 2025

@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 wi...

You're right - the original file should have been deleted. Fixed in commit 8f9c17a. The original Sha1ForNonSecretPurposes.cs has been removed, leaving only the platform-specific .Windows.cs and .Unix.cs versions.

@Copilot Copilot AI requested a review from stephentoub October 11, 2025 22:40
Copilot finished work on behalf of stephentoub October 11, 2025 22:40
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
@stephentoub
Copy link
Member

@copilot, there are build failures:

D:\a\_work\1\s\src\libraries\Common\src\Interop\Windows\BCrypt\Interop.BCryptCreateHash.cs(15,85): error SYSLIB1051: The type 'SafeBCryptAlgorithmHandle' is not supported by source-generated P/Invokes. The generated source will not handle marshalling of parameter 'hAlgorithm'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1051) [D:\a\_work\1\s\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj]
D:\a\_work\1\s\src\libraries\Common\src\Interop\Windows\BCrypt\Interop.BCryptCreateHash.cs(15,59): error CS0246: The type or namespace name 'SafeBCryptAlgorithmHandle' could not be found (are you missing a using directive or an assembly reference?) [D:\a\_work\1\s\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj]
D:\a\_work\1\s\artifacts\obj\coreclr\System.Private.CoreLib\windows.x64.Release\generated\Microsoft.Interop.LibraryImportGenerator\Microsoft.Interop.LibraryImportGenerator\LibraryImports.g.cs(7370,82): error CS0246: The type or namespace name 'SafeBCryptAlgorithmHandle' could not be found (are you missing a using directive or an assembly reference?) [D:\a\_work\1\s\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj]

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 12, 2025

@copilot, there are build failures:

D:\a\_work\1\s\src\libraries\Common\src\Interop\Windows\BCrypt\Interop.BCryptCreateHash.cs(15,85): error SYSLIB1051: The type 'SafeBCryptAlgorithmHandle' is n...

Fixed in commit e8f0e1b. Added SafeBCryptAlgorithmHandle to System.Private.CoreLib.Shared.projitems. The BCryptCreateHash interop file has two overloads - one uses SafeBCryptAlgorithmHandle (for regular algorithm handles) and one uses nuint (for pseudo-handles). Even though we only use the pseudo-handle version, the first overload still needs SafeBCryptAlgorithmHandle to be available for compilation.

Copilot finished work on behalf of stephentoub October 12, 2025 02:59
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
@stephentoub
Copy link
Member

@copilot, there are build failures:


Build FAILED.

/__w/1/s/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Managed.cs(116,21): error CS0246: The type or namespace name 'Sha1ForNonSecretPurposes' could not be found (are you missing a using directive or an assembly reference?) [/__w/1/s/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj::TargetFramework=net10.0-browser]
    0 Warning(s)
    1 Error(s)

Span<byte> source = totalLength <= 256 ? stackalloc byte[totalLength] : new byte[totalLength];

namespaceBytes.CopyTo(source);
nameBytes.CopyTo(source.Slice(namespaceBytes.Length));
Copy link
Member

Choose a reason for hiding this comment

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

@copilot, currently this always allocates a byte[] for the name, and then copies that again into another buffer, which itself might be stack or heap allocated. You could instead compute the output length of the encoding with GetByteCount, then get the single buffer to use to store the whole output, either stack or heap allocated, copying in the namespace bytes and encoding directly into the relevant sliced portion of the destination. This will cost the extra length computation, but still save the extra allocation and copy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in commit df3a9c3. Changed to use GetByteCount to compute the length upfront, then encode directly into the destination buffer slice. This eliminates the extra nameBytes array allocation and copy while trading off an additional length computation.

Copy link
Contributor Author

Copilot AI commented Oct 12, 2025

@copilot, there are build failures:


...

Fixed in commit fc5be0a. Added Sha1ForNonSecretPurposes.Unix.cs to the Browser ItemGroup in System.Security.Cryptography.csproj. The Browser build uses SHAHashProvider.Browser.Managed.cs which needs the Unix implementation with its incremental hashing support (Start/Append/Finish).

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
@Copilot Copilot AI requested a review from stephentoub October 12, 2025 12:14
Copilot finished work on behalf of stephentoub October 12, 2025 12:14
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copilot finished work on behalf of stephentoub October 12, 2025 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants