Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Allow EnvelopedCms to round-trip correctly cross platform when ContentInfo is not Pkcs7Data#29926

Merged
bartonjs merged 10 commits intodotnet:masterfrom
krwq:fix_29825_ecms_xplat_roundtripping
Jun 8, 2018
Merged

Allow EnvelopedCms to round-trip correctly cross platform when ContentInfo is not Pkcs7Data#29926
bartonjs merged 10 commits intodotnet:masterfrom
krwq:fix_29825_ecms_xplat_roundtripping

Conversation

@krwq
Copy link
Member

@krwq krwq commented May 25, 2018

Fixes https://github.com/dotnet/corefx/issues/29825

This might need to wait until @bartonjs is back from vacation.

We should consider this for 2.1.1 because documents encoded on non-Windows platforms with EnvelopedCms which have ContentInfo Oid different than 1.2.840.113549.1.7.1 will not roundtrip correctly cross platform. Any currently existing document encoded in such way will have additional DER "Sequence" prefix (0x30, <length>) after decryption

…tInfo type is not 1.2.840.113549.1.7.1 (Pkcs7Data)
using (AsnWriter writer = new AsnWriter(AsnEncodingRules.DER))
{
writer.PushSequence();
AsnReader reader = new AsnReader(decrypted, AsnEncodingRules.DER);
Copy link
Member

Choose a reason for hiding this comment

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

Most of CMS says that it's BER, meaning indefinite length encodings are permitted. This may be overly restrictive. (Same with the writer, or it will validate the call to WriteEncodedValue)

Copy link
Member Author

Choose a reason for hiding this comment

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

@bartonjs I did it this way because Windows appends DER prefix and I wasn't sure how to enforce length definite Sequence tag. I was thinking about octet string instead of this and replacing the first byte to 0x30 to avoid validation - I haven't checked what BER produces by default though so I presume you're right about being to restrictive (I also thought this is always DER).

{
using (MemoryStream memoryStream = new MemoryStream())
{
using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
Copy link
Member

Choose a reason for hiding this comment

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

There's no need to drag CryptoStream and System.IO into this, because you only have one call to Write. Just build the data and call encryptor.OneShot.

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 did it this way to avoid building intermediate buffers - this is a modified copy of OneShot method which tries to not unnecessarily allocate. I thought the purpose of CryptoStream was to feed it with data I want to encrypt/decrypt which I'm doing here.

To build data I'll also need some equivalent of MemoryStream so not sure what the ask is. Unless you mean something else?

Copy link
Member Author

Choose a reason for hiding this comment

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

@bartonjs - missed the part about "one call to Write" - currently it's reading the sequence so there is more than one call (in most of the cases there will be 1 element but technically there could be more)

Copy link
Member

Choose a reason for hiding this comment

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

CryptoStream and MemoryStream both will create a fair amount of intermediate buffers. But since the call to Write is in a loop I can believe that this code is easier to understand.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, it looks like this is just trying to do reader.PeekContentBytes(). And I think Windows might not care that the outer tag is SEQUENCE.

Looks like this passes on Windows:

ContentInfo content = new ContentInfo(
    new Oid(Oids.Pkcs7Enveloped),
    "0403010203".HexToByteArray());

EnvelopedCms cms = new EnvelopedCms(content);

using (X509Certificate2 cert = Certificates.RSAKeyTransferCapi1.TryGetCertificateWithPrivateKey())
{
    cms.Encrypt(new CmsRecipient(cert));
    byte[] temp = cms.Encode();

    // Double checking.
    cms = new EnvelopedCms();
    cms.Decode(temp);
    cms.Decrypt(new X509Certificate2Collection(cert));
}

Assert.Equal("3003010203", cms.ContentInfo.Content.ByteArrayToHex());

I don't know that I agree with it passing, but since we're making a compat fix, we should be compatible.

Copy link
Member

@bartonjs bartonjs left a comment

Choose a reason for hiding this comment

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

I think this will fail when told that 04 03 01 02 03 is nested enveloped content. Please add a compatibility test for that.

048103010203 also seems like a good test case (=> 3003010203, probably).

Windows doesn't seem to be happy to create a wrapped thing with the outer value having an indefinite encoding, but I don't know that we need to fail as aggressively as Windows does.

One final test case to consider is enveloping something using an OID that isn't from the PKCS7 set. Like, oh, "0.0". Just to make sure that Windows is applying "not PKCS7-data" vs "is PKCS7-{enveloped/signed/etc}"

@danmoseley
Copy link
Member

@krwq if you believe this shoudl be considered for servicing please tag the issue (not PR) with servicing-consider-2.1.x. (It missed 2.1.1 I think)

@krwq krwq added Servicing-consider Issue for next servicing release review and removed Servicing-consider Issue for next servicing release review labels Jun 4, 2018
Assert.Equal<byte>(content, contentInfo.Content);
}

[Fact]
Copy link
Member Author

@krwq krwq Jun 7, 2018

Choose a reason for hiding this comment

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

This change is unrelated but I noticed this test does not run (Xunit doesn't run static tests in abstract class)

}

[Fact]
public static void DecryptUsingCertificateWithSameSubjectKeyIdentifierButDifferentKeyPair()
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 move this to a different place as it is not running right now (two conflicting PRs happened at similar time but they did not cause merge conflict)

@krwq
Copy link
Member Author

krwq commented Jun 8, 2018

@bartonjs the only difference between Windows & Manged implementation is indefinite length encoding - Windows doesn't like it - on managed implementation I allowed it as there is no reason not to.

decrypted[0] = 0x30;
}
}
decrypted = GetAsnSequenceWithContentNoValidation(decrypted);
Copy link
Member Author

