Add SignerIdentifierType.NoSignature support to SignedCms
authorJeremy Barton <jbarton@microsoft.com>
Wed, 12 Sep 2018 22:36:08 +0000 (15:36 -0700)
committerGitHub <noreply@github.com>
Wed, 12 Sep 2018 22:36:08 +0000 (15:36 -0700)
This also changes the zero-argument ComputeSignature and moves the PNSE
to later in the flow, since it is successful when the document was in implicit
NoSignature mode.

Commit migrated from https://github.com/dotnet/corefx/commit/8cfcb3d75922db94d822080d76903c6316fc0665

src/libraries/Common/src/System/Security/Cryptography/Oids.cs
src/libraries/System.Security.Cryptography.Pkcs/src/Resources/Strings.resx
src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/CmsSigner.cs
src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignedCms.cs
src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignerInfo.cs
src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs
src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.cs

index 8b435b7..ffc7759 100644 (file)
@@ -78,6 +78,9 @@ namespace System.Security.Cryptography
 
         internal const string Mgf1 = "1.2.840.113549.1.1.8";
 
+        // PKCS#7
+        internal const string NoSignature = "1.3.6.1.5.5.7.6.2";
+
         // X500 Names
         internal const string CommonName = "2.5.4.3";
         internal const string Organization = "2.5.4.10";
index cbfe351..bfeac02 100644 (file)
   <data name="Cryptography_Cms_NoSignerCert" xml:space="preserve">
     <value>No signer certificate was provided. This platform does not implement the certificate picker UI.</value>
   </data>
+  <data name="Cryptography_Cms_NoSignerCertSilent" xml:space="preserve">
+    <value>No signer certificate was provided.</value>
+  </data>
   <data name="Cryptography_Cms_NoSignerAtIndex" xml:space="preserve">
     <value>The signed cryptographic message does not have a signer for the specified signer index.</value>
   </data>
   <data name="Cryptography_Cms_Sign_Empty_Content" xml:space="preserve">
     <value>Cannot create CMS signature for empty content.</value>
   </data>
+  <data name="Cryptography_Cms_Sign_No_Signature_First_Signer" xml:space="preserve">
+    <value>CmsSigner has to be the first signer with NoSignature.</value>
+  </data>
   <data name="Cryptography_Cms_SignerNotFound" xml:space="preserve">
     <value>Cannot find the original signer.</value>
   </data>
index 334d77f..1111cd0 100644 (file)
@@ -224,14 +224,27 @@ namespace System.Security.Cryptography.Pkcs
                 newSignerInfo.UnsignedAttributes = PkcsHelpers.NormalizeAttributeSet(attrs.ToArray());
             }
 
