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;
6 using System.Buffers;
7 using System.Text;
8 using System.Diagnostics;
9 using System.Runtime.InteropServices;
10 using System.Security.Cryptography;
11 using System.Security.Cryptography.Asn1;
12 using System.Security.Cryptography.Pkcs;
13 using System.Security.Cryptography.Pkcs.Asn1;
14 using System.Security.Cryptography.X509Certificates;
15 using X509IssuerSerial = System.Security.Cryptography.Xml.X509IssuerSerial;
16 
17 namespace Internal.Cryptography
18 {
19     internal static class Helpers
20     {
CloneByteArray(this byte[] a)21         public static byte[] CloneByteArray(this byte[] a)
22         {
23             return (byte[])(a.Clone());
24         }
25 
26 #if !netcoreapp
27         // Compatibility API.
AppendData(this IncrementalHash hasher, ReadOnlySpan<byte> data)28         internal static void AppendData(this IncrementalHash hasher, ReadOnlySpan<byte> data)
29         {
30             hasher.AppendData(data.ToArray());
31         }
32 #endif
33 
GetDigestAlgorithm(Oid oid)34         internal static HashAlgorithmName GetDigestAlgorithm(Oid oid)
35         {
36             Debug.Assert(oid != null);
37             return GetDigestAlgorithm(oid.Value);
38         }
39 
GetDigestAlgorithm(string oidValue)40         internal static HashAlgorithmName GetDigestAlgorithm(string oidValue)
41         {
42             switch (oidValue)
43             {
44                 case Oids.Md5:
45                     return HashAlgorithmName.MD5;
46                 case Oids.Sha1:
47                     return HashAlgorithmName.SHA1;
48                 case Oids.Sha256:
49                     return HashAlgorithmName.SHA256;
50                 case Oids.Sha384:
51                     return HashAlgorithmName.SHA384;
52                 case Oids.Sha512:
53                     return HashAlgorithmName.SHA512;
54                 default:
55                     throw new CryptographicException(SR.Cryptography_UnknownHashAlgorithm, oidValue);
56             }
57         }
58 
59         /// <summary>
60         /// This is not just a convenience wrapper for Array.Resize(). In DEBUG builds, it forces the array to move in memory even if no resize is needed. This should be used by
61         /// helper methods that do anything of the form "call a native api once to get the estimated size, call it again to get the data and return the data in a byte[] array."
62         /// Sometimes, that data consist of a native data structure containing pointers to other parts of the block. Using such a helper to retrieve such a block results in an intermittent
63         /// AV. By using this helper, you make that AV repro every time.
64         /// </summary>
Resize(this byte[] a, int size)65         public static byte[] Resize(this byte[] a, int size)
66         {
67             Array.Resize(ref a, size);
68 #if DEBUG
69             a = a.CloneByteArray();
70 #endif
71             return a;
72         }
73 
RemoveAt(ref T[] arr, int idx)74         public static void RemoveAt<T>(ref T[] arr, int idx)
75         {
76             Debug.Assert(arr != null);
77             Debug.Assert(idx >= 0);
78             Debug.Assert(idx < arr.Length);
79 
80             if (arr.Length == 1)
81             {
82                 arr = Array.Empty<T>();
83                 return;
84             }
85 
86             T[] tmp = new T[arr.Length - 1];
87 
88             if (idx != 0)
89             {
90                 Array.Copy(arr, 0, tmp, 0, idx);
91             }
92 
93             if (idx < tmp.Length)
94             {
95                 Array.Copy(arr, idx + 1, tmp, idx, tmp.Length - idx);
96             }
97 
98             arr = tmp;
99         }
100 
NormalizeSet( T[] setItems, Action<byte[]> encodedValueProcessor=null)101         public static T[] NormalizeSet<T>(
102             T[] setItems,
103             Action<byte[]> encodedValueProcessor=null)
104         {
105             AsnSet<T> set = new AsnSet<T>
106             {
107                 SetData = setItems,
108             };
109 
110             AsnWriter writer = AsnSerializer.Serialize(set, AsnEncodingRules.DER);
111             byte[] normalizedValue = writer.Encode();
112             set = AsnSerializer.Deserialize<AsnSet<T>>(normalizedValue, AsnEncodingRules.DER);
113 
114             if (encodedValueProcessor != null)
115             {
116                 encodedValueProcessor(normalizedValue);
117             }
118 
119             return set.SetData;
120         }
121 
DeepCopy(this CmsRecipientCollection recipients)122         public static CmsRecipientCollection DeepCopy(this CmsRecipientCollection recipients)
123         {
124             CmsRecipientCollection recipientsCopy = new CmsRecipientCollection();
125             foreach (CmsRecipient recipient in recipients)
126             {
127                 X509Certificate2 originalCert = recipient.Certificate;
128                 X509Certificate2 certCopy = new X509Certificate2(originalCert.Handle);
129                 CmsRecipient recipientCopy = new CmsRecipient(recipient.RecipientIdentifierType, certCopy);
130                 recipientsCopy.Add(recipientCopy);
131                 GC.KeepAlive(originalCert);
132             }
133             return recipientsCopy;
134         }
135 
UnicodeToOctetString(this string s)136         public static byte[] UnicodeToOctetString(this string s)
137         {
138             byte[] octets = new byte[2 * (s.Length + 1)];
139             Encoding.Unicode.GetBytes(s, 0, s.Length, octets, 0);
140             return octets;
141         }
142 
OctetStringToUnicode(this byte[] octets)143         public static string OctetStringToUnicode(this byte[] octets)
144         {
145             if (octets.Length < 2)
146                 return string.Empty;   // Desktop compat: 0-length byte array maps to string.empty. 1-length byte array gets passed to Marshal.PtrToStringUni() with who knows what outcome.
147 
148             string s = Encoding.Unicode.GetString(octets, 0, octets.Length - 2);
149             return s;
150         }
151 
GetStoreCertificates(StoreName storeName, StoreLocation storeLocation, bool openExistingOnly)152         public static X509Certificate2Collection GetStoreCertificates(StoreName storeName, StoreLocation storeLocation, bool openExistingOnly)
153         {
154             using (X509Store store = new X509Store())
155             {
156                 OpenFlags flags = OpenFlags.ReadOnly | OpenFlags.IncludeArchived;
157                 if (openExistingOnly)
158                     flags |= OpenFlags.OpenExistingOnly;
159 
160                 store.Open(flags);
161                 X509Certificate2Collection certificates = store.Certificates;
162                 return certificates;
163             }
164         }
165 
166         /// <summary>
167         /// Desktop compat: We do not complain about multiple matches. Just take the first one and ignore the rest.
168         /// </summary>
TryFindMatchingCertificate(this X509Certificate2Collection certs, SubjectIdentifier recipientIdentifier)169         public static X509Certificate2 TryFindMatchingCertificate(this X509Certificate2Collection certs, SubjectIdentifier recipientIdentifier)
170         {
171             //
172             // Note: SubjectIdentifier has no public constructor so the only one that can construct this type is this assembly.
173             //       Therefore, we trust that the string-ized byte array (serial or ski) in it is correct and canonicalized.
174             //
175 
176             SubjectIdentifierType recipientIdentifierType = recipientIdentifier.Type;
177             switch (recipientIdentifierType)
178             {
179                 case SubjectIdentifierType.IssuerAndSerialNumber:
180                     {
181                         X509IssuerSerial issuerSerial = (X509IssuerSerial)(recipientIdentifier.Value);
182                         byte[] serialNumber = issuerSerial.SerialNumber.ToSerialBytes();
183                         string issuer = issuerSerial.IssuerName;
184                         foreach (X509Certificate2 candidate in certs)
185                         {
186                             byte[] candidateSerialNumber = candidate.GetSerialNumber();
187                             if (AreByteArraysEqual(candidateSerialNumber, serialNumber) && candidate.Issuer == issuer)
188                                 return candidate;
189                         }
190                     }
191                     break;
192 
193                 case SubjectIdentifierType.SubjectKeyIdentifier:
194                     {
195                         string skiString = (string)(recipientIdentifier.Value);
196                         byte[] ski = skiString.ToSkiBytes();
197                         foreach (X509Certificate2 cert in certs)
198                         {
199                             byte[] candidateSki = PkcsPal.Instance.GetSubjectKeyIdentifier(cert);
200                             if (AreByteArraysEqual(ski, candidateSki))
201                                 return cert;
202                         }
203                     }
204                     break;
205 
206                 default:
207                     // RecipientInfo's can only be created by this package so if this an invalid type, it's the package's fault.
208                     Debug.Fail($"Invalid recipientIdentifier type: {recipientIdentifierType}");
209                     throw new CryptographicException();
210             }
211             return null;
212         }
213 
AreByteArraysEqual(byte[] ba1, byte[] ba2)214         private static bool AreByteArraysEqual(byte[] ba1, byte[] ba2)
215         {
216             if (ba1.Length != ba2.Length)
217                 return false;
218 
219             for (int i = 0; i < ba1.Length; i++)
220             {
221                 if (ba1[i] != ba2[i])
222                     return false;
223             }
224             return true;
225         }
226 
227         /// <summary>
228         /// Asserts on bad or non-canonicalized input. Input must come from trusted sources.
229         ///
230         /// Subject Key Identifier is string-ized as an upper case hex string. This format is part of the public api behavior and cannot be changed.
231         /// </summary>
ToSkiBytes(this string skiString)232         private static byte[] ToSkiBytes(this string skiString)
233         {
234             return skiString.UpperHexStringToByteArray();
235         }
236 
ToSkiString(this ReadOnlySpan<byte> skiBytes)237         public static string ToSkiString(this ReadOnlySpan<byte> skiBytes)
238         {
239             return ToUpperHexString(skiBytes);
240         }
241 
ToSkiString(this byte[] skiBytes)242         public static string ToSkiString(this byte[] skiBytes)
243         {
244             return ToUpperHexString(skiBytes);
245         }
246 
247         /// <summary>
248         /// Asserts on bad or non-canonicalized input. Input must come from trusted sources.
249         ///
250         /// Serial number is string-ized as a reversed upper case hex string. This format is part of the public api behavior and cannot be changed.
251         /// </summary>
ToSerialBytes(this string serialString)252         private static byte[] ToSerialBytes(this string serialString)
253         {
254             byte[] ba = serialString.UpperHexStringToByteArray();
255             Array.Reverse(ba);
256             return ba;
257         }
258 
ToSerialString(this byte[] serialBytes)259         public static string ToSerialString(this byte[] serialBytes)
260         {
261             serialBytes = serialBytes.CloneByteArray();
262             Array.Reverse(serialBytes);
263             return ToUpperHexString(serialBytes);
264         }
265 
ToUpperHexString(ReadOnlySpan<byte> ba)266         private static string ToUpperHexString(ReadOnlySpan<byte> ba)
267         {
268             StringBuilder sb = new StringBuilder(ba.Length * 2);
269 
270             for (int i = 0; i < ba.Length; i++)
271             {
272                 sb.Append(ba[i].ToString("X2"));
273             }
274 
275             return sb.ToString();
276         }
277 
278         /// <summary>
279         /// Asserts on bad input. Input must come from trusted sources.
280         /// </summary>
UpperHexStringToByteArray(this string normalizedString)281         private static byte[] UpperHexStringToByteArray(this string normalizedString)
282         {
283             Debug.Assert((normalizedString.Length & 0x1) == 0);
284 
285             byte[] ba = new byte[normalizedString.Length / 2];
286             for (int i = 0; i < ba.Length; i++)
287             {
288                 char c = normalizedString[i * 2];
289                 byte b = (byte)(UpperHexCharToNybble(c) << 4);
290                 c = normalizedString[i * 2 + 1];
291                 b |= UpperHexCharToNybble(c);
292                 ba[i] = b;
293             }
294             return ba;
295         }
296 
297         /// <summary>
298         /// Asserts on bad input. Input must come from trusted sources.
299         /// </summary>
UpperHexCharToNybble(char c)300         private static byte UpperHexCharToNybble(char c)
301         {
302             if (c >= '0' && c <= '9')
303                 return (byte)(c - '0');
304             if (c >= 'A' && c <= 'F')
305                 return (byte)(c - 'A' + 10);
306 
307             Debug.Fail($"Invalid hex character: {c}");
308             throw new CryptographicException();  // This just keeps the compiler happy. We don't expect to reach this.
309         }
310 
311         /// <summary>
312         /// Useful helper for "upgrading" well-known CMS attributes to type-specific objects such as Pkcs9DocumentName, Pkcs9DocumentDescription, etc.
313         /// </summary>
CreateBestPkcs9AttributeObjectAvailable(Oid oid, byte[] encodedAttribute)314         public static Pkcs9AttributeObject CreateBestPkcs9AttributeObjectAvailable(Oid oid, byte[] encodedAttribute)
315         {
316             Pkcs9AttributeObject attributeObject = new Pkcs9AttributeObject(oid, encodedAttribute);
317             switch (oid.Value)
318             {
319                 case Oids.DocumentName:
320                     attributeObject = Upgrade<Pkcs9DocumentName>(attributeObject);
321                     break;
322 
323                 case Oids.DocumentDescription:
324                     attributeObject = Upgrade<Pkcs9DocumentDescription>(attributeObject);
325                     break;
326 
327                 case Oids.SigningTime:
328                     attributeObject = Upgrade<Pkcs9SigningTime>(attributeObject);
329                     break;
330 
331                 case Oids.ContentType:
332                     attributeObject = Upgrade<Pkcs9ContentType>(attributeObject);
333                     break;
334 
335                 case Oids.MessageDigest:
336                     attributeObject = Upgrade<Pkcs9MessageDigest>(attributeObject);
337                     break;
338 
339                 default:
340                     break;
341             }
342             return attributeObject;
343         }
344 
345         private static T Upgrade<T>(Pkcs9AttributeObject basicAttribute) where T : Pkcs9AttributeObject, new()
346         {
347             T enhancedAttribute = new T();
348             enhancedAttribute.CopyFrom(basicAttribute);
349             return enhancedAttribute;
350         }
351 
GetSubjectKeyIdentifier(this X509Certificate2 certificate)352         public static byte[] GetSubjectKeyIdentifier(this X509Certificate2 certificate)
353         {
354             Debug.Assert(certificate != null);
355 
356             X509Extension extension = certificate.Extensions[Oids.SubjectKeyIdentifier];
357 
358             if (extension != null)
359             {
360                 // Certificates are DER encoded.
361                 AsnReader reader = new AsnReader(extension.RawData, AsnEncodingRules.DER);
362 
363                 if (reader.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> contents))
364                 {
365                     return contents.ToArray();
366                 }
367 
368                 // TryGetPrimitiveOctetStringBytes will have thrown if the next tag wasn't
369                 // Universal (primitive) OCTET STRING, since we're in DER mode.
370                 // So there's really no way we can get here.
371                 Debug.Fail($"TryGetPrimitiveOctetStringBytes returned false in DER mode");
372                 throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
373             }
374 
375             // The Desktop/Windows version of this method use CertGetCertificateContextProperty
376             // with a property ID of CERT_KEY_IDENTIFIER_PROP_ID.
377             //
378             // MSDN says that when there's no extension, this method takes the SHA-1 of the
379             // SubjectPublicKeyInfo block, and returns that.
380             //
381             // https://msdn.microsoft.com/en-us/library/windows/desktop/aa376079%28v=vs.85%29.aspx
382 
383 #pragma warning disable CA5350 // SHA-1 is required for compat.
384             using (HashAlgorithm hash = SHA1.Create())
385 #pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1.
386             {
387                 ReadOnlyMemory<byte> publicKeyInfoBytes = GetSubjectPublicKeyInfo(certificate);
388                 return hash.ComputeHash(publicKeyInfoBytes.ToArray());
389             }
390         }
391 
DigestWriter(IncrementalHash hasher, AsnWriter writer)392         internal static void DigestWriter(IncrementalHash hasher, AsnWriter writer)
393         {
394 #if netcoreapp
395             hasher.AppendData(writer.EncodeAsSpan());
396 #else
397             hasher.AppendData(writer.Encode());
398 #endif
399         }
400 
GetSubjectPublicKeyInfo(X509Certificate2 certificate)401         private static ReadOnlyMemory<byte> GetSubjectPublicKeyInfo(X509Certificate2 certificate)
402         {
403             var parsedCertificate = AsnSerializer.Deserialize<Certificate>(certificate.RawData, AsnEncodingRules.DER);
404             return parsedCertificate.TbsCertificate.SubjectPublicKeyInfo;
405         }
406 
407         [StructLayout(LayoutKind.Sequential)]
408         private struct Certificate
409         {
410             internal TbsCertificateLite TbsCertificate;
411             internal AlgorithmIdentifierAsn AlgorithmIdentifier;
412             [BitString]
413             internal ReadOnlyMemory<byte> SignatureValue;
414         }
415 
416         [StructLayout(LayoutKind.Sequential)]
417         private struct TbsCertificateLite
418         {
419             [ExpectedTag(0, ExplicitTag = true)]
420 #pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant
421             [DefaultValue(0xA0, 0x03, 0x02, 0x01, 0x00)]
422 #pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant
423             internal int Version;
424 
425             [Integer]
426             internal ReadOnlyMemory<byte> SerialNumber;
427 
428             internal AlgorithmIdentifierAsn AlgorithmIdentifier;
429 
430             [AnyValue]
431             [ExpectedTag(TagClass.Universal, (int)UniversalTagNumber.SequenceOf)]
432             internal ReadOnlyMemory<byte> Issuer;
433 
434             [AnyValue]
435             [ExpectedTag(TagClass.Universal, (int)UniversalTagNumber.Sequence)]
436             internal ReadOnlyMemory<byte> Validity;
437 
438             [AnyValue]
439             [ExpectedTag(TagClass.Universal, (int)UniversalTagNumber.SequenceOf)]
440             internal ReadOnlyMemory<byte> Subject;
441 
442             [AnyValue]
443             [ExpectedTag(TagClass.Universal, (int)UniversalTagNumber.Sequence)]
444             internal ReadOnlyMemory<byte> SubjectPublicKeyInfo;
445 
446             [ExpectedTag(1)]
447             [OptionalValue]
448             [BitString]
449             internal ReadOnlyMemory<byte>? IssuerUniqueId;
450 
451             [ExpectedTag(2)]
452             [OptionalValue]
453             [BitString]
454             internal ReadOnlyMemory<byte>? SubjectUniqueId;
455 
456             [OptionalValue]
457             [AnyValue]
458             [ExpectedTag(3)]
459             internal ReadOnlyMemory<byte>? Extensions;
460         }
461 
462         [StructLayout(LayoutKind.Sequential)]
463         internal struct AsnSet<T>
464         {
465             [SetOf]
466             public T[] SetData;
467         }
468     }
469 }
470