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

SignedCms: Improve NetFx compat for SignedCms wrapping EnvelopedCms#30405

Merged
bartonjs merged 3 commits intodotnet:masterfrom
bartonjs:signedcms_over_envelopedcms
Jun 15, 2018
Merged

SignedCms: Improve NetFx compat for SignedCms wrapping EnvelopedCms#30405
bartonjs merged 3 commits intodotnet:masterfrom
bartonjs:signedcms_over_envelopedcms

Conversation

@bartonjs
Copy link
Member

The SignedCms implementation on .NET Framework writes using the older PKCS7 encoding when writing documents only signed with IssuerAndSerialNumber as the signer identifier type.

As far as SignedData is concerned, the only difference between PKCS7 and CMS is that when the content type is not the OID for "data" (plain bytes) CMS wraps the bytes in an OCTET STRING, PKCS7 does not. Unfortunately, this means that .NET Core cannot read those messages from .NET Framework.

This change adds 4 more static documents to the test suite, each of NetFx and CoreFx signing an EnvelopedCms with each of IssuerAndSerialNumber and SubjectKeyIdentifier. Before the product code changes the only test that failed was CoreFx reading the NetFx-prepared document. Now CoreFx is capable of reading older PKCS7 messages.

Additionally, this change fixes a CMS conformance issue, where the content-type attribute should be added to all signers when the document content type is not "data".

In NetFX if a SignedCms is created using only CmsSigners with IssuerAndSerial as
the signer identifier type, the document gets encoded using the older PKCS7
structural definition instead of the newer CMS one.

RFC 5652 has a long section (5.2.1) on how to read these documents compatibly.

Since the defaults in SignedCms / CmsSigner are the PKCS7 behavior, not
reading it means that Signed(Enveloped) documents from NetFX cannot be read.
@bartonjs bartonjs force-pushed the signedcms_over_envelopedcms branch from 2e9c258 to ab14272 Compare June 14, 2018 21:37

rented = ArrayPool<byte>.Shared.Rent(wrappedContent.Length);

if (!reader.TryCopyOctetStringBytes(rented, out bytesWritten))
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity: When is TryGetPrimitiveOctetStringBytes going to fail and TryCopyOctetStringBytes will succeed?

Copy link
Member

Choose a reason for hiding this comment

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

Is it the primitive vs non-primitive?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep. TryGetPrimitive returns false if the tag is correct, but the Primitive vs Constructed is not. TryCopy can handle both.


return rented.AsSpan(0, bytesWritten).ToArray();
}
catch (Exception) when (contentType != Oids.Pkcs7Data)
Copy link
Member

Choose a reason for hiding this comment

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

Should this be CryptographicException?

Copy link
Member Author

Choose a reason for hiding this comment

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

There shouldn't be any other errors there, but the guidance from the RFC is

... If the
implementation is unable to ASN.1 decode the SignedData type using
the CMS SignedData encapContentInfo eContent OCTET STRING syntax,
then the implementation MAY attempt to decode the SignedData type
using the PKCS #7 SignedData contentInfo content ANY syntax and
compute the message digest accordingly.

So I went with "if anything goes wrong, fall back".

Copy link
Member

Choose a reason for hiding this comment

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

fair enough

if (!Detached)
{
_signedData.EncapContentInfo.Content = content;
using (AsnWriter writer = new AsnWriter(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.

Is this always writing empty octet string? (_hasData == false) if so we could cache this value

Copy link
Member Author

Choose a reason for hiding this comment

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

!_hasData here means "we're the first signer" and thus "we have to build the 'document data' in addition to the 'signer data'". It doesn't mean that content is empty.

Copy link
Member

Choose a reason for hiding this comment

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

I presume double wrapping (contentType != Pks7Data) is intended here?

Copy link
Member Author

@bartonjs bartonjs Jun 14, 2018

Choose a reason for hiding this comment

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

Well, it's single wrapping. This is putting back the OCTET STRING that got lost by changing from [OctetString] to [AnyValue] in the serializer type. The current CMS RFCs say to always write the OCTET STRING.

Rather than match what NetFx writes I just went with "be capable of reading what NetFx writes", but since the CMS update was from 1999, it seems that the "new" format should be pretty well supported by readers.

[Fact]
public static void CheckSignedEncrypted_IssuerSerial_FromNetFx()
{
CheckSignedEncrypted(
Copy link
Member

Choose a reason for hiding this comment

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

Those can be collapsed into a single theory with MemberData - I'm fine either way though

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, but when passing in byte[] that produces test names that are hard to understand in failure messages and result logs.

Copy link
Member

@krwq krwq left a comment

Choose a reason for hiding this comment

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

LGTM

// Due to the way the underlying Windows CMS API behaves a copy of the content
// bytes will be held separate once the content is "bound" (first signature or decode)
private ReadOnlyMemory<byte>? _heldContent;
// During decode, if the PKCS#7 fallback for a missing OCTET STRING is present, this
Copy link
Member

Choose a reason for hiding this comment

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

Nit: blank line above this?

@bartonjs bartonjs merged commit 70f1b80 into dotnet:master Jun 15, 2018
@bartonjs bartonjs deleted the signedcms_over_envelopedcms branch June 15, 2018 15:19
@karelz karelz added this to the 3.0 milestone Jul 8, 2018
@bartonjs bartonjs removed their assignment Nov 23, 2018
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