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

[browser][wasm] API proposal: Hashing and Hashed Message Authentication #43939

Closed
kjpou1 opened this issue Oct 28, 2020 · 14 comments
Closed

[browser][wasm] API proposal: Hashing and Hashed Message Authentication #43939

kjpou1 opened this issue Oct 28, 2020 · 14 comments
Assignees
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Security
Milestone

Comments

@kjpou1
Copy link
Contributor

kjpou1 commented Oct 28, 2020

Background and Motivation

Define an async API surface to handle the async nature of the Web Crypto.

The current API surface only defines support for synchronous platform integration.

Original background issue #40074

Relevant documentation can be found at https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto.

Note: MD5 is not supported by the browser API so even though it may be referenced below it will continue to throw a PNSE on browser.

Proposed API

Secure Hash Algorithm (SHA) Family:

SHA-1
SHA-256
SHA-384
SHA-512

The purpose is to supply some input data, run it through the hashing function, and get a hash code back using an Async API surface instead of the synchronous method calls.

namespace System.Security.Cryptography
{
    public abstract partial class HashAlgorithm : System.IDisposable, System.Security.Cryptography.ICryptoTransform
    {
        public System.Threading.Tasks.Task<byte[]> ComputeHashAsync(byte[] buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
        public System.Threading.Tasks.Task<byte[]> ComputeHashAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
        protected abstract System.Threading.Tasks.Task HashCoreAsync(byte[] array, int ibStart, int cbSize, System.Threading.CancellationToken cancellationToken);
        protected abstract System.Threading.Tasks.Task<byte[]> HashFinalAsync(System.Threading.CancellationToken cancellationToken);
    }
}


    public abstract partial class SHA1 : System.Security.Cryptography.HashAlgorithm
    {
        public static System.Threading.Tasks.Task<byte[]> HashDataAsync(byte[] source, System.Threading.CancellationToken cancellationToken = default) { throw null; }
        public static System.Threading.Tasks.Task<int> HashDataAsync(byte[] source, byte[] destination, System.Threading.CancellationToken cancellationToken = default) { throw null; }
        public static System.Threading.Tasks.Task<(bool IsSuccess, int BytesWritten)> TryHashDataAsync(byte[] source, byte[] destination, System.Threading.CancellationToken cancellationToken = default) { throw null; }
    }
 