-            bool signed = CmsSignature.Sign(
-                dataHash,
-                hashAlgorithmName,
-                Certificate,
-                PrivateKey,
-                silent,
-                out Oid signatureAlgorithm,
-                out ReadOnlyMemory<byte> signatureValue);
+            bool signed;
+            Oid signatureAlgorithm;
+            ReadOnlyMemory<byte> signatureValue;
+
+            if (SignerIdentifierType == SubjectIdentifierType.NoSignature)
+            {
+                signatureAlgorithm = new Oid(Oids.NoSignature, null);
+                signatureValue = dataHash;
+                signed = true;
+            }
+            else
+            {
+                signed = CmsSignature.Sign(
+                    dataHash,
+                    hashAlgorithmName,
+                    Certificate,
+                    PrivateKey,
+                    silent,
+                    out signatureAlgorithm,
+                    out signatureValue);
+            }
 
             if (!signed)
             {
index e656b9f..8b71f7c 100644 (file)
@@ -17,6 +17,7 @@ namespace System.Security.Cryptography.Pkcs
     {
         private SignedDataAsn _signedData;
         private bool _hasData;
+        private SubjectIdentifierType _signerIdentifierType;
 
         // A defensive copy of the relevant portions of the data to Decode
         private Memory<byte> _heldData;
@@ -45,12 +46,22 @@ namespace System.Security.Cryptography.Pkcs
             if (contentInfo.Content == null)
                 throw new ArgumentNullException("contentInfo.Content");
 
-            // signerIdentifierType is ignored.
-            // In .NET Framework it is used for the signer type of a prompt-for-certificate signer.
-            // In .NET Core we don't support prompting.
-            //
-            // .NET Framework turned any unknown value into IssuerAndSerialNumber, so no exceptions
-            // are required, either.
+            // Normalize the subject identifier type the same way as .NET Framework.
+            // This value is only used in the zero-argument ComputeSignature overload,
+            // where it controls whether it succeeds (NoSignature) or throws (anything else),
+            // but in case it ever applies to anything else, make sure we're storing it
+            // faithfully.
+            switch (signerIdentifierType)
+            {
+                case SubjectIdentifierType.NoSignature:
+                case SubjectIdentifierType.IssuerAndSerialNumber:
+                case SubjectIdentifierType.SubjectKeyIdentifier:
+                    _signerIdentifierType = signerIdentifierType;
+                    break;
+                default:
+                    _signerIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber;
+                    break;
+            }
 
             ContentInfo = contentInfo;
             Detached = detached;
@@ -227,10 +238,7 @@ namespace System.Security.Cryptography.Pkcs
             return wrappedContent;
         }
 
-        public void ComputeSignature()
-        {
-            throw new PlatformNotSupportedException(SR.Cryptography_Cms_NoSignerCert);
-        }
+        public void ComputeSignature() => ComputeSignature(new CmsSigner(_signerIdentifierType), true);
 
         public void ComputeSignature(CmsSigner signer) => ComputeSignature(signer, true);
 
@@ -249,6 +257,32 @@ namespace System.Security.Cryptography.Pkcs
                 throw new CryptographicException(SR.Cryptography_Cms_Sign_Empty_Content);
             }
             
+            if (_hasData && signer.SignerIdentifierType == SubjectIdentifierType.NoSignature)
+            {
+                // Even if all signers have been removed, throw if doing a NoSignature signature
+                // on a loaded (from file, or from first signature) document.
+                //
+                // This matches the NetFX behavior.
+                throw new CryptographicException(SR.Cryptography_Cms_Sign_No_Signature_First_Signer);
+            }
+
+            if (signer.Certificate == null && signer.SignerIdentifierType != SubjectIdentifierType.NoSignature)
+            {
+                if (silent)
+                {
+                    // NetFX compatibility, silent disallows prompting, so throws InvalidOperationException
+                    // in this state.
+                    //
+                    // The message is different than on NetFX, because the resource string was for
+                    // enveloped CMS recipients (and the other site which would use that resource
+                    // is unreachable code due to CmsRecipient's ctor guarding against null certificates)
+                    throw new InvalidOperationException(SR.Cryptography_Cms_NoSignerCertSilent);
+                }
+
+                // Otherwise, PNSE. .NET Core doesn't support launching the cert picker UI.
+                throw new PlatformNotSupportedException(SR.Cryptography_Cms_NoSignerCert);
+            }
+
             // If we had content already, use that now.
             // (The second signer doesn't inherit edits to signedCms.ContentInfo.Content)
             ReadOnlyMemory<byte> content = _heldContent ?? ContentInfo.Content;
