Skip to content

Commit

Permalink
Implement PKCS12 Export in terms of Pkcs12Builder for non-Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
vcsjones authored Jan 27, 2025
1 parent eb73fc6 commit c1ed07c
Show file tree
Hide file tree
Showing 32 changed files with 673 additions and 823 deletions.
346 changes: 346 additions & 0 deletions src/libraries/Common/src/Internal/Cryptography/PkcsHelpers.cs
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),
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<asn:Sequence
xmlns:asn="http://schemas.dot.net/asnxml/201808/"
name="SecretBagAsn"
namespace="System.Security.Cryptography.Pkcs.Asn1">
namespace="System.Security.Cryptography.Asn1.Pkcs12">

<!--
https://tools.ietf.org/html/rfc7292#section-4.2.5
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Formats.Asn1;
using System.Runtime.InteropServices;

namespace System.Security.Cryptography.Pkcs.Asn1
namespace System.Security.Cryptography.Asn1.Pkcs12
{
[StructLayout(LayoutKind.Sequential)]
internal partial struct SecretBagAsn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

namespace System.Security.Cryptography
{
public sealed class CryptographicAttributeObject
#if BUILDING_PKCS
public
#else
#pragma warning disable CA1510, CA1512
internal
#endif
sealed class CryptographicAttributeObject
{
//
// Constructors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@

namespace System.Security.Cryptography
{
public sealed class CryptographicAttributeObjectCollection : ICollection
#if BUILDING_PKCS
public
#else
#pragma warning disable CA1510, CA1512
internal
#endif
sealed class CryptographicAttributeObjectCollection : ICollection
{
public CryptographicAttributeObjectCollection()
{
Expand Down
Loading

0 comments on commit c1ed07c

Please sign in to comment.