   [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
    public sealed partial class SHA1Managed : System.Security.Cryptography.SHA1
    {
        protected sealed override System.Threading.Tasks.Task HashCoreAsync(byte[] array, int ibStart, int cbSize, System.Threading.CancellationToken cancellationToken) { throw null; }
        protected sealed override System.Threading.Tasks.Task<byte[]> HashFinalAsync(System.Threading.CancellationToken cancellationToken) { throw null; }

    }
    public abstract partial class SHA256 : System.Security.Cryptography.HashAlgorithm
    {
        public static System.Threading.Tasks.Task<byte[]> HashDataAsync(byte[] source, System.Threading.CancellationToken cancellationToken = default) { throw null; }
        public static System.Threading.Tasks.Task<int> HashDataAsync(byte[] source, byte[] destination, System.Threading.CancellationToken cancellationToken = default) { throw null; }
        public static System.Threading.Tasks.Task<(bool IsSuccess, int BytesWritten)> TryHashDataAsync(byte[] source, byte[] destination, System.Threading.CancellationToken cancellationToken = default) { throw null; }
    }
    [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
    public sealed partial class SHA256Managed : System.Security.Cryptography.SHA256
    {
        protected sealed override System.Threading.Tasks.Task HashCoreAsync(byte[] array, int ibStart, int cbSize, System.Threading.CancellationToken cancellationToken) { throw null; }
        protected sealed override System.Threading.Tasks.Task<byte[]> HashFinalAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
    }
    public abstract partial class SHA384 : System.Security.Cryptography.HashAlgorithm
    {
        public static System.Threading.Tasks.Task<byte[]> HashDataAsync(byte[] source, System.Threading.CancellationToken cancellationToken = default) { throw null; }
        public static System.Threading.Tasks.Task<int> HashDataAsync(byte[] source, byte[] destination, System.Threading.CancellationToken cancellationToken = default) { throw null; }
        public static System.Threading.Tasks.Task<(bool IsSuccess, int BytesWritten)> TryHashDataAsync(byte[] source, byte[] destination, System.Threading.CancellationToken cancellationToken = default) { throw null; }

    }
    [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
    public sealed partial class SHA384Managed : System.Security.Cryptography.SHA384
    {
        protected sealed override System.Threading.Tasks.Task HashCoreAsync(byte[] array, int ibStart, int cbSize, System.Threading.CancellationToken cancellationToken) { throw null; }
        protected sealed override System.Threading.Tasks.Task<byte[]> HashFinalAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
    }
    public abstract partial class SHA512 : System.Security.Cryptography.HashAlgorithm
    {
        public static System.Threading.Tasks.Task<byte[]> HashDataAsync(byte[] source, System.Threading.CancellationToken cancellationToken = default) { throw null; }
        public static System.Threading.Tasks.Task<int> HashDataAsync(byte[] source, byte[] destination, System.Threading.CancellationToken cancellationToken = default) { throw null; }
        public static System.Threading.Tasks.Task<(bool IsSuccess, int BytesWritten)> TryHashDataAsync(byte[] source, byte[] destination, System.Threading.CancellationToken cancellationToken = default) { throw null; }

    }
    [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
    public sealed partial class SHA512Managed : System.Security.Cryptography.SHA512
    {
        protected sealed override System.Threading.Tasks.Task HashCoreAsync(byte[] array, int ibStart, int cbSize, System.Threading.CancellationToken cancellationToken) { throw null; }
        protected sealed override System.Threading.Tasks.Task<byte[]> HashFinalAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
    }

Authenticated Hashing (HMAC)

    public partial class HMACMD5 : System.Security.Cryptography.HMAC
    {
        protected override System.Threading.Tasks.Task HashCoreAsync(byte[] rgb, int ib, int cb, System.Threading.CancellationToken cancellationToken) { throw null; }
        protected override System.Threading.Tasks.Task<byte[]> HashFinalAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
    }
    public partial class HMACSHA1 : System.Security.Cryptography.HMAC
    {
        protected override System.Threading.Tasks.Task HashCoreAsync(byte[] rgb, int ib, int cb, System.Threading.CancellationToken cancellationToken) { throw null; }
        protected override System.Threading.Tasks.Task<byte[]> HashFinalAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
    }
    public partial class HMACSHA256 : System.Security.Cryptography.HMAC
    {
        protected override System.Threading.Tasks.Task HashCoreAsync(byte[] rgb, int ib, int cb, System.Threading.CancellationToken cancellationToken) { throw null; }
        protected override System.Threading.Tasks.Task<byte[]> HashFinalAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
    }
    public partial class HMACSHA384 : System.Security.Cryptography.HMAC
    {
        protected override System.Threading.Tasks.Task HashCoreAsync(byte[] rgb, int ib, int cb, System.Threading.CancellationToken cancellationToken) { throw null; }
        protected override System.Threading.Tasks.Task<byte[]> HashFinalAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
    }
    public partial class HMACSHA512 : System.Security.Cryptography.HMAC
    {
        protected override System.Threading.Tasks.Task HashCoreAsync(byte[] rgb, int ib, int cb, System.Threading.CancellationToken cancellationToken) { throw null; }
        protected override System.Threading.Tasks.Task<byte[]> HashFinalAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
    }

Usage Examples

HashAlgorithm

            public static Task<byte[]> ComputeHashSha256Async(byte[] toBeHashed)
            {
                using (var sha256 = SHA256.Create())
                {
                    return sha256.ComputeHashAsync(toBeHashed, CancellationToken.None);
                }
            }


            var sha256HashedMessage = await ComputeHashSha256Async(
                Encoding.UTF8.GetBytes(originalMessage)).ConfigureAwait(false);

Async HashData usage

            sha1HashedMessage = await SHA1.HashDataAsync(Encoding.UTF8.GetBytes(originalMessage)).ConfigureAwait(false);
            Console.WriteLine("Message 1 HashDataAsync = " +
                        Convert.ToBase64String(sha1HashedMessage));

            byte[] destination = new byte[20];
            int written = await SHA1.HashDataAsync(Encoding.UTF8.GetBytes(originalMessage), destination).ConfigureAwait(false);
            sha1HashedMessage = destination;
            Console.WriteLine($"Message 1 HashDataAsync {written} with destination = " +
                        Convert.ToBase64String(sha1HashedMessage, 0, written));

Authentication Hashing

            public static Task<byte[]> ComputeHmacsha1Async(byte[] toBeHashed, byte[] key, CancellationToken cancellationToken = default)
            {
                using (var hmac = new HMACSHA1(key))
                {
                    return hmac.ComputeHashAsync(toBeHashed, cancellationToken);
                }
            }

            var hmacSHA1Message = await Hmac.ComputeHmacsha1Async(
            Encoding.UTF8.GetBytes(originalMessage), key).ConfigureAwait(false);


            public static Task<byte[]> ComputeHmacsha256Async(byte[] toBeHashed, byte[] key, CancellationToken cancellationToken = default)
            {
                using (var hmac = new HMACSHA256(key))
                {
                    return hmac.ComputeHashAsync(toBeHashed, 5, 8, cancellationToken);
                }
            }


           var hmacSHA256Message = await ComputeHmacsha256Async(
            Encoding.UTF8.GetBytes(originalMessage), key).ConfigureAwait(false);

Alternative Designs

Other options would be shipping OpenSSL for Browser which should be avoided. Shipping OpenSSL for Browser presents a whole other set of problem areas. See original issue for more detailed information : #40074

Risks

Risks are a change to the existing library implementations to include the async api calls for browser support.

The browser API can not support synchronous calls so will throw PNSE.
The non browser implementations will not at the beginning define implementations for the asynchronous calls so will throw PNSE.

Synchronous Hashing functions for browser will now throw PNSE but one alternative would be to leave the managed implementation of the sync call in as well. This has been externally commented on and should be taken into account here as well to mitigate API Risks.

@kjpou1 kjpou1 added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Net.Security labels Oct 28, 2020
@kjpou1 kjpou1 self-assigned this Oct 28, 2020
@ghost
Copy link

ghost commented Oct 28, 2020

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@ghost
Copy link

ghost commented Oct 28, 2020

Tagging subscribers to this area: @bartonjs, @vcsjones, @krwq, @jeffhandley
See info in area-owners.md if you want to be subscribed.

@GrabYourPitchforks
Copy link
Member

I am strongly against the API as proposed here. We have evidence in other threads that people want to take their existing code and port it to these environments, and that they're unable to make changes to these existing code paths because it would require asyncifying their code in unnatural ways. The current plan is to remove entirely the managed implementations in the .NET 6 time frame.

I would instead prefer the following. Either:

  • Make the existing APIs work by utilizing a sync-over-async mechanism, or
  • Put the async APIs in a wasm-specific namespace and tell developers that these are the only APIs that will be supported in wasm

@blowdart
Copy link
Contributor

This feels like polluting/hobbling the greater framework because of the sins of javascript and SubtleCrypto.

This, combined with the fact that the only reason managed crypto made it back was to be a stopga for WASM until we can remove it again, breaking the approach we've had for .NET Core to not implement crypto in the stack makes this a big non-starter.

@kjpou1
Copy link
Contributor Author

kjpou1 commented Nov 9, 2020

Hey everyone.

Right now there really is not a way to do sync over async as webassembly does not support threads at this time and is planned for .NET 7 time frame from my understanding.

Not sure I would classify these as sins of javascript and SubtleCrypto but the way they implemented them could be thought of as keeping up with with the times of bringing an async API to something that can be potentially heavy processing.

Putting the async APIs in a wasm-specific namespace and tell developers that these are the only APIs that will be supported in wasm is an option that can be considered.

@kjpou1
Copy link
Contributor Author

kjpou1 commented Nov 9, 2020

@vcsjones
Copy link
Member

vcsjones commented Nov 9, 2020

Right now there really is not a way to do sync over async as webassembly does not support threads at this time and is planned for .NET 7 time frame from my understanding.

What then would be an eventual end goal with all cryptographic primitives? I would imagine if folks want hashing, they are also going to want symmetric / asymmetric encryption, signing, KDFs, etc. This moves toward having many new API additions for all cryptographic primitive operations.

There is also a precedent for async within the API surface.

That, to me, is different than an async cryptographic primitive. That does async IO to update a hash.

Putting the async APIs in a wasm-specific namespace and tell developers that these are the only APIs that will be supported in wasm

I'm rather uneducated about blazor, but I thought to task: do new APIs need to exist in this case? Isn't there a mechanism to invoke browser JS from Blazor WASM via IJSRuntime or similar?

@GrabYourPitchforks
Copy link
Member

Right now there really is not a way to do sync over async as webassembly does not support threads at this time and is planned for .NET 7 time frame from my understanding.

Is there any way at all to mimic sync-over-async in wasm? Our security signoff of this feature in .NET 5 was contingent on the managed implementation being completely removed in the .NET 6 timeframe. This was listed as an explicit goal for the .NET 6 timeframe, and it's what allowed our exception to go through.

Not sure I would classify these as sins of javascript and SubtleCrypto but the way they implemented them could be thought of as keeping up with with the times of bringing an async API to something that can be potentially heavy processing.

Aside from the PFX library, we don't normally provide async overloads of methods known to be CPU-bound. Async methods are really meant for things like non-blocking i/o. For CPU-bound work, the normal pattern is to provide a sync-only overload, and the developer can Task.Run it if needed.

Now, if you have a key handle instead of raw key material, that's a different matter, since the processing might actually be done off-box. But the APIs we're talking about here take their key material as raw bytes, which means they're all going to be processed in-proc using the local CPU.

@kjpou1
Copy link
Contributor Author

kjpou1 commented Nov 10, 2020

Is there any way at all to mimic sync-over-async in wasm? Our security signoff of this feature in .NET 5 was contingent on the managed implementation being completely removed in the .NET 6 timeframe. This was listed as an explicit goal for the .NET 6 timeframe, and it's what allowed our exception to go through.

Not at this time due to threads and that support is, again from my understanding, scheduled for .NET 7.

Putting the async APIs in an async-specific namespace and tell developers that these are the only APIs that will be supported in wasm
is an option that is being discussed.

@GrabYourPitchforks
Copy link
Member

We can't even do something naïve like kick off the async work, then spin the original thread waiting for the promise to finish?

@marek-safar marek-safar removed the untriaged New issue has not been triaged by the area owner label Nov 10, 2020
@marek-safar marek-safar added this to the 6.0.0 milestone Nov 10, 2020
@jkoritzinsky
Copy link
Member

There's no mechanism to wait for a Promise in Javascript. It only provides a then function to specify a continuation when it is complete.

I've tried numerous ways to work around this with Web Workers, but trying something as simple as spin-waiting while waiting for an entry in a shared buffer to be updated seems to block the Web Worker from executing, and the Atomics.wait function which would allow us to block and not spin-wait is disallowed on the main thread on Chrome and Edge.

@bartonjs
Copy link
Member

How insane would it be to move the main synchronous flow off of the main thread and have main just be another web worker?

@jkoritzinsky
Copy link
Member

As someone who doesn't work on the WASM backend or on Blazor and only looked into this for the total of an hour or so, it looks like it should be possible, but it would require refactoring all of the Blazor calls into Javascript that touch the DOM to be asynchronous and to work via Promises and message passing from a Web Worker back to the main thread.

Given that everything ASP.NET Core is already async-ified, it shouldn't be impossible to do, but I'm not the right person to make that designation.

@GrabYourPitchforks
Copy link
Member

We just posted our plan for cryptographic support in Blazor/wasm apps in .NET 7 and beyond. See here for full details. That document also includes sample code demonstrating how to interop with the JS layer to get support for other algorithms (like HMACSHA256) which aren't supported out-of-the-box in Blazor/wasm in .NET 6.

Please use #40074 for feedback on that document. I am closing this issue (#43939) because, per that document, we do not have plans to provide async overloads of the one-shot hash routines. Our primary focus is on getting the existing synchronous overloads to work properly.

Thanks!

@ghost ghost locked as resolved and limited conversation to collaborators Sep 3, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Security
Projects
None yet
Development

No branches or pull requests

8 participants