index 70663b9..7d3338c 100644 (file)
@@ -473,6 +473,11 @@ namespace System.Security.Cryptography.Pkcs
 
         public void CheckHash()
         {
+            if (_signatureAlgorithm.Value != Oids.NoSignature)
+            {
+                throw new CryptographicException(SR.Cryptography_Pkcs_InvalidSignatureParameters);
+            }
+
             if (!CheckHash(compatMode: false) && !CheckHash(compatMode: true))
             {
                 throw new CryptographicException(SR.Cryptography_BadSignature);
index 7f21700..7c3e07c 100644 (file)
@@ -378,6 +378,79 @@ namespace System.Security.Cryptography.Pkcs.Tests
         }
 
         [Theory]
+        [InlineData(SubjectIdentifierType.Unknown)]
+        [InlineData(SubjectIdentifierType.IssuerAndSerialNumber)]
+        [InlineData(SubjectIdentifierType.SubjectKeyIdentifier)]
+        [InlineData((SubjectIdentifierType)76)]
+        public static void ZeroArgComputeSignature(SubjectIdentifierType identifierType)
+        {
+            ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 });
+            SignedCms cms = new SignedCms(identifierType, contentInfo);
+
+            Assert.Throws<InvalidOperationException>(() => cms.ComputeSignature());
+
+            cms = new SignedCms(identifierType, contentInfo, detached: true);
+            Assert.Throws<InvalidOperationException>(() => cms.ComputeSignature());
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void ZeroArgComputeSignature_NoSignature(bool detached)
+        {
+            ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 });
+            SignedCms cms = new SignedCms(SubjectIdentifierType.NoSignature, contentInfo, detached);
+
+            if (PlatformDetection.IsFullFramework)
+            {
+                Assert.Throws<NullReferenceException>(() => cms.ComputeSignature());
+            }
+            else
+            {
+                cms.ComputeSignature();
+
+                SignerInfoCollection signers = cms.SignerInfos;
+                Assert.Equal(1, signers.Count);
+                Assert.Equal(SubjectIdentifierType.NoSignature, signers[0].SignerIdentifier.Type);
+                cms.CheckHash();
+                Assert.Throws<CryptographicException>(() => cms.CheckSignature(true));
+            }
+        }
+
+        [Theory]
+        [InlineData(SubjectIdentifierType.IssuerAndSerialNumber, false)]
+        [InlineData(SubjectIdentifierType.IssuerAndSerialNumber, true)]
+        [InlineData(SubjectIdentifierType.SubjectKeyIdentifier, false)]
+        [InlineData(SubjectIdentifierType.SubjectKeyIdentifier, true)]
+        // NoSignature is a different test, because it succeeds (CoreFX) or fails differently (NetFX)
+        public static void SignSilentWithNoCertificate(SubjectIdentifierType identifierType, bool detached)
+        {
+            ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 });
+            SignedCms cms = new SignedCms(contentInfo, detached);
+
+            Assert.Throws<InvalidOperationException>(
+                () => cms.ComputeSignature(new CmsSigner(identifierType), silent: true));
+        }
+
+        [Theory]
+        [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+        [InlineData(SubjectIdentifierType.IssuerAndSerialNumber, false)]
+        [InlineData(SubjectIdentifierType.IssuerAndSerialNumber, true)]
+        [InlineData(SubjectIdentifierType.SubjectKeyIdentifier, false)]
+        [InlineData(SubjectIdentifierType.SubjectKeyIdentifier, true)]
+        // NoSignature is a different test, because it succeeds (CoreFX) or fails differently (NetFX)
+        public static void SignNoisyWithNoCertificate_NotSupported(
+            SubjectIdentifierType identifierType,
+            bool detached)
+        {
+            ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 });
+            SignedCms cms = new SignedCms(contentInfo, detached);
+
+            Assert.Throws<PlatformNotSupportedException>(
+                () => cms.ComputeSignature(new CmsSigner(identifierType), silent: false));
+        }
+
+        [Theory]
         [InlineData(SubjectIdentifierType.IssuerAndSerialNumber, false)]
         [InlineData(SubjectIdentifierType.IssuerAndSerialNumber, true)]
         [InlineData(SubjectIdentifierType.SubjectKeyIdentifier, false)]
@@ -600,6 +673,236 @@ namespace System.Security.Cryptography.Pkcs.Tests
         [Theory]
         [InlineData(false, false)]
         [InlineData(false, true)]
