From b7f2cb90078a2806652ae0d9edbd783f9a6d706a Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 25 Sep 2026 16:57:55 -0400 Subject: [PATCH 1/3] Support private-only ECC key imports on mobile Apple --- .../Interop.Ecc.cs | 42 +++++++++++++++++ .../Cryptography/EccAppleCrypto.iOS.cs | 22 ++++++++- .../DefaultECDiffieHellmanProvider.Unix.cs | 2 +- .../entrypoints.c | 1 + .../pal_swiftbindings.h | 2 + .../pal_swiftbindings.swift | 47 +++++++++++++++++++ 6 files changed, 114 insertions(+), 2 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..ccf8c898690448 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,12 +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; +#pragma warning disable CS3016 // Arrays as attribute arguments are not CLS Compliant + internal static partial class Interop { internal static partial class AppleCrypto @@ -21,6 +26,43 @@ 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 partial int AppleCryptoNative_EccExportPublicKeyFromPrivateKey( + int keySizeInBits, + ReadOnlySpan privateKey, + int privateKeyLength, + Span destination, + int destinationLength); + + internal static void EccExportPublicKeyFromPrivateKey( + int keySizeInBits, + ReadOnlySpan privateKey, + Span destination) + { + const int Success = 1; + const int InvalidKey = 0; + + int result = AppleCryptoNative_EccExportPublicKeyFromPrivateKey( + keySizeInBits, + privateKey, + privateKey.Length, + destination, + 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..5bd614d0b24c7a 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/EccAppleCrypto.iOS.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/EccAppleCrypto.iOS.cs @@ -17,7 +17,27 @@ private static ECParameters ExportParametersFromLegacyKey(SecKeyPair keys, bool => throw new CryptographicException(); private static void ExtractPublicKeyFromPrivateKey(ref ECParameters ecParameters) - => throw new PlatformNotSupportedException(SR.Cryptography_NotValidPublicOrPrivateKey); + { + int keySizeInBits = ecParameters.Curve.Oid.Value switch + { + Oids.secp256r1 => 256, + Oids.secp384r1 => 384, + Oids.secp521r1 => 521, + _ => throw new UnreachableException(), + }; + + byte[] privateKey = ecParameters.D!; + int fieldSize = (keySizeInBits + 7) / 8; + Debug.Assert(privateKey.Length == fieldSize); + + Span publicKey = stackalloc byte[1 + 2 * fieldSize]; + Interop.AppleCrypto.EccExportPublicKeyFromPrivateKey(keySizeInBits, privateKey, publicKey); + AsymmetricAlgorithmHelpers.DecodeFromUncompressedAnsiX963Key( + publicKey, + hasPrivateKey: false, + out ECParameters publicParameters); + ecParameters.Q = publicParameters.Q; + } #pragma warning restore IDE0060 } } 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..ad691483e0f6db 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,53 @@ public func AppleCryptoNative_DigestCurrent(ctx: UnsafeMutableRawPointer?, pOutp return 1 } +@_silgen_name("AppleCryptoNative_EccExportPublicKeyFromPrivateKey") +public func AppleCryptoNative_EccExportPublicKeyFromPrivateKey( + keySizeInBits: Int32, + pPrivateKey: UnsafeMutableRawPointer?, + cbPrivateKey: Int32, + pOutput: UnsafeMutablePointer?, + cbOutput: Int32) -> Int32 { + guard let pPrivateKey, cbPrivateKey > 0, let pOutput, cbOutput > 0 else { + return -1 + } + + let privateKey = Data(bytesNoCopy: pPrivateKey, count: Int(cbPrivateKey), deallocator: Data.Deallocator.none) + 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 + } + + let destination = UnsafeMutableRawBufferPointer(start: pOutput, count: Int(cbOutput)) + + guard publicKey.count == destination.count else { + return -1 + } + + let copied = publicKey.withUnsafeBytes { publicKeyBytes in + return publicKeyBytes.copyBytes(to: destination) == publicKeyBytes.count + } + + return copied ? 1 : -1 +} + // Return values: // 1: success // 0: key agreement failed (e.g. peer is a low-order point and the shared From 8b691b7222964fdbe81b4cf9fefd256e898862fa Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 25 Sep 2026 17:38:58 -0400 Subject: [PATCH 2/3] Refactor and add some clarifying comments --- .../Interop.Ecc.cs | 31 ++++++++++++------- .../Cryptography/EccAppleCrypto.iOS.cs | 13 ++++++-- .../pal_swiftbindings.swift | 18 +++++------ 3 files changed, 37 insertions(+), 25 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 ccf8c898690448..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 @@ -9,6 +9,7 @@ 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 @@ -28,27 +29,35 @@ private static partial int AppleCryptoNative_EccGenerateKey( [LibraryImport(Libraries.AppleCryptoNative)] [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] - private static partial int AppleCryptoNative_EccExportPublicKeyFromPrivateKey( + private static unsafe partial int AppleCryptoNative_EccExportPublicKeyFromPrivateKey( int keySizeInBits, - ReadOnlySpan privateKey, - int privateKeyLength, - Span destination, - int destinationLength); + 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 = AppleCryptoNative_EccExportPublicKeyFromPrivateKey( - keySizeInBits, - privateKey, - privateKey.Length, - destination, - destination.Length); + 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) { 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 5bd614d0b24c7a..23b68f7be6a2e7 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/EccAppleCrypto.iOS.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/EccAppleCrypto.iOS.cs @@ -15,6 +15,7 @@ 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) { @@ -23,21 +24,27 @@ private static void ExtractPublicKeyFromPrivateKey(ref ECParameters ecParameters Oids.secp256r1 => 256, Oids.secp384r1 => 384, Oids.secp521r1 => 521, - _ => throw new UnreachableException(), + _ => throw DebugFail(), // Apple only supports NIST curves. }; byte[] privateKey = ecParameters.D!; int fieldSize = (keySizeInBits + 7) / 8; Debug.Assert(privateKey.Length == fieldSize); - Span publicKey = stackalloc byte[1 + 2 * fieldSize]; + const int MaxPublicKeySize = 192; // P-521 is 133 bytes, round this off to 192. + 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(); + } } -#pragma warning restore IDE0060 } } 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 ad691483e0f6db..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 @@ -598,15 +598,15 @@ public func AppleCryptoNative_DigestCurrent(ctx: UnsafeMutableRawPointer?, pOutp @_silgen_name("AppleCryptoNative_EccExportPublicKeyFromPrivateKey") public func AppleCryptoNative_EccExportPublicKeyFromPrivateKey( keySizeInBits: Int32, - pPrivateKey: UnsafeMutableRawPointer?, - cbPrivateKey: Int32, - pOutput: UnsafeMutablePointer?, - cbOutput: Int32) -> Int32 { - guard let pPrivateKey, cbPrivateKey > 0, let pOutput, cbOutput > 0 else { + privateKey: UnsafeBufferPointer, + destination: UnsafeMutableBufferPointer) -> Int32 { + guard !privateKey.isEmpty, !destination.isEmpty else { return -1 } - let privateKey = Data(bytesNoCopy: pPrivateKey, count: Int(cbPrivateKey), deallocator: Data.Deallocator.none) + // 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 { @@ -629,15 +629,11 @@ public func AppleCryptoNative_EccExportPublicKeyFromPrivateKey( return -1 } - let destination = UnsafeMutableRawBufferPointer(start: pOutput, count: Int(cbOutput)) - guard publicKey.count == destination.count else { return -1 } - let copied = publicKey.withUnsafeBytes { publicKeyBytes in - return publicKeyBytes.copyBytes(to: destination) == publicKeyBytes.count - } + let copied = publicKey.copyBytes(to: destination) == publicKey.count return copied ? 1 : -1 } From 91abf3cea186871a3affcfd59498e6adcab65a7a Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 25 Sep 2026 18:41:05 -0400 Subject: [PATCH 3/3] Apply feedback --- .../src/System/Security/Cryptography/EccAppleCrypto.iOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 23b68f7be6a2e7..0ba8cacc98aeef 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/EccAppleCrypto.iOS.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/EccAppleCrypto.iOS.cs @@ -31,7 +31,7 @@ private static void ExtractPublicKeyFromPrivateKey(ref ECParameters ecParameters int fieldSize = (keySizeInBits + 7) / 8; Debug.Assert(privateKey.Length == fieldSize); - const int MaxPublicKeySize = 192; // P-521 is 133 bytes, round this off to 192. + 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(