From 91e6df6f03c982d7a01c9144fbfadd1626741b74 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 26 Sep 2026 08:22:34 -0400 Subject: [PATCH] Support EC private scalar-only imports on iOS On iOS / tvOS / MacCatalyst, importing an EC key with only `d` would fail because it was not implemented. Previously it was not implemented because CryptoKit was not available to the platform. But now it is, so lets implement it. This matters for HPKE - right now most integration tests for HPKE are failing because HPKE depends on being able to import only the private scalar. This fixes all HPKE tests on mobile Apple platforms. --- .../Interop.Ecc.cs | 51 +++++++++++++++++++ .../Cryptography/EccAppleCrypto.iOS.cs | 31 ++++++++++- .../DefaultECDiffieHellmanProvider.Unix.cs | 2 +- .../entrypoints.c | 1 + .../pal_swiftbindings.h | 2 + .../pal_swiftbindings.swift | 43 ++++++++++++++++ 6 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Ecc.cs b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Ecc.cs index fa96f3c3577aab..a5431a324677bf 100644 --- a/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Ecc.cs +++ b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Ecc.cs @@ -1,11 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Swift; using System.Security.Cryptography; using System.Security.Cryptography.Apple; using Microsoft.Win32.SafeHandles; +using Swift.Runtime; + +#pragma warning disable CS3016 // Arrays as attribute arguments are not CLS Compliant internal static partial class Interop { @@ -21,6 +27,51 @@ private static partial int AppleCryptoNative_EccGenerateKey( [LibraryImport(Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_EccGetKeySizeInBits")] internal static partial int EccGetKeySizeInBits(SafeSecKeyRefHandle publicKey); + [LibraryImport(Libraries.AppleCryptoNative)] + [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] + private static unsafe partial int AppleCryptoNative_EccExportPublicKeyFromPrivateKey( + int keySizeInBits, + UnsafeBufferPointer privateKey, + UnsafeMutableBufferPointer destination); + + internal static void EccExportPublicKeyFromPrivateKey( + int keySizeInBits, + ReadOnlySpan privateKey, + Span destination) + { + Debug.Assert(!privateKey.IsEmpty); + Debug.Assert(!destination.IsEmpty); + + const int Success = 1; + const int InvalidKey = 0; + + int result; + + unsafe + { + fixed (byte* privateKeyPtr = privateKey) + fixed (byte* destinationPtr = destination) + { + result = AppleCryptoNative_EccExportPublicKeyFromPrivateKey( + keySizeInBits, + new UnsafeBufferPointer(privateKeyPtr, privateKey.Length), + new UnsafeMutableBufferPointer(destinationPtr, destination.Length)); + } + } + + switch (result) + { + case Success: + return; + case InvalidKey: + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + default: + Debug.Fail( + $"Unexpected result from {nameof(AppleCryptoNative_EccExportPublicKeyFromPrivateKey)}: {result}"); + throw new CryptographicException(); + } + } + internal static void EccGenerateKey( int keySizeInBits, out SafeSecKeyRefHandle pPublicKey, diff --git a/src/libraries/Common/src/System/Security/Cryptography/EccAppleCrypto.iOS.cs b/src/libraries/Common/src/System/Security/Cryptography/EccAppleCrypto.iOS.cs index 16785944f39ad6..0ba8cacc98aeef 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/EccAppleCrypto.iOS.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/EccAppleCrypto.iOS.cs @@ -15,9 +15,36 @@ internal sealed partial class EccAppleCrypto #pragma warning disable IDE0060 private static ECParameters ExportParametersFromLegacyKey(SecKeyPair keys, bool includePrivateParameters) => throw new CryptographicException(); +#pragma warning restore IDE0060 private static void ExtractPublicKeyFromPrivateKey(ref ECParameters ecParameters) - => throw new PlatformNotSupportedException(SR.Cryptography_NotValidPublicOrPrivateKey); -#pragma warning restore IDE0060 + { + int keySizeInBits = ecParameters.Curve.Oid.Value switch + { + Oids.secp256r1 => 256, + Oids.secp384r1 => 384, + Oids.secp521r1 => 521, + _ => throw DebugFail(), // Apple only supports NIST curves. + }; + + byte[] privateKey = ecParameters.D!; + int fieldSize = (keySizeInBits + 7) / 8; + Debug.Assert(privateKey.Length == fieldSize); + + const int MaxPublicKeySize = 136; // P-521 is 133 bytes, round this off to 136. + Span publicKey = (stackalloc byte[MaxPublicKeySize]).Slice(0, 1 + 2 * fieldSize); + Interop.AppleCrypto.EccExportPublicKeyFromPrivateKey(keySizeInBits, privateKey, publicKey); + AsymmetricAlgorithmHelpers.DecodeFromUncompressedAnsiX963Key( + publicKey, + hasPrivateKey: false, + out ECParameters publicParameters); + ecParameters.Q = publicParameters.Q; + + static Exception DebugFail() + { + Debug.Fail($"Unexpected curve with OID."); + return new CryptographicException(); + } + } } } diff --git a/src/libraries/System.Security.Cryptography/tests/DefaultECDiffieHellmanProvider.Unix.cs b/src/libraries/System.Security.Cryptography/tests/DefaultECDiffieHellmanProvider.Unix.cs index 7a79ec911cf1b4..980e52de11e504 100644 --- a/src/libraries/System.Security.Cryptography/tests/DefaultECDiffieHellmanProvider.Unix.cs +++ b/src/libraries/System.Security.Cryptography/tests/DefaultECDiffieHellmanProvider.Unix.cs @@ -32,7 +32,7 @@ public override bool ExplicitCurvesSupported } } - public override bool CanDeriveNewPublicKey => !PlatformDetection.IsiOS && !PlatformDetection.IstvOS && !PlatformDetection.IsMacCatalyst; + public override bool CanDeriveNewPublicKey => true; public override bool SupportsRawDerivation => true; public override bool SupportsSha3 => PlatformDetection.SupportsSha3; diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c b/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c index 65996b5471eaab..b94700bda7289f 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c @@ -41,6 +41,7 @@ static const Entry s_cryptoAppleNative[] = DllImportEntry(AppleCryptoNative_DigestOneShot) DllImportEntry(AppleCryptoNative_DigestReset) DllImportEntry(AppleCryptoNative_DigestUpdate) + DllImportEntry(AppleCryptoNative_EccExportPublicKeyFromPrivateKey) DllImportEntry(AppleCryptoNative_EccGenerateKey) DllImportEntry(AppleCryptoNative_EccGetKeySizeInBits) DllImportEntry(AppleCryptoNative_EcdhKeyAgree) diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h index 1200ceeb13b10f..8de19d512dfe10 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h @@ -14,6 +14,8 @@ EXTERN_C void* AppleCryptoNative_AesKeyWrapEncrypt; EXTERN_C void* AppleCryptoNative_AesKeyWrapDecrypt; EXTERN_C void* AppleCryptoNative_IsAuthenticationFailure; +EXTERN_C void* AppleCryptoNative_EccExportPublicKeyFromPrivateKey; + EXTERN_C void* AppleCryptoNative_HKDFDeriveKey; EXTERN_C void* AppleCryptoNative_HKDFExpand; EXTERN_C void* AppleCryptoNative_HKDFExtract; diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift index 28a3da7e14aeee..fc056d05d0d6d7 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift @@ -595,6 +595,49 @@ public func AppleCryptoNative_DigestCurrent(ctx: UnsafeMutableRawPointer?, pOutp return 1 } +@_silgen_name("AppleCryptoNative_EccExportPublicKeyFromPrivateKey") +public func AppleCryptoNative_EccExportPublicKeyFromPrivateKey( + keySizeInBits: Int32, + privateKey: UnsafeBufferPointer, + destination: UnsafeMutableBufferPointer) -> Int32 { + guard !privateKey.isEmpty, !destination.isEmpty else { + return -1 + } + + // The purpose of this method is to take an EC private scalar D and compute the public value, Q. For this + // limited purpose it doesn't matter if we use KeyAgreement or Signing because the result will be the same. + // This implementation just uses KeyAgreement. + let publicKey: Data + + switch keySizeInBits { + case 256: + guard let key = try? P256.KeyAgreement.PrivateKey(rawRepresentation: privateKey) else { + return 0 + } + publicKey = key.publicKey.x963Representation + case 384: + guard let key = try? P384.KeyAgreement.PrivateKey(rawRepresentation: privateKey) else { + return 0 + } + publicKey = key.publicKey.x963Representation + case 521: + guard let key = try? P521.KeyAgreement.PrivateKey(rawRepresentation: privateKey) else { + return 0 + } + publicKey = key.publicKey.x963Representation + default: + return -1 + } + + guard publicKey.count == destination.count else { + return -1 + } + + let copied = publicKey.copyBytes(to: destination) == publicKey.count + + return copied ? 1 : -1 +} + // Return values: // 1: success // 0: key agreement failed (e.g. peer is a low-order point and the shared