+        [InlineData(true, false)]
+        [InlineData(true, true)]
+        public static void AddFirstSigner_NoSignature(bool detached, bool includeExtraCert)
+        {
+            ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 });
+            SignedCms cms = new SignedCms(contentInfo, detached);
+            X509Certificate2Collection certs;
+
+            // A certificate shouldn't really be required here, but on .NET Framework
+            // it will encounter throw a NullReferenceException.
+            using (X509Certificate2 cert = Certificates.RSAKeyTransferCapi1.GetCertificate())
+            using (X509Certificate2 cert2 = Certificates.DHKeyAgree1.GetCertificate())
+            {
+                CmsSigner cmsSigner = new CmsSigner(SubjectIdentifierType.NoSignature, cert);
+
+                if (includeExtraCert)
+                {
+                    cmsSigner.Certificates.Add(cert2);
+                }
+
+                cms.ComputeSignature(cmsSigner);
+
+                certs = cms.Certificates;
+
+                if (includeExtraCert)
+                {
+                    Assert.Equal(1, certs.Count);
+                    Assert.Equal(cert2.RawData, certs[0].RawData);
+                }
+                else
+                {
+                    Assert.Equal(0, certs.Count);
+                }
+            }
+
+            Assert.ThrowsAny<CryptographicException>(() => cms.CheckSignature(true));
+            cms.CheckHash();
+
+            byte[] encoded = cms.Encode();
+
+            if (detached)
+            {
+                cms = new SignedCms(contentInfo, detached);
+            }
+            else
+            {
+                cms = new SignedCms();
+            }
+
+            cms.Decode(encoded);
+            Assert.ThrowsAny<CryptographicException>(() => cms.CheckSignature(true));
+            cms.CheckHash();
+
+            SignerInfoCollection signerInfos = cms.SignerInfos;
+            Assert.Equal(1, signerInfos.Count);
+
+            SignerInfo firstSigner = signerInfos[0];
+            Assert.ThrowsAny<CryptographicException>(() => firstSigner.CheckSignature(true));
+            firstSigner.CheckHash();
+
+            certs = cms.Certificates;
+
+            if (includeExtraCert)
+            {
+                Assert.Equal(1, certs.Count);
+                Assert.Equal("CN=DfHelleKeyAgreement1", certs[0].SubjectName.Name);
+            }
+            else
+            {
+                Assert.Equal(0, certs.Count);
+            }
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void AddFirstSigner_NoSignature_NoCert(bool detached)
+        {
+            ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 });
+            SignedCms cms = new SignedCms(contentInfo, detached);
+
+            Action sign = () =>
+                cms.ComputeSignature(
+                    new CmsSigner(SubjectIdentifierType.NoSignature)
+                    {
+                        IncludeOption = X509IncludeOption.None,
+                    });
+
+            if (PlatformDetection.IsFullFramework)
+            {
+                Assert.Throws<NullReferenceException>(sign);
+            }
+            else
+            {
+                sign();
+                Assert.ThrowsAny<CryptographicException>(() => cms.CheckSignature(true));
+                cms.CheckHash();
+                Assert.Equal(1, cms.SignerInfos.Count);
+            }
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void AddSecondSigner_NoSignature(bool detached)
+        {
+            ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 });
+            SignedCms cms = new SignedCms(contentInfo, detached);
+
+            using (X509Certificate2 cert = Certificates.RSAKeyTransferCapi1.TryGetCertificateWithPrivateKey())
+            {
+                cms.ComputeSignature(
+                    new CmsSigner(cert)
+                    {
+                        IncludeOption = X509IncludeOption.None,
+                    });
+
+                Assert.Throws<CryptographicException>(
+                    () =>
+                        cms.ComputeSignature(
+                            new CmsSigner(SubjectIdentifierType.NoSignature)
+                            {
+                                IncludeOption = X509IncludeOption.None,
+                            }));
+            }
+
+            Assert.Equal(1, cms.SignerInfos.Count);
+            Assert.ThrowsAny<CryptographicException>(() => cms.CheckSignature(true));
+            cms.CheckHash();
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void AddSecondSigner_NoSignature_AfterRemove(bool detached)
+        {
+            ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 });
+            SignedCms cms = new SignedCms(contentInfo, detached);
+
+            using (X509Certificate2 cert = Certificates.RSAKeyTransferCapi1.TryGetCertificateWithPrivateKey())
+            {
+                cms.ComputeSignature(
+                    new CmsSigner(cert)
+                    {
+                        IncludeOption = X509IncludeOption.None,
+                    });
+
+                Assert.Throws<CryptographicException>(
+                    () =>
+                        cms.ComputeSignature(
+                            new CmsSigner(SubjectIdentifierType.NoSignature)
+                            {
+                                IncludeOption = X509IncludeOption.None,
+                            }));
+
+                cms.RemoveSignature(0);
+
+                // Because the document was already initialized (when initially signed),
+                // the "NoSignature must be the first signer" exception is thrown, even
+                // though there are no signers.
+                Assert.Throws<CryptographicException>(
+                    () =>
+                        cms.ComputeSignature(
+                            new CmsSigner(SubjectIdentifierType.NoSignature)
+                            {
+                                IncludeOption = X509IncludeOption.None,
+                            }));
+            }
+
+            Assert.Equal(0, cms.SignerInfos.Count);
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void AddSecondSigner_NoSignature_LoadUnsigned(bool detached)
+        {
+            ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 });
+            SignedCms cms = new SignedCms(contentInfo, detached);
+
+            using (X509Certificate2 cert = Certificates.RSAKeyTransferCapi1.TryGetCertificateWithPrivateKey())
+            {
+                cms.ComputeSignature(
+                    new CmsSigner(cert)
+                    {
+                        IncludeOption = X509IncludeOption.None,
+                    });
+
+                Assert.Throws<CryptographicException>(
+                    () =>
+                        cms.ComputeSignature(
+                            new CmsSigner(SubjectIdentifierType.NoSignature)
+                            {
+                                IncludeOption = X509IncludeOption.None,
+                            }));
+
+                cms.RemoveSignature(0);
+
+                // Reload the document now that it has no signatures.
+                byte[] encoded = cms.Encode();
+
+                if (detached)
+                {
+                    cms = new SignedCms(contentInfo, detached);
+                }
+                else
+                {
+                    cms = new SignedCms();
+                }
+
+                cms.Decode(encoded);
+
+                // Because the document was already initialized (when loaded),
+                // the "NoSignature must be the first signer" exception is thrown, even
+                // though there are no signers.
+                Assert.Throws<CryptographicException>(
+                    () =>
+                        cms.ComputeSignature(
+                            new CmsSigner(SubjectIdentifierType.NoSignature)
+                            {
+                                IncludeOption = X509IncludeOption.None,
+                            }));
+            }
+
+            Assert.Equal(0, cms.SignerInfos.Count);
+        }
+
+        [Theory]
+        [InlineData(false, false)]
+        [InlineData(false, true)]
         [InlineData(true, true)]
         [InlineData(true, false)]
         public static void AddSigner_DuplicateCert_RSA(bool skidFirst, bool detached)
