Allow EnvelopedCms to round-trip correctly cross platform when ContentInfo is not Pkcs7Data#29926
Conversation
…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); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
@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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
@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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
bartonjs
left a comment
There was a problem hiding this comment.
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}"
|
@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) |
| Assert.Equal<byte>(content, contentInfo.Content); | ||
| } | ||
|
|
||
| [Fact] |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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)
|
@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); |
There was a problem hiding this comment.
seems like we were always going into the catch path before so this got slightly smaller
|
@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; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
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.1will not roundtrip correctly cross platform. Any currently existing document encoded in such way will have additional DER "Sequence" prefix (0x30,<length>) after decryption