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