Choose a reason for hiding this comment

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

seems like we were always going into the catch path before so this got slightly smaller

@krwq
Copy link
Member Author

krwq commented Jun 8, 2018

@bartonjs there is a difference in behavior when encrypting Pkcs7Enveloped and passing an empty array.

NetFX does not allow it, netcoreapp Windows implementation allows it and managed implementation does allow it now (before this PR it did not).

I have mixed feelings if empty array should be valid or not in this context

{
private bool _useExplicitPrivateKey;
public static bool SupportsCngCertificates { get; } = (!PlatformDetection.IsFullFramework || PlatformDetection.IsNetfx462OrNewer);
public static bool SupportsIndefiniteLengthEncoding { get; } = !PlatformDetection.IsWindows;
Copy link
Member

Choose a reason for hiding this comment

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

Having talked in person about this, I think it makes sense for us to just make this work on Windows. (Or make it not work on Unix, which is complicated :)). Either in this issue or a separate one is fine.

Something like if we get back CRYPT_E_UNEXPECTED_ENCODING (which is what I'm expecting) from CryptMsgControl then we try again by running the value through WriteOctetString(berReader.PeekContentBytes()).


if (encodedContent.Length > 0)
{
// Windows will throw if it encounters indefinite length encoding.
Copy link
Member Author

Choose a reason for hiding this comment

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

also encodedContent.Length > 0 is probably why empty array works on Windows 😄

{
// Windows will throw if it encounters indefinite length encoding.
// Let's reencode if that is the case
ReencodeIfUsingIndefiniteLengthEncodingOnOuterStructure(ref encodedContent);
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this method isn't very hygenic; but we should fix it while we're here (and not make it worse).

The array from line 36 (id-data) is a copy, we should clear it and be nice to the user data. If Reencode makes a copy then we need to clear that one, too. But if we hit line 42 and don't rewrite it via reencode then we mustn't copy it, because the user still has access to that array. (ContentInfo.Content returns the raw array, not a clone).

}
finally
{
Array.Clear(oldContent, 0, oldContent.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 know I said (in person) to switch to CryptographicOperations.ZeroMemory; but this file probably compiles under netstandard, which doesn't have that API. So Array.Clear is fine.

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.

Variation of EnvelopedCmsTests.Tests.DecryptTests.Decrypt_SignedWithinEnveloped fails cross platform

4 participants