Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Enable RSA-OAEP(SHA-2) and RSA-PSS on Unix systems #27394

Merged
merged 15 commits into from
Feb 28, 2018

Conversation

bartonjs
Copy link
Member

This change provides an implementation of the OAEP padding algorithm and
the PSS encoding and verification routines in managed code. On the platforms
where we currently lack support for SHA-2-based OAEP and/or PSS the
managed implementation will be used in conjunction with the native layer in
a pre-padded operational context.

The suite of tests which were added uncovered other bugs which are being
addressed in this change, as well. Mainly that RSACng and
RSASecurityTransforms both failed at encrypting zero-length data. To solve
that problem the RSA padding class can build PKCS#1 encryption padding,
but since the native layers are capable of correctly decrypting the payloads
no unpadding code is needed at this time.

Fixes #2522
Fixes #2523
Fixes #27120

This change provides an implementation of the OAEP padding algorithm and
the PSS encoding and verification routines in managed code. On the platforms
where we currently lack support for SHA-2-based OAEP and/or PSS the
managed implementation will be used in conjunction with the native layer in
a pre-padded operational context.

The suite of tests which were added uncovered other bugs which are being
addressed in this change, as well.  Mainly that RSACng and
RSASecurityTransforms both failed at encrypting zero-length data.  To solve
that problem the RSA padding class can build PKCS#1 encryption padding,
but since the native layers are capable of correctly decrypting the payloads
no unpadding code is needed at this time.
@bartonjs bartonjs added this to the 2.1.0 milestone Feb 23, 2018
@bartonjs bartonjs self-assigned this Feb 23, 2018
@@ -61,10 +68,13 @@ internal static unsafe bool TryCFWriteData(SafeCFDataHandle cfData, Span<byte> d
return false;
}

