-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement PKCS12 Export in terms of Pkcs12Builder for non-Windows
- Loading branch information
Showing
32 changed files
with
673 additions
and
823 deletions.
There are no files selected for viewing
346 changes: 346 additions & 0 deletions
346
src/libraries/Common/src/Internal/Cryptography/PkcsHelpers.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,346 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Formats.Asn1; | ||
using System.Security.Cryptography; | ||
using System.Security.Cryptography.Asn1; | ||
using System.Security.Cryptography.Pkcs; | ||
using System.Text; | ||
|
||
namespace Internal.Cryptography | ||
{ | ||
internal static partial class PkcsHelpers | ||
{ | ||
#if BUILDING_PKCS | ||
private static readonly bool s_oidIsInitOnceOnly = DetectInitOnlyOid(); | ||
|
||
private static bool DetectInitOnlyOid() | ||
{ | ||
Oid testOid = new Oid(Oids.Sha256, null); | ||
|
||
try | ||
{ | ||
testOid.Value = Oids.Sha384; | ||
return false; | ||
} | ||
catch (PlatformNotSupportedException) | ||
{ | ||
return true; | ||
} | ||
} | ||
#endif | ||
|
||
internal static List<AttributeAsn> BuildAttributes(CryptographicAttributeObjectCollection? attributes) | ||
{ | ||
List<AttributeAsn> signedAttrs = new List<AttributeAsn>(); | ||
|
||
if (attributes == null || attributes.Count == 0) | ||
{ | ||
return signedAttrs; | ||
} | ||
|
||
foreach (CryptographicAttributeObject attributeObject in attributes) | ||
{ | ||
AttributeAsn newAttr = new AttributeAsn | ||
{ | ||
AttrType = attributeObject.Oid!.Value!, | ||
AttrValues = new ReadOnlyMemory<byte>[attributeObject.Values.Count], | ||
}; | ||
|
||
for (int i = 0; i < attributeObject.Values.Count; i++) | ||
{ | ||
newAttr.AttrValues[i] = attributeObject.Values[i].RawData; | ||
} | ||
|
||
signedAttrs.Add(newAttr); | ||
} | ||
|
||
return signedAttrs; | ||
} | ||
|
||
public static void EnsureSingleBerValue(ReadOnlySpan<byte> source) | ||
{ | ||
if (!AsnDecoder.TryReadEncodedValue(source, AsnEncodingRules.BER, out _, out _, out _, out int consumed) || | ||
consumed != source.Length) | ||
{ | ||
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); | ||
} | ||
} | ||
|
||
public static byte[] EncodeOctetString(byte[] octets) | ||
{ | ||
// Write using DER to support the most readers. | ||
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); | ||
writer.WriteOctetString(octets); | ||
return writer.Encode(); | ||
} | ||
|
||
public static byte[] DecodeOctetString(ReadOnlyMemory<byte> encodedOctets) | ||
{ | ||
try | ||
{ | ||
// Read using BER because the CMS specification says the encoding is BER. | ||
byte[] ret = AsnDecoder.ReadOctetString(encodedOctets.Span, AsnEncodingRules.BER, out int consumed); | ||
|
||
if (consumed != encodedOctets.Length) | ||
{ | ||
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); | ||
} | ||
|
||
return ret; | ||
} | ||
catch (AsnContentException e) | ||
{ | ||
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); | ||
} | ||
} | ||
|
||
public static string DecodeOid(ReadOnlySpan<byte> encodedOid) | ||
{ | ||
// Windows compat for a zero length OID. | ||
if (encodedOid.Length == 2 && encodedOid[0] == 0x06 && encodedOid[1] == 0x00) | ||
{ | ||
return string.Empty; | ||
} | ||
|
||
// Read using BER because the CMS specification says the encoding is BER. | ||
try | ||
{ | ||
string value = AsnDecoder.ReadObjectIdentifier( | ||
encodedOid, | ||
AsnEncodingRules.BER, | ||
out int consumed); | ||
|
||
if (consumed != encodedOid.Length) | ||
{ | ||
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); | ||
} | ||
|
||
return value; | ||
} | ||
catch (AsnContentException e) | ||
{ | ||
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); | ||
} | ||
} | ||
|
||
public static byte[] UnicodeToOctetString(this string s) | ||
{ | ||
byte[] octets = new byte[2 * (s.Length + 1)]; | ||
Encoding.Unicode.GetBytes(s, 0, s.Length, octets, 0); | ||
return octets; | ||
} | ||
|
||
public static string OctetStringToUnicode(this byte[] octets) | ||
{ | ||
if (octets.Length < 2) | ||
return string.Empty; // .NET Framework compat: 0-length byte array maps to string.empty. 1-length byte array gets passed to Marshal.PtrToStringUni() with who knows what outcome. | ||
|
||
int end = octets.Length; | ||
int endMinusOne = end - 1; | ||
|
||
// Truncate the string to before the first embedded \0 (probably the last two bytes). | ||
for (int i = 0; i < endMinusOne; i += 2) | ||
{ | ||
if (octets[i] == 0 && octets[i + 1] == 0) | ||
{ | ||
end = i; | ||
break; | ||
} | ||
} | ||
|
||
string s = Encoding.Unicode.GetString(octets, 0, end); | ||
return s; | ||
} | ||
|
||
public static ReadOnlyMemory<byte> DecodeOctetStringAsMemory(ReadOnlyMemory<byte> encodedOctetString) | ||
{ | ||
#if BUILDING_PKCS | ||
try | ||
{ | ||
ReadOnlySpan<byte> input = encodedOctetString.Span; | ||
|
||
if (AsnDecoder.TryReadPrimitiveOctetString( | ||
input, | ||
AsnEncodingRules.BER, | ||
out ReadOnlySpan<byte> primitive, | ||
out int consumed)) | ||
{ | ||
if (consumed != input.Length) | ||
{ | ||
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); | ||
} | ||
|
||
if (input.Overlaps(primitive, out int offset)) | ||
{ | ||
return encodedOctetString.Slice(offset, primitive.Length); | ||
} | ||
|
||
Debug.Fail("input.Overlaps(primitive) failed after TryReadPrimitiveOctetString succeeded"); | ||
} | ||
|
||
byte[] ret = AsnDecoder.ReadOctetString(input, AsnEncodingRules.BER, out consumed); | ||
|
||
if (consumed != input.Length) | ||
{ | ||
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); | ||
} | ||
|
||
return ret; | ||
} | ||
catch (AsnContentException e) | ||
{ | ||
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); | ||
} | ||
#else | ||
return Helpers.DecodeOctetStringAsMemory(encodedOctetString); | ||
#endif | ||
} | ||
|
||
internal static string GetOidFromHashAlgorithm(HashAlgorithmName algName) | ||
{ | ||
if (algName == HashAlgorithmName.MD5) | ||
return Oids.Md5; | ||
if (algName == HashAlgorithmName.SHA1) | ||
return Oids.Sha1; | ||
if (algName == HashAlgorithmName.SHA256) | ||
return Oids.Sha256; | ||
if (algName == HashAlgorithmName.SHA384) | ||
return Oids.Sha384; | ||
if (algName == HashAlgorithmName.SHA512) | ||
return Oids.Sha512; | ||
#if NET8_0_OR_GREATER | ||
if (algName == HashAlgorithmName.SHA3_256) | ||
return Oids.Sha3_256; | ||
if (algName == HashAlgorithmName.SHA3_384) | ||
return Oids.Sha3_384; | ||
if (algName == HashAlgorithmName.SHA3_512) | ||
return Oids.Sha3_512; | ||
#endif | ||
|
||
throw new CryptographicException(SR.Cryptography_Cms_UnknownAlgorithm, algName.Name); | ||
} | ||
|
||
// Creates a defensive copy of an OID on platforms where OID | ||
// is mutable. On platforms where OID is immutable, return the OID as-is. | ||
[return: NotNullIfNotNull(nameof(oid))] | ||
public static Oid? CopyOid(this Oid? oid) | ||
{ | ||
#if BUILDING_PKCS | ||
if (s_oidIsInitOnceOnly) | ||
{ | ||
return oid; | ||
} | ||
else | ||
{ | ||
return oid is null ? null : new Oid(oid); | ||
} | ||
#else | ||
return oid; // If we are in System.Security.Cryptography, then Oids are known to be immutable. | ||
#endif | ||
} | ||
|
||
internal static CryptographicAttributeObjectCollection MakeAttributeCollection(AttributeAsn[]? attributes) | ||
{ | ||
var coll = new CryptographicAttributeObjectCollection(); | ||
|
||
if (attributes == null) | ||
return coll; | ||
|
||
foreach (AttributeAsn attribute in attributes) | ||
{ | ||
coll.AddWithoutMerge(MakeAttribute(attribute)); | ||
} | ||
|
||
return coll; | ||
} | ||
|
||
internal static CryptographicAttributeObject MakeAttribute(AttributeAsn attribute) | ||
{ | ||
Oid type = new Oid(attribute.AttrType); | ||
AsnEncodedDataCollection valueColl = new AsnEncodedDataCollection(); | ||
|
||
foreach (ReadOnlyMemory<byte> attrValue in attribute.AttrValues) | ||
{ | ||
valueColl.Add(CreateBestPkcs9AttributeObjectAvailable(type, attrValue.ToArray())); | ||
} | ||
|
||
return new CryptographicAttributeObject(type, valueColl); | ||
} | ||
|
||
public static byte[] EncodeUtcTime(DateTime utcTime) | ||
{ | ||
const int maxLegalYear = 2049; | ||
// Write using DER to support the most readers. | ||
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); | ||
|
||
try | ||
{ | ||
// Sending the DateTime through ToLocalTime here will cause the right normalization | ||
// of DateTimeKind.Unknown. | ||
// | ||
// Unknown => Local (adjust) => UTC (adjust "back", add Z marker; matches Windows) | ||
if (utcTime.Kind == DateTimeKind.Unspecified) | ||
{ | ||
writer.WriteUtcTime(utcTime.ToLocalTime(), maxLegalYear); | ||
} | ||
else | ||
{ | ||
writer.WriteUtcTime(utcTime, maxLegalYear); | ||
} | ||
|
||
return writer.Encode(); | ||
} | ||
catch (ArgumentException ex) | ||
{ | ||
throw new CryptographicException(ex.Message, ex); | ||
} | ||
} | ||
|
||
public static DateTime DecodeUtcTime(byte[] encodedUtcTime) | ||
{ | ||
// Read using BER because the CMS specification says the encoding is BER. | ||
try | ||
{ | ||
DateTimeOffset value = AsnDecoder.ReadUtcTime( | ||
encodedUtcTime, | ||
AsnEncodingRules.BER, | ||
out int consumed); | ||
|
||
if (consumed != encodedUtcTime.Length) | ||
{ | ||
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); | ||
} | ||
|
||
return value.UtcDateTime; | ||
} | ||
catch (AsnContentException e) | ||
{ | ||
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Useful helper for "upgrading" well-known CMS attributes to type-specific objects such as Pkcs9DocumentName, Pkcs9DocumentDescription, etc. | ||
/// </summary> | ||
public static Pkcs9AttributeObject CreateBestPkcs9AttributeObjectAvailable(Oid oid, ReadOnlySpan<byte> encodedAttribute) | ||
{ | ||
return oid.Value switch | ||
{ | ||
Oids.DocumentName => new Pkcs9DocumentName(encodedAttribute), | ||
Oids.DocumentDescription => new Pkcs9DocumentDescription(encodedAttribute), | ||
Oids.SigningTime => new Pkcs9SigningTime(encodedAttribute), | ||
Oids.ContentType => new Pkcs9ContentType(encodedAttribute), | ||
Oids.MessageDigest => new Pkcs9MessageDigest(encodedAttribute), | ||
#if NET || NETSTANDARD2_1 | ||
Oids.LocalKeyId => new Pkcs9LocalKeyId() { RawData = encodedAttribute.ToArray() }, | ||
#endif | ||
_ => new Pkcs9AttributeObject(oid, encodedAttribute), | ||
}; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.