1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System.Collections.Generic;
6 using System.Diagnostics;
7 using System.Linq;
8 using System.Security.Cryptography.Asn1;
9 using System.Security.Cryptography.Pkcs.Asn1;
10 using System.Security.Cryptography.X509Certificates;
11 using Internal.Cryptography;
12 
13 namespace System.Security.Cryptography.Pkcs
14 {
15     public sealed partial class SignedCms
16     {
17         private SignedDataAsn _signedData;
18         private bool _hasData;
19 
20         // A defensive copy of the relevant portions of the data to Decode
21         private Memory<byte> _heldData;
22 
23         // Due to the way the underlying Windows CMS API behaves a copy of the content
24         // bytes will be held separate once the content is "bound" (first signature or decode)
25         private ReadOnlyMemory<byte>? _heldContent;
26 
27         // Similar to _heldContent, the Windows CMS API held this separate internally,
28         // and thus we need to be reslilient against modification.
29         private string _contentType;
30 
31         public int Version { get; private set; }
32         public ContentInfo ContentInfo { get; private set; }
33         public bool Detached { get; private set; }
34 
SignedCms(SubjectIdentifierType signerIdentifierType, ContentInfo contentInfo, bool detached)35         public SignedCms(SubjectIdentifierType signerIdentifierType, ContentInfo contentInfo, bool detached)
36         {
37             if (contentInfo == null)
38                 throw new ArgumentNullException(nameof(contentInfo));
39             if (contentInfo.Content == null)
40                 throw new ArgumentNullException("contentInfo.Content");
41 
42             // signerIdentifierType is ignored.
43             // In .NET Framework it is used for the signer type of a prompt-for-certificate signer.
44             // In .NET Core we don't support prompting.
45             //
46             // .NET Framework turned any unknown value into IssuerAndSerialNumber, so no exceptions
47             // are required, either.
48 
49             ContentInfo = contentInfo;
50             Detached = detached;
51             Version = 0;
52         }
53 
54         public X509Certificate2Collection Certificates
55         {
56             get
57             {
58                 var coll = new X509Certificate2Collection();
59 
60                 if (!_hasData)
61                 {
62                     return coll;
63                 }
64 
65                 CertificateChoiceAsn[] certChoices = _signedData.CertificateSet;
66 
67                 if (certChoices == null)
68                 {
69                     return coll;
70                 }
71 
72                 foreach (CertificateChoiceAsn choice in certChoices)
73                 {
74                     coll.Add(new X509Certificate2(choice.Certificate.Value.ToArray()));
75                 }
76 
77                 return coll;
78             }
79         }
80 
81         public SignerInfoCollection SignerInfos
82         {
83             get
84             {
85                 if (!_hasData)
86                 {
87                     return new SignerInfoCollection();
88                 }
89 
90                 return new SignerInfoCollection(_signedData.SignerInfos, this);
91             }
92         }
93 
Encode()94         public byte[] Encode()
95         {
96             if (!_hasData)
97             {
98                 throw new InvalidOperationException(SR.Cryptography_Cms_MessageNotSigned);
99             }
100 
101             // Write as DER, so everyone can read it.
102             AsnWriter writer = AsnSerializer.Serialize(_signedData, AsnEncodingRules.DER);
103             byte[] signedData = writer.Encode();
104 
105             ContentInfoAsn contentInfo = new ContentInfoAsn
106             {
107                 Content = signedData,
108                 ContentType = Oids.Pkcs7Signed,
109             };
110 
111             // Write as DER, so everyone can read it.
112             writer = AsnSerializer.Serialize(contentInfo, AsnEncodingRules.DER);
113             return writer.Encode();
114         }
115 
Decode(byte[] encodedMessage)116         public void Decode(byte[] encodedMessage)
117         {
118             if (encodedMessage == null)
119                 throw new ArgumentNullException(nameof(encodedMessage));
120 
121             // Windows (and thus NetFx) reads the leading data and ignores extra.
122             // The deserializer will complain if too much data is given, so use the reader
123             // to ask how much we want to deserialize.
124             AsnReader reader = new AsnReader(encodedMessage, AsnEncodingRules.BER);
125             ReadOnlyMemory<byte> cmsSegment = reader.GetEncodedValue();
126 
127             ContentInfoAsn contentInfo = AsnSerializer.Deserialize<ContentInfoAsn>(cmsSegment, AsnEncodingRules.BER);
128 
129             if (contentInfo.ContentType != Oids.Pkcs7Signed)
130             {
131                 throw new CryptographicException(SR.Cryptography_Cms_InvalidMessageType);
132             }
133 
134             // Hold a copy of the SignedData memory so we are protected against memory reuse by the caller.
135             _heldData = contentInfo.Content.ToArray();
136             _signedData = AsnSerializer.Deserialize<SignedDataAsn>(_heldData, AsnEncodingRules.BER);
137             _contentType = _signedData.EncapContentInfo.ContentType;
138 
139             if (!Detached)
140             {
141                 ReadOnlyMemory<byte>? content = _signedData.EncapContentInfo.Content;
142 
143                 // This is in _heldData, so we don't need a defensive copy.
144                 _heldContent = content ?? ReadOnlyMemory<byte>.Empty;
145 
146                 // The ContentInfo object/property DOES need a defensive copy, because
147                 // a) it is mutable by the user, and
148                 // b) it is no longer authoritative
149                 //
150                 // (and c: it takes a byte[] and we have a ReadOnlyMemory<byte>)
151                 ContentInfo = new ContentInfo(new Oid(_contentType), _heldContent.Value.ToArray());
152             }
153             else
154             {
155                 // Hold a defensive copy of the content bytes, (Windows/NetFx compat)
156                 _heldContent = ContentInfo.Content.CloneByteArray();
157             }
158 
159             Version = _signedData.Version;
160             _hasData = true;
161         }
162 
ComputeSignature()163         public void ComputeSignature()
164         {
165             throw new PlatformNotSupportedException(SR.Cryptography_Cms_NoSignerCert);
166         }
167 
168         public void ComputeSignature(CmsSigner signer) => ComputeSignature(signer, true);
169 
ComputeSignature(CmsSigner signer, bool silent)170         public void ComputeSignature(CmsSigner signer, bool silent)
171         {
172             if (signer == null)
173             {
174                 throw new ArgumentNullException(nameof(signer));
175             }
176 
177             // While it shouldn't be possible to change the length of ContentInfo.Content
178             // after it's built, use the property at this stage, then use the saved value
179             // (if applicable) after this point.
180             if (ContentInfo.Content.Length == 0)
181             {
182                 throw new CryptographicException(SR.Cryptography_Cms_Sign_Empty_Content);
183             }
184 
185             // If we had content already, use that now.
186             // (The second signer doesn't inherit edits to signedCms.ContentInfo.Content)
187             ReadOnlyMemory<byte> content = _heldContent ?? ContentInfo.Content;
188             string contentType = _contentType ?? ContentInfo.ContentType.Value;
189 
190             X509Certificate2Collection chainCerts;
191             SignerInfoAsn newSigner = signer.Sign(content, contentType, silent, out chainCerts);
192             bool firstSigner = false;
193 
194             if (!_hasData)
195             {
196                 firstSigner = true;
197 
198                 _signedData = new SignedDataAsn
199                 {
200                     DigestAlgorithms = Array.Empty<AlgorithmIdentifierAsn>(),
201                     SignerInfos = Array.Empty<SignerInfoAsn>(),
202                     EncapContentInfo = new EncapsulatedContentInfoAsn { ContentType = contentType },
203                 };
204 
205                 // Since we're going to call Decode before this method exits we don't need to save
206                 // the copy of _heldContent or _contentType here if we're attached.
207                 if (!Detached)
208                 {
209                     _signedData.EncapContentInfo.Content = content;
210                 }
211 
212                 _hasData = true;
213             }
214 
215             int newIdx = _signedData.SignerInfos.Length;
216             Array.Resize(ref _signedData.SignerInfos, newIdx + 1);
217             _signedData.SignerInfos[newIdx] = newSigner;
218             UpdateCertificatesFromAddition(chainCerts);
219             ConsiderDigestAddition(newSigner.DigestAlgorithm);
220             UpdateMetadata();
221 
222             if (firstSigner)
223             {
224                 Reencode();
225 
226                 Debug.Assert(_heldContent != null);
227                 Debug.Assert(_contentType == contentType);
228             }
229         }
230 
RemoveSignature(int index)231         public void RemoveSignature(int index)
232         {
233             if (!_hasData)
234             {
235                 throw new InvalidOperationException(SR.Cryptography_Cms_MessageNotSigned);
236             }
237 
238             if (index < 0 || index >= _signedData.SignerInfos.Length)
239             {
240                 throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_Index);
241             }
242 
243             AlgorithmIdentifierAsn signerAlgorithm = _signedData.SignerInfos[index].DigestAlgorithm;
244             Helpers.RemoveAt(ref _signedData.SignerInfos, index);
245 
246             ConsiderDigestRemoval(signerAlgorithm);
247             UpdateMetadata();
248         }
249 
RemoveSignature(SignerInfo signerInfo)250         public void RemoveSignature(SignerInfo signerInfo)
251         {
252             if (signerInfo == null)
253                 throw new ArgumentNullException(nameof(signerInfo));
254 
255             int idx = SignerInfos.FindIndexForSigner(signerInfo);
256 
257             if (idx < 0)
258             {
259                 throw new CryptographicException(SR.Cryptography_Cms_SignerNotFound);
260             }
261 
262             RemoveSignature(idx);
263         }
264 
GetContentSpan()265         internal ReadOnlySpan<byte> GetContentSpan() => _heldContent.Value.Span;
266 
Reencode()267         internal void Reencode()
268         {
269             // When NetFx re-encodes it just resets the CMS handle, the ContentInfo property
270             // does not get changed.
271             // See ReopenToDecode
272             ContentInfo save = ContentInfo;
273 
274             try
275             {
276                 byte[] encoded = Encode();
277 
278                 if (Detached)
279                 {
280                     // At this point the _heldContent becomes whatever ContentInfo says it should be.
281                     _heldContent = null;
282                 }
283 
284                 Decode(encoded);
285                 Debug.Assert(_heldContent != null);
286             }
287             finally
288             {
289                 ContentInfo = save;
290             }
291         }
292 
UpdateMetadata()293         private void UpdateMetadata()
294         {
295             // Version 5: any certificate of type Other or CRL of type Other. We don't support this.
296             // Version 4: any certificates are V2 attribute certificates.  We don't support this.
297             // Version 3a: any certificates are V1 attribute certificates. We don't support this.
298             // Version 3b: any signerInfos are v3
299             // Version 3c: eContentType != data
300             // Version 2: does not exist for signed-data
301             // Version 1: default
302 
303             // The versions 3 are OR conditions, so we need to check the content type and the signerinfos.
304             int version = 1;
305 
306             if ((_contentType ?? ContentInfo.ContentType.Value) != Oids.Pkcs7Data)
307             {
308                 version = 3;
309             }
310             else if (_signedData.SignerInfos.Any(si => si.Version == 3))
311             {
312                 version = 3;
313             }
314 
315             Version = version;
316             _signedData.Version = version;
317         }
318 
ConsiderDigestAddition(AlgorithmIdentifierAsn candidate)319         private void ConsiderDigestAddition(AlgorithmIdentifierAsn candidate)
320         {
321             int curLength = _signedData.DigestAlgorithms.Length;
322 
323             for (int i = 0; i < curLength; i++)
324             {
325                 ref AlgorithmIdentifierAsn alg = ref _signedData.DigestAlgorithms[i];
326 
327                 if (candidate.Equals(ref alg))
328                 {
329                     return;
330                 }
331             }
332 
333             Array.Resize(ref _signedData.DigestAlgorithms, curLength + 1);
334             _signedData.DigestAlgorithms[curLength] = candidate;
335         }
336 
ConsiderDigestRemoval(AlgorithmIdentifierAsn candidate)337         private void ConsiderDigestRemoval(AlgorithmIdentifierAsn candidate)
338         {
339             bool remove = true;
340 
341             for (int i = 0; i < _signedData.SignerInfos.Length; i++)
342             {
343                 ref AlgorithmIdentifierAsn signerAlg = ref _signedData.SignerInfos[i].DigestAlgorithm;
344 
345                 if (candidate.Equals(ref signerAlg))
346                 {
347                     remove = false;
348                     break;
349                 }
350             }
351 
352             if (!remove)
353             {
354                 return;
355             }
356 
357             for (int i = 0; i < _signedData.DigestAlgorithms.Length; i++)
358             {
359                 ref AlgorithmIdentifierAsn alg = ref _signedData.DigestAlgorithms[i];
360 
361                 if (candidate.Equals(ref alg))
362                 {
363                     Helpers.RemoveAt(ref _signedData.DigestAlgorithms, i);
364                     break;
365                 }
366             }
367         }
368 
UpdateCertificatesFromAddition(X509Certificate2Collection newCerts)369         internal void UpdateCertificatesFromAddition(X509Certificate2Collection newCerts)
370         {
371             if (newCerts.Count == 0)
372             {
373                 return;
374             }
375 
376             int existingLength = _signedData.CertificateSet?.Length ?? 0;
377 
378             if (existingLength > 0 || newCerts.Count > 1)
379             {
380                 var certs = new HashSet<X509Certificate2>(Certificates.OfType<X509Certificate2>());
381 
382                 for (int i = 0; i < newCerts.Count; i++)
383                 {
384                     X509Certificate2 candidate = newCerts[i];
385 
386                     if (!certs.Add(candidate))
387                     {
388                         newCerts.RemoveAt(i);
389                         i--;
390                     }
391                 }
392             }
393 
394             if (newCerts.Count == 0)
395             {
396                 return;
397             }
398 
399             if (_signedData.CertificateSet == null)
400             {
401                 _signedData.CertificateSet = new CertificateChoiceAsn[newCerts.Count];
402             }
403             else
404             {
405                 Array.Resize(ref _signedData.CertificateSet, existingLength + newCerts.Count);
406             }
407 
408             for (int i = existingLength; i < _signedData.CertificateSet.Length; i++)
409             {
410                 _signedData.CertificateSet[i] = new CertificateChoiceAsn
411                 {
412                     Certificate = newCerts[i - existingLength].RawData
413                 };
414             }
415         }
416 
417         public void CheckSignature(bool verifySignatureOnly) =>
418             CheckSignature(new X509Certificate2Collection(), verifySignatureOnly);
419 
CheckSignature(X509Certificate2Collection extraStore, bool verifySignatureOnly)420         public void CheckSignature(X509Certificate2Collection extraStore, bool verifySignatureOnly)
421         {
422             if (!_hasData)
423                 throw new InvalidOperationException(SR.Cryptography_Cms_MessageNotSigned);
424             if (extraStore == null)
425                 throw new ArgumentNullException(nameof(extraStore));
426 
427             CheckSignatures(SignerInfos, extraStore, verifySignatureOnly);
428         }
429 
CheckSignatures( SignerInfoCollection signers, X509Certificate2Collection extraStore, bool verifySignatureOnly)430         private static void CheckSignatures(
431             SignerInfoCollection signers,
432             X509Certificate2Collection extraStore,
433             bool verifySignatureOnly)
434         {
435             Debug.Assert(signers != null);
436 
437             if (signers.Count < 1)
438             {
439                 throw new CryptographicException(SR.Cryptography_Cms_NoSignerAtIndex);
440             }
441 
442             foreach (SignerInfo signer in signers)
443             {
444                 signer.CheckSignature(extraStore, verifySignatureOnly);
445 
446                 SignerInfoCollection counterSigners = signer.CounterSignerInfos;
447 
448                 if (counterSigners.Count > 0)
449                 {
450                     CheckSignatures(counterSigners, extraStore, verifySignatureOnly);
451                 }
452             }
453         }
454 
CheckHash()455         public void CheckHash()
456         {
457             if (!_hasData)
458                 throw new InvalidOperationException(SR.Cryptography_Cms_MessageNotSigned);
459 
460             SignerInfoCollection signers = SignerInfos;
461             Debug.Assert(signers != null);
462 
463             if (signers.Count < 1)
464             {
465                 throw new CryptographicException(SR.Cryptography_Cms_NoSignerAtIndex);
466             }
467 
468             foreach (SignerInfo signer in signers)
469             {
470                 if (signer.SignerIdentifier.Type == SubjectIdentifierType.NoSignature)
471                 {
472                     signer.CheckHash();
473                 }
474             }
475         }
476 
GetRawData()477         internal ref SignedDataAsn GetRawData()
478         {
479             return ref _signedData;
480         }
481     }
482 }
483