Add/Remove for CMS UnsignedAttributes and Certificates (dotnet/corefx#29408)
authorKrzysztof Wicher <mordotymoja@gmail.com>
Tue, 22 May 2018 19:56:43 +0000 (12:56 -0700)
committerGitHub <noreply@github.com>
Tue, 22 May 2018 19:56:43 +0000 (12:56 -0700)
* Initial implementation for Add/RemoveUnsignedAttribute

* initial implementation for Add/RemoveCertificate

* Add/RemoveUnsignedAttribute tests

* add tests for Add/RemoveCert

* apply feedback

* apply missed feedback

* make ReReadSugnedCms take reference

* Fix test to compare bytes of Rfc3161TimestampTokenInfo.SerialNumber

* change AddUnsignedAttribute to more compact and fix RemoveCounterSignature

* apply review feedback

* add test for RemoveCounterSignature

* regenerate repro document with correct settings for DSA counter-signature

* disable test on netfx

* apply feedback

Commit migrated from https://github.com/dotnet/corefx/commit/36c98d4f4b54770088d8f36de5c2d30436c08087

src/libraries/System.Security.Cryptography.Pkcs/ref/System.Security.Cryptography.Pkcs.cs
src/libraries/System.Security.Cryptography.Pkcs/src/Resources/Strings.resx
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.netcoreapp.cs
src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedDocuments.cs
src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.cs
src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.netcoreapp.cs [new file with mode: 0644]
src/libraries/System.Security.Cryptography.Pkcs/tests/System.Security.Cryptography.Pkcs.Tests.csproj

index 32e51ab..7c9f8ce 100644 (file)
@@ -255,6 +255,8 @@ namespace System.Security.Cryptography.Pkcs
         public void CheckSignature(bool verifySignatureOnly) => throw null;
         public void CheckSignature(System.Security.Cryptography.X509Certificates.X509Certificate2Collection extraStore, bool verifySignatureOnly) => throw null;
         public void CheckHash() => throw null;
+        public void AddCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) => throw null;
+        public void RemoveCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) => throw null;
     }
     public sealed partial class SignerInfo
     {
@@ -265,6 +267,8 @@ namespace System.Security.Cryptography.Pkcs
         public Oid DigestAlgorithm => throw null;
         public CryptographicAttributeObjectCollection SignedAttributes => throw null;
         public CryptographicAttributeObjectCollection UnsignedAttributes => throw null;
+        public void AddUnsignedAttribute(System.Security.Cryptography.AsnEncodedData asnEncodedData) => throw null;
+        public void RemoveUnsignedAttribute(System.Security.Cryptography.AsnEncodedData asnEncodedData) => throw null;
         public SignerInfoCollection CounterSignerInfos => throw null;
         [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
         public void ComputeCounterSignature() => throw null;
index 193ce5c..4983963 100644 (file)
   <data name="Cryptography_WriteEncodedValue_OneValueAtATime" xml:space="preserve">
     <value>The input to WriteEncodedValue must represent a single encoded value with no trailing data.</value>
   </data>
-</root>
+  <data name="Cryptography_Cms_NoAttributeFound" xml:space="preserve">
+    <value>Attribute not found.</value>
+  </data>
+  <data name="Cryptography_Cms_NoCertificateFound" xml:space="preserve">
+    <value>Certificate not found.</value>
+  </data>
+  <data name="Cryptography_Cms_CertificateAlreadyInCollection" xml:space="preserve">
+    <value>Certificate already present in the collection.</value>
+  </data>
+</root>
\ No newline at end of file
index 45a0203..3f08ea6 100644 (file)
@@ -470,5 +470,64 @@ namespace System.Security.Cryptography.Pkcs
         {
             return ref _signedData;
         }
+
+        public void AddCertificate(X509Certificate2 certificate)
+        {
+            int existingLength = _signedData.CertificateSet?.Length ?? 0;
+
+            byte[] rawData = certificate.RawData;
+
+            if (existingLength > 0)
+            {
+                foreach (CertificateChoiceAsn cert in _signedData.CertificateSet)
+                {
+                    if (cert.Certificate.Value.Span.SequenceEqual(rawData))
+                    {
+                        throw new CryptographicException(SR.Cryptography_Cms_CertificateAlreadyInCollection);
+                    }
+                }
+            }
+
+            if (_signedData.CertificateSet == null)
+            {
+                _signedData.CertificateSet = new CertificateChoiceAsn[1];
+            }
+            else
+            {
+                Array.Resize(ref _signedData.CertificateSet, existingLength + 1);
+            }
+
+            _signedData.CertificateSet[existingLength] = new CertificateChoiceAsn
+            {
+                Certificate = rawData
+            };
+
+            Reencode();
+        }
+
+        public void RemoveCertificate(X509Certificate2 certificate)
+        {
+            int existingLength = _signedData.CertificateSet?.Length ?? 0;
+
+            if (existingLength != 0)
+            {
+                int idx = 0;
+                byte[] rawData = certificate.RawData;
+
+                foreach (CertificateChoiceAsn cert in _signedData.CertificateSet)
+                {
+                    if (cert.Certificate.Value.Span.SequenceEqual(rawData))
+                    {
+                        Helpers.RemoveAt(ref _signedData.CertificateSet, idx);
+                        Reencode();
+                        return;
+                    }
+
+                    idx++;
+                }
+            }
+
+            throw new CryptographicException(SR.Cryptography_Cms_NoCertificateFound);
+        }
     }
 }
index ad4e03f..af1636c 100644 (file)
@@ -108,6 +108,118 @@ namespace System.Security.Cryptography.Pkcs
 
         public Oid SignatureAlgorithm => new Oid(_signatureAlgorithm);
 
+        public void AddUnsignedAttribute(AsnEncodedData unsignedAttribute)
+        {
+            int myIdx = _document.SignerInfos.FindIndexForSigner(this);
+
+            if (myIdx < 0)
+            {
+                throw new CryptographicException(SR.Cryptography_Cms_SignerNotFound);
+            }
+
+            ref SignedDataAsn signedData = ref _document.GetRawData();
+            ref SignerInfoAsn mySigner = ref signedData.SignerInfos[myIdx];
+
+            int existingAttribute = mySigner.UnsignedAttributes == null ? -1 : FindAttributeIndexByOid(mySigner.UnsignedAttributes, unsignedAttribute.Oid);
+
+            if (existingAttribute == -1)
+            {
+                // create a new attribute
+                AttributeAsn newUnsignedAttr;
+                using (AsnWriter writer = new AsnWriter(AsnEncodingRules.BER))
+                {
+                    writer.PushSetOf();
+                    writer.WriteEncodedValue(unsignedAttribute.RawData);
+                    writer.PopSetOf();
+
+                    newUnsignedAttr = new AttributeAsn
+                    {
+                        AttrType = new Oid(unsignedAttribute.Oid),
+                        AttrValues = writer.Encode(),
+                    };
+                }
+
+                int newAttributeIdx;
+
+                if (mySigner.UnsignedAttributes == null)
+                {
+                    newAttributeIdx = 0;
+                    mySigner.UnsignedAttributes = new AttributeAsn[1];
+                }
+                else
+                {
+                    newAttributeIdx = mySigner.UnsignedAttributes.Length;
+                    Array.Resize(ref mySigner.UnsignedAttributes, newAttributeIdx + 1);
+                }
+
+                mySigner.UnsignedAttributes[newAttributeIdx] = newUnsignedAttr;
+            }
+            else
+            {
+                // merge with existing attribute
+                ref AttributeAsn modifiedAttr = ref mySigner.UnsignedAttributes[existingAttribute];
+
+                using (AsnWriter writer = new AsnWriter(AsnEncodingRules.BER))
+                {
+                    writer.PushSetOf();
+
+                    AsnReader reader = new AsnReader(modifiedAttr.AttrValues, AsnEncodingRules.BER);
+                    AsnReader collReader = reader.ReadSetOf();
+
+                    if (reader.HasData)
+                    {
+                        throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
+                    }
+
+                    // re-add old values
+                    while (collReader.HasData)
+                    {
+                        writer.WriteEncodedValue(collReader.GetEncodedValue());
+                    }
+
+                    writer.WriteEncodedValue(unsignedAttribute.RawData);
+
+                    writer.PopSetOf();
+                    modifiedAttr.AttrValues = writer.Encode();
+                }
+            }
+
+            // Re-normalize the document
+            _document.Reencode();
+        }
+
+        public void RemoveUnsignedAttribute(AsnEncodedData unsignedAttribute)
+        {
+            int myIdx = _document.SignerInfos.FindIndexForSigner(this);
+
+            if (myIdx < 0)
+            {
+                throw new CryptographicException(SR.Cryptography_Cms_SignerNotFound);
+            }
+
+            ref SignedDataAsn signedData = ref _document.GetRawData();
+            ref SignerInfoAsn mySigner = ref signedData.SignerInfos[myIdx];
+
+            (int outerIndex, int innerIndex) = FindAttributeLocation(mySigner.UnsignedAttributes, unsignedAttribute, out bool isOnlyValue);
+
+            if (outerIndex == -1 || innerIndex == -1)
+            {
+                throw new CryptographicException(SR.Cryptography_Cms_NoAttributeFound);
+            }
+
+            if (isOnlyValue)
+            {
+                Helpers.RemoveAt(ref mySigner.UnsignedAttributes, outerIndex);
+            }
+            else
+            {
+                RemoveAttributeValueWithoutIndexChecking(ref mySigner.UnsignedAttributes[outerIndex], innerIndex);
+            }
+
+            // Re-normalize the document
+            _document.Reencode();
+        }
+
         private SignerInfoCollection GetCounterSigners(AttributeAsn[] unsignedAttrs)
         {
             // Since each "attribute" can have multiple "attribute values" there's no real
@@ -306,31 +418,7 @@ namespace System.Security.Cryptography.Pkcs
             }
             else
             {
-                ref AttributeAsn modifiedAttr = ref unsignedAttrs[removeAttrIdx];
-
-                using (AsnWriter writer = new AsnWriter(AsnEncodingRules.BER))
-                {
-                    writer.PushSetOf();
-
-                    AsnReader reader = new AsnReader(modifiedAttr.AttrValues, writer.RuleSet);
-
-                    int i = 0;
-
-                    while (reader.HasData)
-                    {
-                        ReadOnlyMemory<byte> encodedValue = reader.GetEncodedValue();
-
-                        if (i != removeValueIndex)
-                        {
-                            writer.WriteEncodedValue(encodedValue);
-                        }
-
-                        i++;
-                    }
-
-                    writer.PopSetOf();
-                    modifiedAttr.AttrValues = writer.Encode();
-                }
+                RemoveAttributeValueWithoutIndexChecking(ref unsignedAttrs[removeAttrIdx], removeValueIndex);
             }
         }
 
@@ -661,5 +749,98 @@ namespace System.Security.Cryptography.Pkcs
 
             return new CryptographicAttributeObject(type, valueColl);
         }
+
+        private static int FindAttributeIndexByOid(AttributeAsn[] attributes, Oid oid, int startIndex = 0)
+        {
+            for (int i = startIndex; i < attributes.Length; i++)
+            {
+                if (attributes[i].AttrType.Value == oid.Value)
+                {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        private static int FindAttributeValueIndexByEncodedData(ReadOnlyMemory<byte> attributeValues, ReadOnlySpan<byte> asnEncodedData, out bool isOnlyValue)
+        {
+            AsnReader reader = new AsnReader(attributeValues, AsnEncodingRules.BER);
+            AsnReader collReader = reader.ReadSetOf();
+
+            if (reader.HasData)
+            {
+                throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
+            }
+
+            for (int i = 0; collReader.HasData; i++)
+            {
+                ReadOnlySpan<byte> data = collReader.GetEncodedValue().Span;
+                if (data.SequenceEqual(asnEncodedData))
+                {
+                    isOnlyValue = i == 0 && !collReader.HasData;
+                    return i;
+                }
+            }
+
+            isOnlyValue = false;
+            return -1;
+        }
+
+        private static (int, int) FindAttributeLocation(AttributeAsn[] attributes, AsnEncodedData attribute, out bool isOnlyValue)
+        {
+            for (int outerIndex = 0; ; outerIndex++)
+            {
+                outerIndex = FindAttributeIndexByOid(attributes, attribute.Oid, outerIndex);
+
+                if (outerIndex == -1)
+                {
+                    break;
+                }
+
+                int innerIndex = FindAttributeValueIndexByEncodedData(attributes[outerIndex].AttrValues, attribute.RawData, out isOnlyValue);
+                if (innerIndex != -1)
+                {
+                    return (outerIndex, innerIndex);
+                }
+            }
+
+            isOnlyValue = false;
+            return (-1, -1);
+        }
+
+        private static void RemoveAttributeValueWithoutIndexChecking(ref AttributeAsn modifiedAttr, int removeValueIndex)
+        {
+            // Using BER rules to avoid resorting
+            using (AsnWriter writer = new AsnWriter(AsnEncodingRules.BER))
+            {
+                writer.PushSetOf();
+
+                AsnReader reader = new AsnReader(modifiedAttr.AttrValues, writer.RuleSet);
+                AsnReader collReader = reader.ReadSetOf();
+
+                if (reader.HasData)
+                {
+                    throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
+                }
+
+                int i = 0;
+
+                while (collReader.HasData)
+                {
+                    ReadOnlyMemory<byte> encodedValue = collReader.GetEncodedValue();
+
+                    if (i != removeValueIndex)
+                    {
+                        writer.WriteEncodedValue(encodedValue);
+                    }
+
+                    i++;
+                }
+
+                writer.PopSetOf();
+                modifiedAttr.AttrValues = writer.Encode();
+            }
+        }
     }
 }
index 3791deb..19a60f3 100644 (file)
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
+using System.Collections.Generic;
 using System.Linq;
 using System.Security.Cryptography.X509Certificates;
 using Test.Cryptography;
@@ -225,7 +226,130 @@ namespace System.Security.Cryptography.Pkcs.Tests
                 Assert.Throws<CryptographicException>(() => cms.ComputeSignature(signer));
             }
         }
-        
+
+        [Fact]
+        public static void AddCertificate()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.CounterSignedRsaPkcs1OneSigner);
+
+            int numOfCerts = cms.Certificates.Count;
+
+            using (X509Certificate2 newCert = Certificates.RSAKeyTransfer1.GetCertificate())
+            {
+                cms.AddCertificate(newCert);
+
+                Assert.Equal(numOfCerts + 1, cms.Certificates.Count);
+                Assert.True(cms.Certificates.Contains(newCert));
+
+                cms.CheckSignature(true);
+            }
+        }
+
+        [Fact]
+        public static void AddCertificateWithPrivateKey()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.CounterSignedRsaPkcs1OneSigner);
+
+            int numOfCerts = cms.Certificates.Count;
+
+            using (X509Certificate2 newCert = Certificates.RSAKeyTransfer1.TryGetCertificateWithPrivateKey())
+            {
+                Assert.True(newCert.HasPrivateKey);
+                cms.AddCertificate(newCert);
+
+                Assert.Equal(numOfCerts + 1, cms.Certificates.Count);
+
+                X509Certificate2 addedCert = cms.Certificates.OfType<X509Certificate2>().Where((cert) => cert.Equals(newCert)).Single();
+                Assert.False(addedCert.HasPrivateKey);
+
+                Assert.Equal(newCert, addedCert);
+
+                cms.CheckSignature(true);
+            }
+        }
+
+        [Fact]
+        public static void RemoveCertificate()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.CounterSignedRsaPkcs1OneSigner);
+
+            var expectedCerts = new HashSet<X509Certificate2>(cms.Certificates.OfType<X509Certificate2>());
+
+            using (X509Certificate2 cert1 = Certificates.RSAKeyTransfer1.GetCertificate())
+            using (X509Certificate2 cert2 = Certificates.RSAKeyTransfer2.GetCertificate())
+            {
+                Assert.NotEqual(cert1, cert2);
+
+                cms.AddCertificate(cert1);
+                cms.AddCertificate(cert2);
+
+                expectedCerts.Add(cert2);
+
+                cms.RemoveCertificate(cert1);
+
+                Assert.Equal(expectedCerts.Count, cms.Certificates.Count);
+
+                foreach (X509Certificate2 documentCert in cms.Certificates)
+                {
+                    Assert.True(expectedCerts.Contains(documentCert));
+                }
+            }
+        }
+
+        [Fact]
+        public static void RemoveNonExistingCertificate()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.CounterSignedRsaPkcs1OneSigner);
+
+            using (X509Certificate2 certToRemove = Certificates.RSAKeyTransfer1.GetCertificate())
+            {
+                Assert.Throws<CryptographicException>(() => cms.RemoveCertificate(certToRemove));
+            }
+        }
+
+        [Fact]
+        public static void RemoveAllCertsAddBackSignerCert()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.CounterSignedRsaPkcs1OneSigner);
+
+            SignerInfo signerInfoBeforeRemoval = cms.SignerInfos[0];
+            X509Certificate2 signerCert = signerInfoBeforeRemoval.Certificate;
+
+            while (cms.Certificates.Count > 0)
+            {
+                cms.RemoveCertificate(cms.Certificates[0]);
+            }
+
+            // Signer info should be gone
+            Assert.Throws<CryptographicException>(() => cms.CheckSignature(true));
+
+            Assert.Null(cms.SignerInfos[0].Certificate);
+            Assert.NotNull(signerInfoBeforeRemoval.Certificate);
+
+            cms.AddCertificate(signerCert);
+            cms.CheckSignature(true);
+
+            Assert.Equal(1, cms.Certificates.Count);
+        }
+
+        [Fact]
+        public static void AddExistingCertificate()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.CounterSignedRsaPkcs1OneSigner);
+
+            using (X509Certificate2 newCert = Certificates.RSAKeyTransfer1.GetCertificate())
+            {
+                cms.AddCertificate(newCert);
+                Assert.Throws<CryptographicException>(() => cms.AddCertificate(newCert));
+            }
+        }
+
         private static void VerifyWithExplicitPrivateKey(X509Certificate2 cert, AsymmetricAlgorithm key)
         {
             using (var pubCert = new X509Certificate2(cert.RawData))
index 7d0e1fb..a0549e1 100644 (file)
@@ -395,5 +395,101 @@ namespace System.Security.Cryptography.Pkcs.Tests
             "0906052B0E03021A0500300906072A8648CE380403042F302D021476DCB780CE" +
             "D5B308A3630726A85DB97FBC50DFD1021500CDF2649B50500BB7428B9DCA6BEF" +
             "2C7E7EF1B79C").HexToByteArray();
+
+        public static byte[] RsaPkcs1TwoCounterSignaturesInSingleAttribute = (
+            "30820BBA06092A864886F70D010702A0820BAB30820BA7020101310D300B0609" +
+            "608648016503040201301406092A864886F70D010701A00704050102030405A0" +
+            "82081D308201583081FFA003020102021035428F3B3C5107AD49E776D6E74C4D" +
+            "C8300A06082A8648CE3D04030230153113301106035504030C0A454344534120" +
+            "54657374301E170D3135303530313030333730335A170D313630353031303035" +
+            "3730335A30153113301106035504030C0A454344534120546573743059301306" +
+            "072A8648CE3D020106082A8648CE3D030107034200047590F69CA114E92927E0" +
+            "34C997B7C882A8C992AC00CEFB4EB831901536F291E1B515263BCD20E1EA3249" +
+            "6FDAC84E2D8D1B703266A9088F6EAF652549D9BB63D5A331302F300E0603551D" +
+            "0F0101FF040403020388301D0603551D0E0416041411218A92C5EB12273B3C5C" +
+            "CFB8220CCCFDF387DB300A06082A8648CE3D040302034800304502201AFE595E" +
+            "19F1AE4B6A4B231E8851926438C55B5DDE632E6ADF13C1023A65898E022100CB" +
+            "DF434FDD197D8B594E8026E44263BADE773C2BEBD060CC4109484A498E7C7E30" +
+            "82032C30820214A003020102020900E0D8AB6819D7306E300D06092A864886F7" +
+            "0D01010B05003038313630340603550403132D54776F2074686F7573616E6420" +
+            "666F7274792065696768742062697473206F662052534120676F6F646E657373" +
+            "301E170D3137313130333233353131355A170D3138313130333233353131355A" +
+            "3038313630340603550403132D54776F2074686F7573616E6420666F72747920" +
+            "65696768742062697473206F662052534120676F6F646E65737330820122300D" +
+            "06092A864886F70D01010105000382010F003082010A028201010096C114A589" +
+            "8D09133EF859F89C1D848BA8CB5258793E05B92D499C55EEFACE274BBBC26803" +
+            "FB813B9C11C6898153CC1745DED2C4D2672F807F0B2D957BC4B65EBC9DDE26E2" +
+            "EA7B2A6FE9A7C4D8BD1EF6032B8F0BB6AA33C8B57248B3D5E3901D8A38A283D7" +
+            "E25FF8E6F522381EE5484234CFF7B30C174635418FA89E14C468AD89DCFCBBB5" +
+            "35E5AF53510F9EA7F9DA8C1B53375B6DAB95A291439A5648726EE1012E41388E" +
+            "100691642CF6917F5569D8351F2782F435A579014E8448EEA0C4AECAFF2F4767" +
+            "99D88457E2C8BCB56E5E128782B4FE26AFF0720D91D52CCAFE344255808F5271" +
+            "D09F784F787E8323182080915BE0AE15A71D66476D0F264DD084F30203010001" +
+            "A3393037301D0603551D0E04160414745B5F12EF962E84B897E246D399A2BADE" +
+            "A9C5AC30090603551D1304023000300B0603551D0F040403020780300D06092A" +
+            "864886F70D01010B0500038201010087A15DF37FBD6E9DED7A8FFF25E60B731F" +
+            "635469BA01DD14BC03B2A24D99EFD8B894E9493D63EC88C496CB04B33DF25222" +
+            "544F23D43F4023612C4D97B719C1F9431E4DB7A580CDF66A3E5F0DAF89A267DD" +
+            "187ABFFB08361B1F79232376AA5FC5AD384CC2F98FE36C1CEA0B943E1E396119" +
+            "0648889C8ABE8397A5A338843CBFB1D8B212BE46685ACE7B80475CC7C97FC037" +
+            "7936ABD5F664E9C09C463897726650711A1110FA9866BC1C278D95E5636AB96F" +
+            "AE95CCD67FD572A8C727E2C03E7B242457318BEC1BE52CA5BD9454A0A41140AE" +
+            "96ED1C56D220D1FD5DD3B1B4FB2AA0E04FC94F7E3C7D476F298962245563953A" +
+            "D7225EDCEAC8B8509E49292E62D8BF3082038D3082034AA003020102020900AB" +
+            "740A714AA83C92300B060960864801650304030230818D310B30090603550406" +
+            "13025553311330110603550408130A57617368696E67746F6E3110300E060355" +
+            "040713075265646D6F6E64311E301C060355040A13154D6963726F736F667420" +
+            "436F72706F726174696F6E3120301E060355040B13172E4E4554204672616D65" +
+            "776F726B2028436F7265465829311530130603550403130C313032342D626974" +
+            "20445341301E170D3135313132353134343030335A170D313531323235313434" +
+            "3030335A30818D310B3009060355040613025553311330110603550408130A57" +
+            "617368696E67746F6E3110300E060355040713075265646D6F6E64311E301C06" +
+            "0355040A13154D6963726F736F667420436F72706F726174696F6E3120301E06" +
+            "0355040B13172E4E4554204672616D65776F726B2028436F7265465829311530" +
+            "130603550403130C313032342D62697420445341308201B73082012C06072A86" +
+            "48CE3804013082011F02818100AEE3309FC7C9DB750D4C3797D333B3B9B234B4" +
+            "62868DB6FFBDED790B7FC8DDD574C2BD6F5E749622507AB2C09DF5EAAD84859F" +
+            "C0706A70BB8C9C8BE22B4890EF2325280E3A7F9A3CE341DBABEF6058D063EA67" +
+            "83478FF8B3B7A45E0CA3F7BAC9995DCFDDD56DF168E91349130F719A4E717351" +
+            "FAAD1A77EAC043611DC5CC5A7F021500D23428A76743EA3B49C62EF0AA17314A" +
+            "85415F0902818100853F830BDAA738465300CFEE02418E6B07965658EAFDA7E3" +
+            "38A2EB1531C0E0CA5EF1A12D9DDC7B550A5A205D1FF87F69500A4E4AF5759F3F" +
+            "6E7F0C48C55396B738164D9E35FB506BD50E090F6A497C70E7E868C61BD4477C" +
+            "1D62922B3DBB40B688DE7C175447E2E826901A109FAD624F1481B276BF63A665" +
+            "D99C87CEE9FD06330381840002818025B8E7078E149BAC352667623620029F5E" +
+            "4A5D4126E336D56F1189F9FF71EA671B844EBD351514F27B69685DDF716B32F1" +
+            "02D60EA520D56F544D19B2F08F5D9BDDA3CBA3A73287E21E559E6A07586194AF" +
+            "AC4F6E721EDCE49DE0029627626D7BD30EEB337311DB4FF62D7608997B6CC32E" +
+            "9C42859820CA7EF399590D5A388C48A330302E302C0603551D11042530238704" +
+            "7F00000187100000000000000000000000000000000182096C6F63616C686F73" +
+            "74300B0609608648016503040302033000302D021500B9316CC7E05C9F79197E" +
+            "0B41F6FD4E3FCEB72A8A0214075505CCAECB18B7EF4C00F9C069FA3BC78014DE" +
+            "3182035A3082035602010130453038313630340603550403132D54776F207468" +
+            "6F7573616E6420666F7274792065696768742062697473206F66205253412067" +
+            "6F6F646E657373020900E0D8AB6819D7306E300B060960864801650304020130" +
+            "0B06092A864886F70D01010104820100457E2996B3A1AE5C7DC2F4EF4D9010F4" +
+            "8B62B72DFB43F2EDC503FD32408A1058EE7BBCF4750CB4B4242B11A599C40792" +
+            "70D32D15A57FF791FF59836A027E634B9B97E1764173597A9A6155D5ED5365F6" +
+            "5DF14FDD15928ABD63E1409DBF2D1A713D20D80E09EE76BC63775F3FA8638A26" +
+            "ED3816FF87C7CDC8A9299485055BFC38AE158BB6577812AA98436FB54844544A" +
+            "C92CD449690B8107447044580FAE590D8A7326A8D139886C8A4AC8CEEACB0458" +
+            "1666D8447D267F1A9E9CAB20F155E05D5EC055AC863C047B5E1E3A98528EA766" +
+            "7C19B33AD98B2D33ABBD7E607C1DA18BCDB87C626554C277E069CE9EC489BC87" +
+            "2E7DEAED4C642DE5AB10BD2D558EAFB3A18201EA308201E606092A864886F70D" +
+            "010906318201D73082010D02010130819B30818D310B30090603550406130255" +
+            "53311330110603550408130A57617368696E67746F6E3110300E060355040713" +
+            "075265646D6F6E64311E301C060355040A13154D6963726F736F667420436F72" +
+            "706F726174696F6E3120301E060355040B13172E4E4554204672616D65776F72" +
+            "6B2028436F7265465829311530130603550403130C313032342D626974204453" +
+            "41020900AB740A714AA83C92300706052B0E03021AA025302306092A864886F7" +
+            "0D0109043116041409200943E2EDD3DD3B186C5839BDC9B1051903FF30090607" +
+            "2A8648CE380403042F302D0215009FDBE95176B1EC0697155ADDF335E5126A9F" +
+            "59D60214736F650C74E73BEA577151BCFD226FEDC06832E53081C30201013029" +
+            "30153113301106035504030C0A45434453412054657374021035428F3B3C5107" +
+            "AD49E776D6E74C4DC8300B0609608648016503040201A031302F06092A864886" +
+            "F70D01090431220420DF5D49DB775A8F94CAB3129038B200EDE9FCD2AE8F039D" +
+            "B1AB96D9B827D299D2300A06082A8648CE3D0403020447304502202327A60E1A" +
+            "5A798CD29B72C7C7991F968D29DB15C4865BEE83A7E2FD73326CA4022100899F" +
+            "000179F77BFE296783548EAE56BA7F53C0DB0563A27A36A149BAEC9C23AC").HexToByteArray();
     }
 }
index e97223c..0dc5135 100644 (file)
@@ -10,7 +10,7 @@ using Xunit;
 
 namespace System.Security.Cryptography.Pkcs.Tests
 {
-    public static class SignerInfoTests
+    public static partial class SignerInfoTests
     {
         [Fact]
         public static void SignerInfo_SignedAttributes_Cached_WhenEmpty()
@@ -343,6 +343,23 @@ namespace System.Security.Cryptography.Pkcs.Tests
                 () => signerInfo.RemoveCounterSignature(signerInfo));
         }
 
+        [Theory]
+        [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "NetFx bug")]
+        [InlineData(0)]
+        [InlineData(1)]
+        public static void RemoveCounterSignature_EncodedInSingleAttribute(int indexToRemove)
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.RsaPkcs1TwoCounterSignaturesInSingleAttribute);
+            SignerInfo signerInfo = cms.SignerInfos[0];
+
+            Assert.Equal(2, signerInfo.CounterSignerInfos.Count);
+            signerInfo.RemoveCounterSignature(indexToRemove);
+            Assert.Equal(1, signerInfo.CounterSignerInfos.Count);
+
+            cms.CheckSignature(true);
+        }
+
         [Fact]
         public static void RemoveCounterSignature_Null()
         {
diff --git a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.netcoreapp.cs b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.netcoreapp.cs
new file mode 100644 (file)
index 0000000..7688c07
--- /dev/null
@@ -0,0 +1,325 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using Test.Cryptography;
+using Xunit;
+
+namespace System.Security.Cryptography.Pkcs.Tests
+{
+    public static partial class SignerInfoTests
+    {
+        private const string TokenAttributeOid = "1.2.840.113549.1.9.16.2.14";
+
+        [Fact]
+        public static void SignerInfo_AddUnsignedAttribute_Adds()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.RsaPkcs1OneSignerIssuerAndSerialNumber);
+
+            Assert.Equal(0, cms.SignerInfos[0].UnsignedAttributes.Count);
+
+            AsnEncodedData attribute1 = CreateTimestampToken(1);
+            cms.SignerInfos[0].AddUnsignedAttribute(attribute1);
+
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes[0].Values.Count);
+            VerifyAttributesAreEqual(cms.SignerInfos[0].UnsignedAttributes[0].Values[0], attribute1);
+
+            ReReadSignedCms(ref cms);
+
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes[0].Values.Count);
+            VerifyAttributesAreEqual(cms.SignerInfos[0].UnsignedAttributes[0].Values[0], attribute1);
+
+            AsnEncodedData attribute2 = CreateTimestampToken(2);
+
+            cms.SignerInfos[0].AddUnsignedAttribute(attribute2);
+
+            var expectedAttributes = new List<AsnEncodedData>();
+            expectedAttributes.Add(attribute1);
+            expectedAttributes.Add(attribute2);
+
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(2, cms.SignerInfos[0].UnsignedAttributes[0].Values.Count);
+            VerifyAttributesContainsAll(cms.SignerInfos[0].UnsignedAttributes, expectedAttributes);
+
+            ReReadSignedCms(ref cms);
+
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(2, cms.SignerInfos[0].UnsignedAttributes[0].Values.Count);
+            VerifyAttributesContainsAll(cms.SignerInfos[0].UnsignedAttributes, expectedAttributes);
+        }
+
+        [Fact]
+        public static void SignerInfo_RemoveUnsignedAttribute_RemoveCounterSignature()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.OneRsaSignerTwoRsaCounterSigners);
+
+            Assert.Equal(2, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(2, cms.SignerInfos[0].CounterSignerInfos.Count);
+            byte[] secondSignerCounterSignature = cms.SignerInfos[0].CounterSignerInfos[1].GetSignature();
+
+            cms.SignerInfos[0].RemoveUnsignedAttribute(cms.SignerInfos[0].UnsignedAttributes[0].Values[0]);
+
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(1, cms.SignerInfos[0].CounterSignerInfos.Count);
+            Assert.Equal(secondSignerCounterSignature, cms.SignerInfos[0].CounterSignerInfos[0].GetSignature());
+
+            ReReadSignedCms(ref cms);
+
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(1, cms.SignerInfos[0].CounterSignerInfos.Count);
+            Assert.Equal(secondSignerCounterSignature, cms.SignerInfos[0].CounterSignerInfos[0].GetSignature());
+        }
+
+        [Theory]
+        [MemberData(nameof(SignedDocumentsWithAttributesTestData))]
+        public static void SignerInfo_RemoveUnsignedAttributes_RemoveAllAttributesFromBeginning(byte[] document)
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(document);
+
+            List<AsnEncodedData> attributes = GetAllAsnEncodedDataFromAttributes(cms.SignerInfos[0].UnsignedAttributes);
+            Assert.True(attributes.Count > 0);
+
+            for (int i = 0; i < attributes.Count; i++)
+            {
+                AsnEncodedData attribute = attributes[i];
+                cms.SignerInfos[0].RemoveUnsignedAttribute(attribute);
+                attributes.RemoveAt(0);
+
+                VerifyAttributesContainsAll(cms.SignerInfos[0].UnsignedAttributes, attributes);
+
+                ReReadSignedCms(ref cms);
+                VerifyAttributesContainsAll(cms.SignerInfos[0].UnsignedAttributes, attributes);
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(SignedDocumentsWithAttributesTestData))]
+        public static void SignerInfo_RemoveUnsignedAttributes_RemoveAllAttributesFromEnd(byte[] document)
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(document);
+
+            List<AsnEncodedData> attributes = GetAllAsnEncodedDataFromAttributes(cms.SignerInfos[0].UnsignedAttributes);
+            Assert.True(attributes.Count > 0);
+
+            for (int i = attributes.Count - 1; i >= 0; i--)
+            {
+                AsnEncodedData attribute = attributes[i];
+                cms.SignerInfos[0].RemoveUnsignedAttribute(attribute);
+                attributes.RemoveAt(i);
+
+                VerifyAttributesContainsAll(cms.SignerInfos[0].UnsignedAttributes, attributes);
+
+                ReReadSignedCms(ref cms);
+                VerifyAttributesContainsAll(cms.SignerInfos[0].UnsignedAttributes, attributes);
+            }
+        }
+
+        [Fact]
+        public static void SignerInfo_RemoveUnsignedAttributes_RemoveWithNonMatchingOid()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.OneRsaSignerTwoRsaCounterSigners);
+
+            int numberOfAttributes = cms.SignerInfos[0].UnsignedAttributes.Count;
+            Assert.NotEqual(0, numberOfAttributes);
+
+            AsnEncodedData fakeAttribute = new AsnEncodedData(new Oid("1.2.3.4", "1.2.3.4"), cms.SignerInfos[0].UnsignedAttributes[0].Values[0].RawData);
+            Assert.Throws<CryptographicException>(() => cms.SignerInfos[0].RemoveUnsignedAttribute(fakeAttribute));
+
+            Assert.Equal(numberOfAttributes, cms.SignerInfos[0].UnsignedAttributes.Count);
+        }
+
+        [Fact]
+        public static void SignerInfo_RemoveUnsignedAttributes_RemoveWithNonMatchingData()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.OneRsaSignerTwoRsaCounterSigners);
+
+            int numberOfAttributes = cms.SignerInfos[0].UnsignedAttributes.Count;
+            Assert.NotEqual(0, numberOfAttributes);
+
+            AsnEncodedData fakeAttribute = new AsnEncodedData(
+                cms.SignerInfos[0].UnsignedAttributes[0].Oid,
+                cms.SignerInfos[0].UnsignedAttributes[0].Values[0].RawData.Skip(1).ToArray());
+            Assert.Throws<CryptographicException>(() => cms.SignerInfos[0].RemoveUnsignedAttribute(fakeAttribute));
+
+            Assert.Equal(numberOfAttributes, cms.SignerInfos[0].UnsignedAttributes.Count);
+        }
+
+        [Fact]
+        public static void SignerInfo_RemoveUnsignedAttributes_MultipleAttributeValues()
+        {
+            SignedCms cms = new SignedCms();
+            cms.Decode(SignedDocuments.RsaPkcs1OneSignerIssuerAndSerialNumber);
+
+            Assert.Equal(0, cms.SignerInfos[0].UnsignedAttributes.Count);
+
+            AsnEncodedData attribute1 = CreateTimestampToken(1);
+            AsnEncodedData attribute2 = CreateTimestampToken(2);
+            cms.SignerInfos[0].AddUnsignedAttribute(attribute1);
+            cms.SignerInfos[0].AddUnsignedAttribute(attribute2);
+            
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(2, cms.SignerInfos[0].UnsignedAttributes[0].Values.Count);
+
+            cms.SignerInfos[0].RemoveUnsignedAttribute(attribute1);
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes[0].Values.Count);
+            Assert.True(AsnEncodedDataEqual(attribute2, cms.SignerInfos[0].UnsignedAttributes[0].Values[0]));
+
+            cms.SignerInfos[0].RemoveUnsignedAttribute(attribute2);
+            Assert.Equal(0, cms.SignerInfos[0].UnsignedAttributes.Count);
+        }
+
+        [Fact]
+        public static void SignerInfo_AddRemoveUnsignedAttributes_JoinCounterSignaturesAttributesIntoOne()
+        {
+            byte[] message = { 1, 2, 3, 4, 5 };
+            ContentInfo content = new ContentInfo(message);
+            SignedCms cms = new SignedCms(content);
+
+            using (X509Certificate2 signerCert = Certificates.RSA2048SignatureOnly.TryGetCertificateWithPrivateKey())
+            {
+                CmsSigner signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, signerCert);
+                cms.ComputeSignature(signer);
+            }
+
+            using (X509Certificate2 counterSigner1cert = Certificates.Dsa1024.TryGetCertificateWithPrivateKey())
+            {
+                CmsSigner counterSigner = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, counterSigner1cert);
+                counterSigner.IncludeOption = X509IncludeOption.EndCertOnly;
+                counterSigner.DigestAlgorithm = new Oid(Oids.Sha1, Oids.Sha1);
+                cms.SignerInfos[0].ComputeCounterSignature(counterSigner);
+            }
+
+            using (X509Certificate2 counterSigner2cert = Certificates.ECDsaP256Win.TryGetCertificateWithPrivateKey())
+            {
+                CmsSigner counterSigner = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, counterSigner2cert);
+                cms.SignerInfos[0].ComputeCounterSignature(counterSigner);
+            }
+
+            Assert.Equal(2, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes[0].Values.Count);
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes[1].Values.Count);
+            cms.CheckSignature(true);
+
+            AsnEncodedData counterSignature = cms.SignerInfos[0].UnsignedAttributes[0].Values[0];
+            cms.SignerInfos[0].RemoveUnsignedAttribute(counterSignature);
+            cms.SignerInfos[0].AddUnsignedAttribute(counterSignature);
+
+            Assert.Equal(1, cms.SignerInfos[0].UnsignedAttributes.Count);
+            Assert.Equal(2, cms.SignerInfos[0].UnsignedAttributes[0].Values.Count);
+            cms.CheckSignature(true);
+        }
+
+        private static void VerifyAttributesContainsAll(CryptographicAttributeObjectCollection attributes, List<AsnEncodedData> expectedAttributes)
+        {
+            var indices = new HashSet<int>();
+            foreach (CryptographicAttributeObject attribute in attributes)
+            {
+                foreach (AsnEncodedData attributeValue in attribute.Values)
+                {
+                    int idx = FindAsnEncodedData(expectedAttributes, attributeValue);
+                    Assert.NotEqual(-1, idx);
+                    indices.Add(idx);
+                }
+            }
+
+            Assert.Equal(expectedAttributes.Count, indices.Count);
+        }
+
+        private static int FindAsnEncodedData(List<AsnEncodedData> array, AsnEncodedData data)
+        {
+            for (int i = 0; i < array.Count; i++)
+            {
+                if (AsnEncodedDataEqual(array[i], data))
+                {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        private static List<AsnEncodedData> GetAllAsnEncodedDataFromAttributes(CryptographicAttributeObjectCollection attributes)
+        {
+            var ret = new List<AsnEncodedData>();
+            foreach (CryptographicAttributeObject attribute in attributes)
+            {
+                foreach (AsnEncodedData attributeValue in attribute.Values)
+                {
+                    ret.Add(attributeValue);
+                }
+            }
+
+            return ret;
+        }
+
+        private static bool AsnEncodedDataEqual(AsnEncodedData a, AsnEncodedData b)
+        {
+            return a.Oid.Value == b.Oid.Value && a.RawData.SequenceEqual(b.RawData);
+        }
+
+        private static void ReReadSignedCms(ref SignedCms cms)
+        {
+            byte[] bytes = cms.Encode();
+
+            cms = new SignedCms();
+            cms.Decode(bytes);
+        }
+
+        private static AsnEncodedData CreateTimestampToken(byte serial)
+        {
+            Oid tokenOid = new Oid(TokenAttributeOid, TokenAttributeOid);
+
+            Oid policyId = new Oid("0.0", "0.0");
+            Oid hashAlgorithmId = new Oid(Oids.Sha256);
+
+            var tokenInfo = new Rfc3161TimestampTokenInfo(
+                policyId,
+                hashAlgorithmId,
+                new byte[256 / 8],
+                new byte[] { (byte)serial },
+                DateTimeOffset.UtcNow);
+
+            return new AsnEncodedData(tokenOid, tokenInfo.Encode());
+        }
+
+        private static void VerifyAttributesAreEqual(AsnEncodedData actual, AsnEncodedData expected)
+        {
+            Assert.NotSame(expected.Oid, actual.Oid);
+            Assert.Equal(expected.Oid.Value, actual.Oid.Value);
+            Assert.Equal(expected.Oid.FriendlyName, actual.Oid.FriendlyName);
+
+            // We need to decode bytes because DER and BER may encode the same information slightly differently
+            Rfc3161TimestampTokenInfo expectedToken;
+            Assert.True(Rfc3161TimestampTokenInfo.TryDecode(expected.RawData, out expectedToken, out _));
+
+            Rfc3161TimestampTokenInfo actualToken;
+            Assert.True(Rfc3161TimestampTokenInfo.TryDecode(actual.RawData, out actualToken, out _));
+
+            Assert.Equal(expectedToken.GetSerialNumber().ByteArrayToHex(), actualToken.GetSerialNumber().ByteArrayToHex());
+            Assert.Equal(expectedToken.Timestamp, actualToken.Timestamp);
+            Assert.Equal(expectedToken.HashAlgorithmId.Value, Oids.Sha256);
+            Assert.Equal(expectedToken.HashAlgorithmId.Value, actualToken.HashAlgorithmId.Value);
+        }
+
+        private static IEnumerable<object[]> SignedDocumentsWithAttributesTestData()
+        {
+            yield return new object[] { SignedDocuments.CounterSignedRsaPkcs1OneSigner };
+            yield return new object[] { SignedDocuments.NoSignatureSignedWithAttributesAndCounterSignature };
+            yield return new object[] { SignedDocuments.OneRsaSignerTwoRsaCounterSigners };
+            yield return new object[] { SignedDocuments.RsaPkcs1CounterSignedWithNoSignature };
+            yield return new object[] { SignedDocuments.UnsortedSignerInfos};
+        }
+    }
+}
index 28485c0..7d3f1f4 100644 (file)
@@ -48,6 +48,7 @@
     <Compile Include="Rfc3161\TimestampTokenTestData.cs" />
     <Compile Include="Rfc3161\TimestampTokenTests.cs" />
     <Compile Include="SignedCms\SignedCmsTests.netcoreapp.cs" />
+    <Compile Include="SignedCms\SignerInfoTests.netcoreapp.cs" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetsWindows)' == 'true' AND '$(TargetGroup)'=='netcoreapp'">
     <Compile Include="EnvelopedCms\DecryptTests.KeyPersistence.cs" />