byte* dataBytes = CFDataGetBytePtr(cfData);
fixed (byte* destinationPtr = &MemoryMarshal.GetReference(destination))
if (length > 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but how about moving the above if block into this one? If length <= 0, then destination.Length will never be < length.


private static int GetHashSizeInBytes(HashAlgorithmName hashAlgorithm)
{
lock (s_hashSizes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use ConcurrentDictionary instead?

return buf;
if (buf != null)
{
Array.Clear(buf, 0, rsaSize);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are you deciding when to use Array.Clear vs CryptographicOperation.ZeroMemory?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honest: The parts I wrote before rebasing are Array.Clear, the parts after are ZeroMemory... unless they're in a file that gets built for something other than netcoreapp.

Less honest: Just the !netcoreapp part, but with a bad self-analysis.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normalized to ZeroMemory

CheckReturn(returnValue);
if (!encrypted || bytesWritten != buf.Length)
{
Debug.Fail("TryEncrypt behaved unexpectedly");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there are multiple conditions that could have led to this failure, consider putting details into the assert, e.g.

Debug.Fail($"TryEncrypt behaved unexpectedly: {nameof(encrypt)}=={encrypted}, {nameof(bytesWritten)}=={bytesWritten}, {nameof(buf.Length)}=={buf.Length}");

Interop.AppleCrypto.PAL_HashAlgorithm palAlgId =
PalAlgorithmFromAlgorithmName(hashAlgorithm, out expectedSize);
// A signature will always be the keysize (in ceiling-bytes) in length.
byte[] output = new byte[(KeySize + 7) / 8];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This math appears a lot... I know you'd proposed a public API for it, but short of that, how about an internal/private one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a utility method on RsaPaddingProcessor for now.


internal static RsaPaddingProcessor OpenProcessor(HashAlgorithmName hashAlgorithmName)
{
lock (s_lookup)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same ConcurrentDictionary question. It'd be nice to avoid a global lock like this for already initialized values.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed both to ConcurrentDictionary.

}

// This is a copy of RandomNumberGeneratorImplementation.GetNonZeroBytes, but adapted
// to the object-less RandomNumberGenerator.Fill.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have a public RandomNumberGenerator.FillNonZeroBytes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only algorithm that I know of which needs it is this one. Absent valid customer scenarios for it I don't think we should bother.

return kErrorBadInput;
}

assert(func != nullptr);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why is this an assert rather than verified in the above return kErrorBadInput check?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above checks are for values that came across the P/Invoke boundary, so it's an API constraint enforcement. func has to be passed in via a non-C-static function in this library, so it's an internal consistency check instead of an API constraint

@bartonjs
Copy link
Member Author

Yay, the last commit fixed the 10.12 problem. (Apparently in 10.12 you couldn't call SecKeyCreateDecryptedData with a public key, which mostly makes sense. And in 10.13 you can, which is where I landed).

And since I have the tests that are for bugfixes in CoreFx RSACng suppressed on NetFx now I'll push that and the code review feedback and hopefully be green.

@bartonjs
Copy link
Member Author

@dotnet-bot Test OSX x64 Debug Build please (build never finished?)

@bartonjs
Copy link
Member Author

@dotnet-bot Test Linux x64 Release Build please (System.Xml.XmlSchemaSet.Tests segfault on Ubuntu 16.04)

Copy link
Member

@GrabYourPitchforks GrabYourPitchforks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only minor feedback.

}
finally
{
CryptographicOperations.ZeroMemory(paddedMessage);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this particular routine is for zero-length data so this isn't critical, but we should be in the habit of pinning buffers if we're going to zero them. This has caused a bit of ugliness in the code in previous reviews. I wonder if it makes sense to have an internal crypto-specific MemoryPool<byte> where the returned Memory<byte> instances are guaranteed to be pinned and where the Return function is guaranteed to call ZeroMemory?

(Not necessary for this review - just thoughts for the future.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need to be careful with it, since we need to make sure it's disposable (for using-goodness) and maybe Finalizable (to avoid memory leaks).

ArrayPooled, eventually, tends to be Gen2, so it doesn't move. But if we're doing guaranteed pin we need to be careful.

@@ -40,6 +64,11 @@ public override byte[] SignHash(byte[] hash, HashAlgorithmName hashAlgorithm, RS

using (SafeNCryptKeyHandle keyHandle = GetDuplicatedKeyHandle())
{
if (hash.Length != GetHashSizeInBytes(hashAlgorithm))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: move this up to the top of the method with the other parameter checking (before the using statement) if possible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally had it down this far so that we more closely emulated the behavior of the OS being the thing to reject the value. That said, I can't think of a reason that this needs to stay this way. So I'll move it.

}
finally
{
tmp.Clear();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZeroMemory


internal static int BytesRequiredForBitCount(int keySizeInBits)
{
return (keySizeInBits + 7) / 8;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return (int)(((uint)keySizeInBits + 7) / 8);, just in case somebody passes in an absurdly large value.

ReadOnlySpan<byte> source,
Span<byte> destination)
{
// https://tools.ietf.org/html/rfc3447#section-7.1.2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7.1.1

Mgf1(hasher, db, seedMask);

// 2(h)
for (int i = 0; i < seedMask.Length; i++)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Split this out into a helper method. I want to add a void Span.Xor(Span<byte> a, ReadOnlySpan<byte> b) static method to BCL in the future, and it'll help find callers if this is already split out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

bool yIsZero = y == 0;
bool separatorMadeSense = separatorPos < dbMask.Length;

bool shouldContinue = lHashMatches & yIsZero & separatorMadeSense;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment that intentionally using non-short-circuiting operators.


Span<byte> message = dbMask.Slice(separatorPos + 1);

if (message.Length <= destination.Length)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this pattern would encourage timing attacks which disclose the decrypted message length. Consider this: the RSA decryption consumer knows that a well-formed decrypted message is n bytes in length. When the consumer calls RSA-decrypt they pass a buffer of length n as the destination. The adversary doesn't necessarily know n, perhaps because it's a computed value instead of a hardcoded value. If the adversary can then generate encrypted messages which are well-formed but overlong, they can time how long it takes the server to reject those messages, and eventually they'll see a spike in the amount of time taken, which discloses n.

I don't know if this is a legitimate concern in practice, since there may exist other signals which disclose n for any arbitrary message. Just food for thought.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps. I took my cues from CNG on this one: they accept accurate destination sizes, not just destinations at least as big as n.

ArrayPool<byte>.Shared.Return(dbMaskRented);
}

internal bool VerifyPss(ReadOnlySpan<byte> mHash, ReadOnlySpan<byte> em, int keySize)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// https://tools.ietf.org/html/rfc3447#section-9.1.2

@@ -170,12 +170,18 @@ public override int KeySize
public override string SignatureAlgorithm => "http://www.w3.org/2000/09/xmldsig#rsa-sha1";

public override byte[] SignData(Stream data, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) =>
padding == null ? throw new ArgumentNullException(nameof(padding)) :
padding != RSASignaturePadding.Pkcs1 ? throw PaddingModeNotSupported() :
_impl.SignData(data, hashAlgorithm, padding);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not crazy about the => into the nested ternary conditionals. Would be more slightly readable as a bracketed declaration:

public override byte[] SignData(Stream data, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
{
             if (padding == null)
                 throw new ArgumentNullException(nameof(padding));
             if (padding != RSASignaturePadding.Pkcs1)
                 throw PaddingModeNotSupported();
             return _impl.SignData(data, hashAlgorithm, padding);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, me neither, but it matched existing style.

int rsaSize = (KeySize + 7) / 8;
const int OaepSha1Overhead = 20 + 20 + 2;

// Normalize the Windows 7 and Windows 8.1+ exception
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move this comment to above the if (foaep).

}
}

// https://tools.ietf.org/html/rfc3447#appendix-B.2.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'm having trouble matching up what this function does and the steps for MGF1 in RFC3447. They're structured very differently, which makes the correctness of this hard for me to verify.

Copy link
Member Author

@bartonjs bartonjs Feb 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this one is sort of weird to write in the order they wrote it in the RFC.

  1. If maskLen > 2^32 hLen, output "mask too long" and stop.

We know that's not the case, because maskLen was specified as mask.Length

  1. For counter from 0 to \ceil (maskLen / hLen) - 1, do the
    following:

It wants us to hash the minimum number of times we need to to produce maskLen bytes.

a. Convert counter to an octet string C of length 4 octets (see
Section 4.1):

       C = I2OSP (counter, 4) .

That means "make a 4 byte big endian value for counter". Aka BinaryPrimitives.WriteInt32BigEndian(bigEndianCount, count);

b. Concatenate the hash of the seed mgfSeed and C to the octet
string T:

       T = T || Hash(mgfSeed || C) .

Let's scope it in to Hash(mgfSeed || C):

hasher.AppendData(mgfSeed);
BinaryPrimitives.WriteInt32BigEndian(bigEndianCount, count);
hasher.AppendData(bigEndianCount);

Let's skip ahead:

  1. Output the leading maskLen octets of T as the octet string mask.

Okay, so rather than build up a big T we can just write this out to the destination because we don't use the collective T value.

The only problem we have is when the last hash is bigger than it needs to be, so we need to write it to a temporary location and then only copy over the remaining data.

Now that we've seen how we got to the end, we look back at

  1. Let T be the empty octet string.

And conclude that it was required for the formula, but not the implementation.

// 1(a) does not apply, we do not allow custom label values.

// 1(b)
if (source.Length > maxInput)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not certain I understand what's happening here for 1B. What we appear to be doing here is based on the assumption that mLen is correct, so the caller knows the destination length? 1C I get.

// Source = C
// Destination = M
int mLen = destination.Length;
int min_k = m_Len + 2*_hLen + 2; // from mLen <= k - 2hLen - 2

// 1(c)
// if (min_k < 2*_hLen + 2) throw cryptoException
// => if (destination.Length +2*_hLen + 2 < 2*_hLen + 2) 
// => if (destination.Length < 0)
// => always false because span.Length is always nonnegative.

// 1 (b)
// if (source.Length != k) throw cryptoException
// => if (source.Length < min_k) 
// => if (source.Length < destination.Length + 2*_hLen + 2)
// ... something here?
// => if (source.Length > destination.Length - 2*_hLen - 2)

I'm missing how to get to the assertion that your maxInput statement is correct.

When I flip source/destination the math makes sense, but then nothing I don't understand the parameters. That would mean that source holds the message and destination holds the ciphertext, so we would be doing the inverse of how the RFC defined the function.

// source = M
// destination = C 
int mLen = source.Length
int min_k = mLen + 2hlen +2 // from mLen <= k - 2hLen - 2

// 1(c)
// if (min_k < 2*_hLen + 2) throw cryptoException
// => if (source.Length +2*_hLen + 2 < 2*_hLen + 2) 
// => if (source.Length < 0)
// => always false because span.Length is always nonnegative.

// 1 (b)
// if (destination.Length != k) throw cryptoexception
// => if (destination.Length < min_k)
// => if (destination.Length < source.Length + 2hlen + 2)
// => if (source.Length > destination.Length - 2hlen - 2

Is that the case, or am I misunderstanding something fundamental here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, I was comparing this to the argument validation to the 7.1.2 section that you mentioned at the top. No wonder it didn't link up. Serves me right for trying to understand it line-by-line and not looking ahead :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants