File: src\runtime\src\libraries\Common\src\System\Security\Cryptography\X25519WindowsHelpers.cs
Web Access
Project: src\runtime\src\libraries\System.Security.Cryptography\src\System.Security.Cryptography.csproj (System.Security.Cryptography)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;

namespace System.Security.Cryptography
{
    internal static class X25519WindowsHelpers
    {
        // https://learn.microsoft.com/en-us/windows/win32/seccng/cng-named-elliptic-curves
        internal const string BCRYPT_ECC_CURVE_25519 = "curve25519";
        private const int PublicKeySizeInBytes = X25519DiffieHellman.PublicKeySizeInBytes;
        private const int ElementSize = 32;

        // p = 2^255 - 19 in little-endian
        private static ReadOnlySpan<byte> FieldPrime =>
        [
            0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f,
        ];

        internal static void ExportKey(ReadOnlySpan<byte> exported, bool privateKey, Span<byte> destination)
        {
            Interop.BCrypt.KeyBlobMagicNumber expectedMagicNumber = privateKey ?
                Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PRIVATE_GENERIC_MAGIC :
                Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PUBLIC_GENERIC_MAGIC;

            unsafe
            {
                int blobHeaderSize = sizeof(Interop.BCrypt.BCRYPT_ECCKEY_BLOB);

                // For private key we expect three parameters (x, y, d) and for public keys we expect two (x, y).
                if (exported.Length < blobHeaderSize + ElementSize * (privateKey ? 3 : 2))
                {
                    throw new CryptographicException();
                }

                fixed (byte* pExportedSpan = exported)
                {
                    Interop.BCrypt.BCRYPT_ECCKEY_BLOB* blob = (Interop.BCrypt.BCRYPT_ECCKEY_BLOB*)pExportedSpan;

                    if (blob->cbKey != ElementSize || blob->Magic != expectedMagicNumber)
                    {
                        throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey);
                    }

                    // The key material after the blob is { x || y }, and optionally d if there is a private key.
                    // y should always be zero as it is not used for Curve25519 keys.

                    // Check y is zero, skip over the blob header and x.
                    ReadOnlySpan<byte> y = new(pExportedSpan + blobHeaderSize + ElementSize, ElementSize);

                    // y shouldn't have a value.
                    if (y.IndexOfAnyExcept((byte)0) >= 0)
                    {
                        throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey);
                    }

                    if (privateKey)
                    {
                        ReadOnlySpan<byte> d = new(pExportedSpan + blobHeaderSize + ElementSize * 2, ElementSize);
                        d.CopyTo(destination);
                    }
                    else
                    {
                        ReadOnlySpan<byte> x = new(pExportedSpan + blobHeaderSize, ElementSize);
                        x.CopyTo(destination);
                    }
                }
            }
        }

        internal static CryptoPoolLease CreateCngBlob(ReadOnlySpan<byte> key, bool privateKey, out byte preservation)
        {
            Interop.BCrypt.KeyBlobMagicNumber magicNumber = privateKey ?
                Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PRIVATE_GENERIC_MAGIC :
                Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PUBLIC_GENERIC_MAGIC;

            unsafe
            {
                int blobHeaderSize = sizeof(Interop.BCrypt.BCRYPT_ECCKEY_BLOB);
                int requiredBufferSize = blobHeaderSize + ElementSize * 2; // blob + X, Y

                if (privateKey)
                {
                    requiredBufferSize += ElementSize; // d
                }

                CryptoPoolLease lease = CryptoPoolLease.Rent(requiredBufferSize, skipClear: !privateKey);
                lease.Span.Clear();

                fixed (byte* pBlobHeader = lease.Span)
                {
                    Interop.BCrypt.BCRYPT_ECCKEY_BLOB* blob = (Interop.BCrypt.BCRYPT_ECCKEY_BLOB*)pBlobHeader;
                    blob->Magic = magicNumber;
                    blob->cbKey = ElementSize;
                }

                if (privateKey)
                {
                    // This builds a blob of { x || y || d }. x is the public key, and we leave it as all zeros
                    // and CNG will reconstruct the public key from the private key.
                    // y is not used for Curve25519 so we leave it as zeros.
                    // d follows y. Since we zeroed the whole blob, skip over the header, x, and y and write d.
                    Span<byte> destination = lease.Span.Slice(blobHeaderSize + ElementSize * 2, ElementSize);
                    key.CopyTo(destination);

                    // Any fixup of the key is done in-place in the rented blob, which gets zeroed later.
                    preservation = FixupPrivateScalar(destination);
                }
                else
                {
                    // Otherwise if we are importing the public key, write x after the header.
                    Span<byte> destination = lease.Span.Slice(blobHeaderSize, ElementSize);
                    key.CopyTo(destination);
                    preservation = 0;
                }

                return lease;
            }
        }

        internal static byte FixupPrivateScalar(Span<byte> bytes)
        {
            byte preservation = (byte)(bytes[0] & 0b111 | bytes[^1] & 0b11000000);

            // From RFC 7748:
            // For X25519, in
            // order to decode 32 random bytes as an integer scalar, set the three
            // least significant bits of the first byte and the most significant bit
            // of the last to zero, set the second most significant bit of the last
            // byte to 1 and, finally, decode as little-endian.
            //
            // Most other X25519 implementations do this for you when importing a private key. CNG does not, so we
            // apply the scalar fixup here.
            //
            // If we import a key that requires us to modify it, we store the modified bits in a byte. This byte does
            // not effectively contain any private key material since these bits are always coerced. However we want
            // keys to roundtrip correctly.
            bytes[0] &= 0b11111000;
            bytes[^1] &= 0b01111111;
            bytes[^1] |= 0b01000000;
            return preservation;
        }

        internal static void RefixPrivateScalar(Span<byte> bytes, byte preservation)
        {
            bytes[0] = (byte)((preservation & 0b111) | (bytes[0] & 0b11111000));
            bytes[^1] = (byte)((preservation & 0b11000000) | (bytes[^1] & 0b00111111));
        }

        internal static bool ReducePublicKey(ReadOnlySpan<byte> publicKey, Span<byte> reduced)
        {
            Debug.Assert(publicKey.Length == PublicKeySizeInBytes);
            Debug.Assert(reduced.Length == PublicKeySizeInBytes);

            // RFC 7748 Section 5: "implementations of X25519 MUST mask the most significant
            // bit in the final byte" and "Implementations MUST accept non-canonical values and
            // process them as if they had been reduced modulo the field prime."
            //
            // CNG rejects non-canonical u-coordinates (values >= p = 2^255 - 19) and does not
            // mask the high bit. We handle both by masking the high bit then if the value is
            // non-canonical, subtract p to reduce it. Since all values are < 2^255 after
            // masking and p = 2^255 - 19, a single subtraction suffices.
            publicKey.CopyTo(reduced);
            reduced[^1] &= 0x7F;

            bool requiredReduction = false;

            if (IsNonCanonicalPublicKey(reduced))
            {
                requiredReduction = true;
                ReducePublicKey(reduced);
            }
            else if ((publicKey[^1] & 0x80) != 0)
            {
                requiredReduction = true;
            }

            return requiredReduction;
        }

        private static bool IsNonCanonicalPublicKey(ReadOnlySpan<byte> key)
        {
            Debug.Assert(key.Length == PublicKeySizeInBytes);
            Debug.Assert((key[^1] & 0x80) == 0);

            // Compare key >= p (little-endian). Since key < 2^255 (high bit masked)
            // and p = 2^255 - 19, a non-canonical value is in [p, 2^255 - 1].
            // Compare from most significant byte to least significant.
            for (int i = PublicKeySizeInBytes - 1; i >= 0; i--)
            {
                if (key[i] > FieldPrime[i])
                    return true;
                if (key[i] < FieldPrime[i])
                    return false;
            }

            // key == p, which is also non-canonical (reduces to 0)
            return true;
        }

        private static void ReducePublicKey(Span<byte> key)
        {
            Debug.Assert(key.Length == PublicKeySizeInBytes);

            // Subtract p from key. Since we only call this when key >= p and key < 2^255,
            // a single subtraction is sufficient: key = key - p.
            int borrow = 0;

            for (int i = 0; i < PublicKeySizeInBytes; i++)
            {
                int diff = key[i] - FieldPrime[i] - borrow;
                key[i] = (byte)diff;
                borrow = (diff < 0) ? 1 : 0;
            }

            Debug.Assert(borrow == 0);
        }
    }
}