@@ -1163,5 +1466,113 @@ namespace System.Security.Cryptography.Pkcs.Tests
 
             Assert.Equal("42", envelopedCms.ContentInfo.Content.ByteArrayToHex());
         }
+
+        [Fact]
+        public static void CheckNoSignature_FromNetFx()
+        {
+            byte[] encoded = (
+                "30819F06092A864886F70D010702A0819130818E020101310F300D0609608648" +
+                "0165030402010500301406092A864886F70D010701A007040509080706053162" +
+                "3060020101301C3017311530130603550403130C44756D6D79205369676E6572" +
+                "020100300D06096086480165030402010500300C06082B060105050706020500" +
+                "0420AF5F6F5C5967C377E49193ECA1EE0B98300A171CD3165C9A2410E8FB7C02" +
+                "8674"
+            ).HexToByteArray();
+
+            CheckNoSignature(encoded);
+        }
+
+        [Fact]
+        public static void CheckNoSignature_FromNetFx_TamperSignatureOid()
+        {
+            // CheckNoSignature_FromNetFx with the algorithm identifier changed from
+            // 1.3.6.1.5.5.7.6.2 to 10.3.6.1.5.5.7.6.10
+            byte[] encoded = (
+                "30819F06092A864886F70D010702A0819130818E020101310F300D0609608648" +
+                "0165030402010500301406092A864886F70D010701A007040509080706053162" +
+                "3060020101301C3017311530130603550403130C44756D6D79205369676E6572" +
+                "020100300D06096086480165030402010500300C06082B0601050507060A0500" +
+                "0420AF5F6F5C5967C377E49193ECA1EE0B98300A171CD3165C9A2410E8FB7C02" +
+                "8674"
+            ).HexToByteArray();
+
+            CheckNoSignature(encoded, badOid: true);
+        }
+
+        [Fact]
+        public static void CheckNoSignature_FromCoreFx()
+        {
+            byte[] encoded = (
+                "30819906092A864886F70D010702A0818B308188020101310D300B0609608648" +
+                "016503040201301406092A864886F70D010701A00704050908070605315E305C" +
+                "020101301C3017311530130603550403130C44756D6D79205369676E65720201" +
+                "00300B0609608648016503040201300A06082B060105050706020420AF5F6F5C" +
+                "5967C377E49193ECA1EE0B98300A171CD3165C9A2410E8FB7C028674"
+            ).HexToByteArray();
+
+            CheckNoSignature(encoded);
+        }
+
+        [Fact]
+        public static void CheckNoSignature_FromCoreFx_TamperSignatureOid()
+        {
+            // CheckNoSignature_FromCoreFx with the algorithm identifier changed from
+            // 1.3.6.1.5.5.7.6.2 to 10.3.6.1.5.5.7.6.10
+            byte[] encoded = (
+                "30819906092A864886F70D010702A0818B308188020101310D300B0609608648" +
+                "016503040201301406092A864886F70D010701A00704050908070605315E305C" +
+                "020101301C3017311530130603550403130C44756D6D79205369676E65720201" +
+                "00300B0609608648016503040201300A06082B0601050507060A0420AF5F6F5C" +
+                "5967C377E49193ECA1EE0B98300A171CD3165C9A2410E8FB7C028674"
+            ).HexToByteArray();
+
+            CheckNoSignature(encoded, badOid: true);
+        }
+
+        [Fact]
+        public static void CheckNoSignature_FromCoreFx_TamperIssuerName()
+        {
+            // CheckNoSignature_FromCoreFx with the issuer name changed from "Dummy Cert"
+            // to "Dumny Cert" (m => n / 0x6D => 0x6E)
+            byte[] encoded = (
+                "30819906092A864886F70D010702A0818B308188020101310D300B0609608648" +
+                "016503040201301406092A864886F70D010701A00704050908070605315E305C" +
+                "020101301C3017311530130603550403130C44756D6E79205369676E65720201" +
+                "00300B0609608648016503040201300A06082B060105050706020420AF5F6F5C" +
+                "5967C377E49193ECA1EE0B98300A171CD3165C9A2410E8FB7C028674"
+            ).HexToByteArray();
+
+            SignedCms cms = new SignedCms();
+            cms.Decode(encoded);
+            SignerInfoCollection signers = cms.SignerInfos;
+            Assert.Equal(1, signers.Count);
+            Assert.Equal(SubjectIdentifierType.IssuerAndSerialNumber, signers[0].SignerIdentifier.Type);
+            Assert.ThrowsAny<CryptographicException>(() => cms.CheckSignature(true));
+            Assert.ThrowsAny<CryptographicException>(() => signers[0].CheckSignature(true));
+
+            // Assert.NoThrow
+            cms.CheckHash();
+            signers[0].CheckHash();
+        }
+
+        private static void CheckNoSignature(byte[] encoded, bool badOid=false)
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(encoded);
+            SignerInfoCollection signers = cms.SignerInfos;
+            Assert.Equal(1, signers.Count);
+            Assert.Equal(SubjectIdentifierType.NoSignature, signers[0].SignerIdentifier.Type);
+            Assert.Throws<CryptographicException>(() => cms.CheckSignature(true));
+
+            if (badOid)
+            {
+                Assert.ThrowsAny<CryptographicException>(() => cms.CheckHash());
+            }
+            else
+            {
+                // Assert.NoThrow
+                cms.CheckHash();
+            }
+        }
     }
 }
