Skip to content

Commit d4bab6b

Browse files
authored
Manually depad RSAES-PKCS1 on Apple OSes
1 parent 6f6bfc3 commit d4bab6b

File tree

6 files changed

+319
-11
lines changed

6 files changed

+319
-11
lines changed

src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.RSA.cs

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Buffers;
56
using System.Diagnostics;
67
using System.Runtime.InteropServices;
78
using System.Security.Cryptography;
@@ -69,8 +70,8 @@ private static partial int RsaDecryptOaep(
6970
out SafeCFDataHandle pEncryptedOut,
7071
out SafeCFErrorHandle pErrorOut);
7172

72-
[LibraryImport(Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_RsaDecryptPkcs")]
73-
private static partial int RsaDecryptPkcs(
73+
[LibraryImport(Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_RsaDecryptRaw")]
74+
private static partial int RsaDecryptRaw(
7475
SafeSecKeyRefHandle publicKey,
7576
ReadOnlySpan<byte> pbData,
7677
int cbData,
@@ -166,17 +167,40 @@ internal static byte[] RsaDecrypt(
166167
byte[] data,
167168
RSAEncryptionPadding padding)
168169
{
170+
if (padding == RSAEncryptionPadding.Pkcs1)
171+
{
172+
byte[] padded = ExecuteTransform(
173+
data,
174+
(ReadOnlySpan<byte> source, out SafeCFDataHandle decrypted, out SafeCFErrorHandle error) =>
175+
RsaDecryptRaw(privateKey, source, source.Length, out decrypted, out error));
176+
177+
byte[] depad = CryptoPool.Rent(padded.Length);
178+
OperationStatus status = RsaPaddingProcessor.DepadPkcs1Encryption(padded, depad, out int written);
179+
byte[]? ret = null;
180+
181+
if (status == OperationStatus.Done)
182+
{
183+
ret = depad.AsSpan(0, written).ToArray();
184+
}
185+
186+
// Clear the whole thing, especially on failure.
187+
CryptoPool.Return(depad);
188+
CryptographicOperations.ZeroMemory(padded);
189+
190+
if (ret is null)
191+
{
192+
throw new CryptographicException(SR.Cryptography_InvalidPadding);
193+
}
194+
195+
return ret;
196+
}
197+
198+
Debug.Assert(padding.Mode == RSAEncryptionPaddingMode.Oaep);
199+
169200
return ExecuteTransform(
170201
data,
171202
(ReadOnlySpan<byte> source, out SafeCFDataHandle decrypted, out SafeCFErrorHandle error) =>
172203
{
173-
if (padding == RSAEncryptionPadding.Pkcs1)
174-
{
175-
return RsaDecryptPkcs(privateKey, source, source.Length, out decrypted, out error);
176-
}
177-
178-
Debug.Assert(padding.Mode == RSAEncryptionPaddingMode.Oaep);
179-
180204
return RsaDecryptOaep(
181205
privateKey,
182206
source,
@@ -195,14 +219,63 @@ internal static bool TryRsaDecrypt(
195219
out int bytesWritten)
196220
{
197221
Debug.Assert(padding.Mode == RSAEncryptionPaddingMode.Pkcs1 || padding.Mode == RSAEncryptionPaddingMode.Oaep);
222+
223+
if (padding.Mode == RSAEncryptionPaddingMode.Pkcs1)
224+
{
225+
byte[] padded = CryptoPool.Rent(source.Length);
226+
byte[] depad = CryptoPool.Rent(source.Length);
227+
228+
bool processed = TryExecuteTransform(
229+
source,
230+
padded,
231+
out int paddedLength,
232+
(ReadOnlySpan<byte> innerSource, out SafeCFDataHandle outputHandle, out SafeCFErrorHandle errorHandle) =>
233+
RsaDecryptRaw(privateKey, innerSource, innerSource.Length, out outputHandle, out errorHandle));
234+
235+
Debug.Assert(
236+
processed,
237+
"TryExecuteTransform should always return true for a large enough buffer.");
238+
239+
OperationStatus status = OperationStatus.InvalidData;
240+
int depaddedLength = 0;
241+
242+
if (processed)
243+
{
244+
status = RsaPaddingProcessor.DepadPkcs1Encryption(
245+
new ReadOnlySpan<byte>(padded, 0, paddedLength),
246+
depad,
247+
out depaddedLength);
248+
}
249+
250+
CryptoPool.Return(padded);
251+
252+
if (status == OperationStatus.Done)
253+
{
254+
if (depaddedLength <= destination.Length)
255+
{
256+
depad.AsSpan(0, depaddedLength).CopyTo(destination);
257+
CryptoPool.Return(depad);
258+
bytesWritten = depaddedLength;
259+
return true;
260+
}
261+
262+
CryptoPool.Return(depad);
263+
bytesWritten = 0;
264+
return false;
265+
}
266+
267+
CryptoPool.Return(depad);
268+
Debug.Assert(status == OperationStatus.InvalidData);
269+
throw new CryptographicException(SR.Cryptography_InvalidPadding);
270+
}
271+
198272
return TryExecuteTransform(
199273
source,
200274
destination,
201275
out bytesWritten,
202276
delegate (ReadOnlySpan<byte> innerSource, out SafeCFDataHandle outputHandle, out SafeCFErrorHandle errorHandle)
203277
{
204-
return padding.Mode == RSAEncryptionPaddingMode.Pkcs1 ?
205-
RsaDecryptPkcs(privateKey, innerSource, innerSource.Length, out outputHandle, out errorHandle) :
278+
return
206279
RsaDecryptOaep(privateKey, innerSource, innerSource.Length, PalAlgorithmFromAlgorithmName(padding.OaepHashAlgorithm), out outputHandle, out errorHandle);
207280
});
208281
}

src/libraries/Common/src/System/Security/Cryptography/RsaPaddingProcessor.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
45
using System.Buffers.Binary;
56
using System.Collections.Concurrent;
67
using System.Diagnostics;
@@ -142,6 +143,109 @@ internal static void PadPkcs1Encryption(
142143
source.CopyTo(mInEM);
143144
}
144145

146+
internal static OperationStatus DepadPkcs1Encryption(
147+
ReadOnlySpan<byte> source,
148+
Span<byte> destination,
149+
out int bytesWritten)
150+
{
151+
int primitive = DepadPkcs1Encryption(source);
152+
int primitiveSign = SignStretch(primitive);
153+
154+
// Primitive is a positive length, or ~length to indicate
155+
// an error, so flip ~length to length if the high bit is set.
156+
int len = Choose(primitiveSign, ~primitive, primitive);
157+
int spaceRemain = destination.Length - len;
158+
int spaceRemainSign = SignStretch(spaceRemain);
159+
160+
// len = clampHigh(len, destination.Length);
161+
len = Choose(spaceRemainSign, destination.Length, len);
162+
163+
// ret = spaceRemain < 0 ? DestinationTooSmall : Done
164+
int ret = Choose(
165+
spaceRemainSign,
166+
(int)OperationStatus.DestinationTooSmall,
167+
(int)OperationStatus.Done);
168+
169+
// ret = primitive < 0 ? InvalidData : ret;
170+
ret = Choose(primitiveSign, (int)OperationStatus.InvalidData, ret);
171+
172+
// Write some number of bytes, regardless of the final return.
173+
source[^len..].CopyTo(destination);
174+
175+
// bytesWritten = ret == Done ? len : 0;
176+
bytesWritten = Choose(CheckZero(ret), len, 0);
177+
return (OperationStatus)ret;
178+
}
179+
180+
private static int DepadPkcs1Encryption(ReadOnlySpan<byte> source)
181+
{
182+
Debug.Assert(source.Length > 11);
183+
ReadOnlySpan<byte> afterPadding = source.Slice(10);
184+
ReadOnlySpan<byte> noZeros = source.Slice(2, 8);
185+
186+
// Find the first zero in noZeros, or -1 for no zeros.
187+
int zeroPos = BlindFindFirstZero(noZeros);
188+
189+
// If zeroPos is negative, valid is -1, otherwise 0.
190+
int valid = SignStretch(zeroPos);
191+
192+
// If there are no zeros in afterPadding then zeroPos is negative,
193+
// so negating the sign stretch is 0, which makes hasPos 0.
194+
// If there -was- a zero, sign stretching is 0, so negating it makes hasPos -1.
195+
zeroPos = BlindFindFirstZero(afterPadding);
196+
int hasLen = ~SignStretch(zeroPos);
197+
valid &= hasLen;
198+
199+
// Check that the first two bytes are { 00 02 }
200+
valid &= CheckZero(source[0] | (source[1] ^ 0x02));
201+
202+
int lenIfGood = afterPadding.Length - zeroPos - 1;
203+
// If there were no zeros, use the full after-min-padding segment.
204+
int lenIfBad = ~Choose(hasLen, lenIfGood, source.Length - 11);
205+
206+
Debug.Assert(lenIfBad < 0);
207+
return Choose(valid, lenIfGood, lenIfBad);
208+
}
209+
210+
private static int BlindFindFirstZero(ReadOnlySpan<byte> source)
211+
{
212+
// Any vectorization of this routine needs to use non-early termination,
213+
// and instructions that do not vary their completion time on the input.
214+
215+
int pos = -1;
216+
217+
for (int i = source.Length - 1; i >= 0; i--)
218+
{
219+
// pos = source[i] == 0 ? i : pos;
220+
int local = CheckZero(source[i]);
221+
pos = Choose(local, i, pos);
222+
}
223+
224+
return pos;
225+
}
226+
227+
private static int SignStretch(int value)
228+
{
229+
return value >> 31;
230+
}
231+
232+
private static int Choose(int selector, int yes, int no)
233+
{
234+
Debug.Assert((selector | (selector - 1)) == -1);
235+
return (selector & yes) | (~selector & no);
236+
}
237+
238+
private static int CheckZero(int value)
239+
{
240+
// For zero, ~value and value-1 are both all bits set (negative).
241+
// For positive values, ~value is negative and value-1 is positive.
242+
// For negative values except MinValue, ~value is positive and value-1 is negative.
243+
// For MinValue, ~value is positive and value-1 is also positive.
244+
// All together, the only thing that has negative & negative is 0, so stretch the sign bit.
245+
int mask = ~value & (value - 1);
246+
return SignStretch(mask);
247+
}
248+
145249
internal static void PadPkcs1Signature(
146250
HashAlgorithmName hashAlgorithmName,
147251
ReadOnlySpan<byte> source,

src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/EncryptDecrypt.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.Numerics;
57
using Test.Cryptography;
68
using Microsoft.DotNet.XUnitExtensions;
79
using Xunit;
@@ -736,6 +738,119 @@ public void Decrypt_Pkcs1_ErrorsForInvalidPadding(byte[] data)
736738
}
737739
}
738740

741+
[Fact]
742+
public void Decrypt_Pkcs1_BadPadding()
743+
{
744+
if ((PlatformDetection.IsWindows && !PlatformDetection.IsWindows10Version2004OrGreater))
745+
{
746+
return;
747+
}
748+
749+
RSAParameters keyParams = TestData.RSA2048Params;
750+
BigInteger e = new BigInteger(keyParams.Exponent, true, true);
751+
BigInteger n = new BigInteger(keyParams.Modulus, true, true);
752+
byte[] buf = new byte[keyParams.Modulus.Length];
753+
byte[] c = new byte[buf.Length];
754+
755+
buf[1] = 2;
756+
buf.AsSpan(2).Fill(1);
757+
758+
ref byte afterMinPadding = ref buf[10];
759+
ref byte lastByte = ref buf[^1];
760+
afterMinPadding = 0;
761+
762+
using (RSA rsa = RSAFactory.Create(keyParams))
763+
{
764+
RawEncrypt(buf, e, n, c);
765+
// Assert.NoThrow, check that manual padding is coherent
766+
Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1);
767+
768+
// All RSA encryption schemes start with 00, so pick any other number.
769+
//
770+
// If buf > modulus then encrypt should fail, so this
771+
// is the largest legal-but-invalid value to test.
772+
buf[0] = keyParams.Modulus[0];
773+
RawEncrypt(buf, e, n, c);
774+
Assert.ThrowsAny<CryptographicException>(() => Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1));
775+
776+
// Check again with a zero length payload
777+
(afterMinPadding, lastByte) = (lastByte, afterMinPadding);
778+
RawEncrypt(buf, e, n, c);
779+
Assert.ThrowsAny<CryptographicException>(() => Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1));
780+
781+
// Back to valid padding
782+
buf[0] = 0;
783+
(afterMinPadding, lastByte) = (lastByte, afterMinPadding);
784+
RawEncrypt(buf, e, n, c);
785+
Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1);
786+
787+
// This is (sort of) legal for PKCS1 signatures, but not decryption.
788+
buf[1] = 1;
789+
RawEncrypt(buf, e, n, c);
790+
Assert.ThrowsAny<CryptographicException>(() => Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1));
791+
792+
// No RSA PKCS1 padding scheme starts with 00 FF.
793+
buf[1] = 255;
794+
RawEncrypt(buf, e, n, c);
795+
Assert.ThrowsAny<CryptographicException>(() => Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1));
796+
797+
// Check again with a zero length payload
798+
(afterMinPadding, lastByte) = (lastByte, afterMinPadding);
799+
RawEncrypt(buf, e, n, c);
800+
Assert.ThrowsAny<CryptographicException>(() => Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1));
801+
802+
// Back to valid padding
803+
buf[1] = 2;
804+
(afterMinPadding, lastByte) = (lastByte, afterMinPadding);
805+
RawEncrypt(buf, e, n, c);
806+
Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1);
807+
808+
// Try a zero in every possible required padding position
809+
for (int i = 2; i < 10; i++)
810+
{
811+
buf[i] = 0;
812+
813+
RawEncrypt(buf, e, n, c);
814+
Assert.ThrowsAny<CryptographicException>(() => Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1));
815+
816+
// It used to be 1, now it's 2, still not zero.
817+
buf[i] = 2;
818+
}
819+
820+
// Back to valid padding
821+
RawEncrypt(buf, e, n, c);
822+
Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1);
823+
824+
// Make it such that
825+
// "there is no octet with hexadecimal value 0x00 to separate PS from M"
826+
// (RFC 3447 sec 7.2.2, rule 3, third clause)
827+
buf.AsSpan(10).Fill(3);
828+
RawEncrypt(buf, e, n, c);
829+
Assert.ThrowsAny<CryptographicException>(() => Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1));
830+
831+
// Every possible problem, for good measure.
832+
buf[0] = 2;
833+
buf[1] = 0;
834+
buf[4] = 0;
835+
RawEncrypt(buf, e, n, c);
836+
Assert.ThrowsAny<CryptographicException>(() => Decrypt(rsa, c, RSAEncryptionPadding.Pkcs1));
837+
}
838+
839+
static void RawEncrypt(ReadOnlySpan<byte> source, BigInteger e, BigInteger n, Span<byte> destination)
840+
{
841+
BigInteger m = new BigInteger(source, true, true);
842+
BigInteger c = BigInteger.ModPow(m, e, n);
843+
int shift = destination.Length - c.GetByteCount(true);
844+
destination.Slice(0, shift).Clear();
845+
bool wrote = c.TryWriteBytes(destination.Slice(shift), out int written, true, true);
846+
847+
if (!wrote || written + shift != destination.Length)
848+
{
849+
throw new UnreachableException();
850+
}
851+
}
852+
}
853+
739854
public static IEnumerable<object[]> OaepPaddingModes
740855
{
741856
get

src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ static const Entry s_cryptoAppleNative[] =
7070
DllImportEntry(AppleCryptoNative_RsaGenerateKey)
7171
DllImportEntry(AppleCryptoNative_RsaDecryptOaep)
7272
DllImportEntry(AppleCryptoNative_RsaDecryptPkcs)
73+
DllImportEntry(AppleCryptoNative_RsaDecryptRaw)
7374
DllImportEntry(AppleCryptoNative_RsaEncryptOaep)
7475
DllImportEntry(AppleCryptoNative_RsaEncryptPkcs)
7576
DllImportEntry(AppleCryptoNative_RsaSignaturePrimitive)

0 commit comments

Comments
 (0)