index 8d876fa..b948506 100644 (file)
@@ -732,6 +732,202 @@ namespace System.Security.Cryptography.Pkcs.Tests
         }
 
         [Fact]
+        public static void AddFirstCounterSigner_NoSignature_NoPrivateKey()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.RsaPkcs1OneSignerIssuerAndSerialNumber);
+
+            SignerInfo firstSigner = cms.SignerInfos[0];
+
+            using (X509Certificate2 cert = Certificates.RSAKeyTransferCapi1.GetCertificate())
+            {
+                Action sign = () =>
+                    firstSigner.ComputeCounterSignature(
+                        new CmsSigner(
+                            SubjectIdentifierType.NoSignature,
+                            cert)
+                        {
+                            IncludeOption = X509IncludeOption.None,
+                        });
+
+                if (PlatformDetection.IsFullFramework)
+                {
+                    Assert.ThrowsAny<CryptographicException>(sign);
+                }
+                else
+                {
+                    sign();
+                    cms.CheckHash();
+                    Assert.ThrowsAny<CryptographicException>(() => cms.CheckSignature(true));
+                    firstSigner.CheckSignature(true);
+                }
+            }
+        }
+
+        [Fact]
+        public static void AddFirstCounterSigner_NoSignature()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.RsaPkcs1OneSignerIssuerAndSerialNumber);
+
+            SignerInfo firstSigner = cms.SignerInfos[0];
+
+            // A certificate shouldn't really be required here, but on .NET Framework
+            // it will prompt for the counter-signer's certificate if it's null,
+            // even if the signature type is NoSignature.
+            using (X509Certificate2 cert = Certificates.RSAKeyTransferCapi1.TryGetCertificateWithPrivateKey())
+            {
+                firstSigner.ComputeCounterSignature(
+                    new CmsSigner(
+                        SubjectIdentifierType.NoSignature,
+                        cert)
+                    {
+                        IncludeOption = X509IncludeOption.None,
+                    });
+            }
+
+            Assert.ThrowsAny<CryptographicException>(() => cms.CheckSignature(true));
+            cms.CheckHash();
+
+            byte[] encoded = cms.Encode();
+            cms = new SignedCms();
+            cms.Decode(encoded);
+            Assert.ThrowsAny<CryptographicException>(() => cms.CheckSignature(true));
+            cms.CheckHash();
+
+            firstSigner = cms.SignerInfos[0];
+            firstSigner.CheckSignature(verifySignatureOnly: true);
+            Assert.ThrowsAny<CryptographicException>(() => firstSigner.CheckHash());
+
+            SignerInfo firstCounterSigner = firstSigner.CounterSignerInfos[0];
+            Assert.ThrowsAny<CryptographicException>(() => firstCounterSigner.CheckSignature(true));
+
+            if (PlatformDetection.IsFullFramework)
+            {
+                // NetFX's CheckHash only looks at top-level SignerInfos to find the
+                // crypt32 CMS signer ID, so it fails on any check from a countersigner.
+                Assert.ThrowsAny<CryptographicException>(() => firstCounterSigner.CheckHash());
+            }
+            else
+            {
+                firstCounterSigner.CheckHash();
+            }
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void AddSecondCounterSignature_NoSignature_WithCert(bool addExtraCert)
+        {
+            AddSecondCounterSignature_NoSignature(withCertificate: true, addExtraCert);
+        }
+
+        [Theory]
+        // On .NET Framework it will prompt for the counter-signer's certificate if it's null,
+        // even if the signature type is NoSignature, so don't run the test there.
+        [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void AddSecondCounterSignature_NoSignature_WithoutCert(bool addExtraCert)
+        {
+            AddSecondCounterSignature_NoSignature(withCertificate: false, addExtraCert);
+        }
+
+        private static void AddSecondCounterSignature_NoSignature(bool withCertificate, bool addExtraCert)
+        {
+            X509Certificate2Collection certs;
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.RsaPkcs1OneSignerIssuerAndSerialNumber);
+
+            SignerInfo firstSigner = cms.SignerInfos[0];
+
+            using (X509Certificate2 cert = Certificates.RSAKeyTransferCapi1.TryGetCertificateWithPrivateKey())
+            using (X509Certificate2 cert2 = Certificates.DHKeyAgree1.GetCertificate())
+            {
+                firstSigner.ComputeCounterSignature(
+                    new CmsSigner(cert)
+                    {
+                        IncludeOption = X509IncludeOption.None,
+                    });
+
+                CmsSigner counterSigner;
+
+                if (withCertificate)
+                {
+                    counterSigner = new CmsSigner(SubjectIdentifierType.NoSignature, cert);
+                }
+                else
+                {
+                    counterSigner = new CmsSigner(SubjectIdentifierType.NoSignature);
+                }
+
+                if (addExtraCert)
+                {
+                    counterSigner.Certificates.Add(cert2);
+                }
+
+                firstSigner.ComputeCounterSignature(counterSigner);
+
+                certs = cms.Certificates;
+
+                if (addExtraCert)
+                {
+                    Assert.Equal(2, certs.Count);
+                    Assert.NotEqual(cert2.RawData, certs[0].RawData);
+                    Assert.Equal(cert2.RawData, certs[1].RawData);
+                }
+                else
+                {
+                    Assert.Equal(1, certs.Count);
+                    Assert.NotEqual(cert2.RawData, certs[0].RawData);
+                }
+            }
+
+            Assert.ThrowsAny<CryptographicException>(() => cms.CheckSignature(true));
+            cms.CheckHash();
+
+            byte[] encoded = cms.Encode();
+            cms = new SignedCms();
+            cms.Decode(encoded);
+            Assert.ThrowsAny<CryptographicException>(() => cms.CheckSignature(true));
+            cms.CheckHash();
+
+            firstSigner = cms.SignerInfos[0];
+            firstSigner.CheckSignature(verifySignatureOnly: true);
+            Assert.ThrowsAny<CryptographicException>(() => firstSigner.CheckHash());
+
+            // The NoSignature CounterSigner sorts first.
+            SignerInfo firstCounterSigner = firstSigner.CounterSignerInfos[0];
+            Assert.Equal(SubjectIdentifierType.NoSignature, firstCounterSigner.SignerIdentifier.Type);
+            Assert.ThrowsAny<CryptographicException>(() => firstCounterSigner.CheckSignature(true));
+
+            if (PlatformDetection.IsFullFramework)
+            {
+                // NetFX's CheckHash only looks at top-level SignerInfos to find the
+                // crypt32 CMS signer ID, so it fails on any check from a countersigner.
+                Assert.ThrowsAny<CryptographicException>(() => firstCounterSigner.CheckHash());
+            }
+            else
+            {
+                firstCounterSigner.CheckHash();
+            }
+
+            certs = cms.Certificates;
+
+            if (addExtraCert)
+            {
+                Assert.Equal(2, certs.Count);
+                Assert.Equal("CN=DfHelleKeyAgreement1", certs[1].SubjectName.Name);
+            }
+            else
+            {
+                Assert.Equal(1, certs.Count);
+            }
+
+            Assert.Equal("CN=RSAKeyTransferCapi1", certs[0].SubjectName.Name);
+        }
+
+        [Fact]
         [ActiveIssue(31977, TargetFrameworkMonikers.Uap)]
         public static void EnsureExtraCertsAdded()
         {