未验证 提交 321cec85 编写于 作者: D David Cantú 提交者: GitHub

Use ROS<byte> instead of byte[] where it makes sense on S.S.C.Cose (#66741)

* Use ROS<byte> instead of byte[] on S.S.C.Cose

* Add TrySign and improve Sign implementation to use less byte[]

* Add TrySign tests

* Address using scope feedback

* Address src feedback

* Refactor tests to avoid duplicated ones

* Remove invalid asserts in Crypto code

* Fix 'new()' without the type on the left-hand side

* Fix ThreadStatic issues in tests

* * Don't use ArrayPool in SignCore
* Add comment describing reusability of encoded protected headers
* Cache toBeSigned

* Don't cache toBeSigned for detached content

* Address nits in tests
上级 3cec42fa
......@@ -56,17 +56,22 @@ public abstract partial class CoseMessage
public System.Security.Cryptography.Cose.CoseHeaderMap ProtectedHeaders { get { throw null; } }
public System.Security.Cryptography.Cose.CoseHeaderMap UnprotectedHeaders { get { throw null; } }
public static System.Security.Cryptography.Cose.CoseSign1Message DecodeSign1(byte[] cborPayload) { throw null; }
public static System.Security.Cryptography.Cose.CoseSign1Message DecodeSign1(ReadOnlySpan<byte> cborPayload) { throw null; }
}
public sealed partial class CoseSign1Message : System.Security.Cryptography.Cose.CoseMessage
{
internal CoseSign1Message() { }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public static byte[] Sign(ReadOnlySpan<byte> content, System.Security.Cryptography.AsymmetricAlgorithm key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.Cose.CoseHeaderMap? protectedHeaders = null, System.Security.Cryptography.Cose.CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public static byte[] Sign(byte[] content, System.Security.Cryptography.AsymmetricAlgorithm key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.Cose.CoseHeaderMap? protectedHeaders = null, System.Security.Cryptography.Cose.CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public static byte[] Sign(byte[] content, System.Security.Cryptography.ECDsa key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, bool isDetached = false) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public static byte[] Sign(byte[] content, System.Security.Cryptography.RSA key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, bool isDetached = false) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public static bool TrySign(ReadOnlySpan<byte> content, Span<byte> destination, System.Security.Cryptography.AsymmetricAlgorithm key!!, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, out int bytesWritten, System.Security.Cryptography.Cose.CoseHeaderMap? protectedHeaders = null, System.Security.Cryptography.Cose.CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public bool Verify(System.Security.Cryptography.ECDsa key) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public bool Verify(System.Security.Cryptography.ECDsa key, System.ReadOnlySpan<byte> content) { throw null; }
......
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum)</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>
<!-- Disabling baseline validation since this is a brand new package.
......@@ -13,8 +14,10 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="$(CommonPath)System\Memory\PointerMemoryManager.cs" Link="Common\System\Memory\PointerMemoryManager.cs" />
<Compile Include="System\Security\Cryptography\Cose\CoseHeaderLabel.cs" />
<Compile Include="System\Security\Cryptography\Cose\CoseHeaderMap.cs" />
<Compile Include="System\Security\Cryptography\Cose\CoseHelpers.cs" />
<Compile Include="System\Security\Cryptography\Cose\CoseMessage.cs" />
<Compile Include="System\Security\Cryptography\Cose\CoseSign1Message.cs" />
<Compile Include="System\Security\Cryptography\Cose\KnownCoseAlgorithms.cs" />
......@@ -31,10 +34,12 @@
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<Reference Include="System.Collections" />
<Reference Include="System.Memory" />
<Reference Include="System.Security.Cryptography.Primitives" />
<Reference Include="System.Security.Cryptography.Algorithms" />
<Reference Include="System.Runtime" />
<Reference Include="System.Runtime.Numerics" />
<Reference Include="System.Text.Encoding.Extensions" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
......
......@@ -24,11 +24,13 @@ namespace System.Security.Cryptography.Cose
internal int LabelAsInt32 { get; }
internal string? LabelAsString { get; }
internal int EncodedSize { get; }
public CoseHeaderLabel(int label)
{
this = default;
LabelAsInt32 = label;
EncodedSize = CoseHelpers.GetIntegerEncodedSize(label);
}
public CoseHeaderLabel(string label)
......@@ -40,6 +42,7 @@ public CoseHeaderLabel(string label)
this = default;
LabelAsString = label;
EncodedSize = CoseHelpers.GetTextStringEncodedSize(label);
}
public bool Equals(CoseHeaderLabel other)
......
......@@ -192,14 +192,15 @@ static void ValidateKnownHeaderValue(int label, CborReaderState? initialState, C
}
}
internal static byte[] Encode(CoseHeaderMap? map, bool mustReturnEmptyBstrIfEmpty = false, int? algHeaderValueToSlip = null)
internal static int Encode(CoseHeaderMap? map, Span<byte> destination, bool mustReturnEmptyBstrIfEmpty = false, int? algHeaderValueToSlip = null)
{
map ??= s_emptyMap;
bool shouldSlipAlgHeader = algHeaderValueToSlip.HasValue;
if (map._headerParameters.Count == 0 && mustReturnEmptyBstrIfEmpty && !shouldSlipAlgHeader)
{
return s_emptyBstrEncoded;
s_emptyBstrEncoded.CopyTo(destination);
return s_emptyBstrEncoded.Length;
}
int mapLength = map._headerParameters.Count;
......@@ -231,7 +232,33 @@ internal static byte[] Encode(CoseHeaderMap? map, bool mustReturnEmptyBstrIfEmpt
writer.WriteEncodedValue(encodedValue.Span);
}
writer.WriteEndMap();
return writer.Encode();
return writer.Encode(destination);
}
internal static int ComputeEncodedSize(CoseHeaderMap? map, int? algHeaderValueToSlip = null)
{
map ??= s_emptyMap;
// encoded map length => map length + (label + value)*
int encodedSize = 0;
int mapLength = map._headerParameters.Count;
if (algHeaderValueToSlip != null)
{
mapLength += 1;
encodedSize += CoseHeaderLabel.Algorithm.EncodedSize;
encodedSize += CoseHelpers.GetIntegerEncodedSize(algHeaderValueToSlip.Value);
}
encodedSize += CoseHelpers.GetIntegerEncodedSize(mapLength);
foreach ((CoseHeaderLabel label, ReadOnlyMemory<byte> encodedValue) in map)
{
encodedSize += label.EncodedSize + encodedValue.Length;
}
return encodedSize;
}
public Enumerator GetEnumerator() => new Enumerator(_headerParameters.GetEnumerator());
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
namespace System.Security.Cryptography.Cose
{
internal static class CoseHelpers
{
private static readonly UTF8Encoding s_utf8EncodingStrict = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
internal static int GetByteStringEncodedSize(int bstrLength)
{
return GetIntegerEncodedSize(bstrLength) + bstrLength;
}
internal static int GetTextStringEncodedSize(string value)
{
int strEncodedLength = s_utf8EncodingStrict.GetByteCount(value);
return GetIntegerEncodedSize(strEncodedLength) + strEncodedLength;
}
internal static int GetIntegerEncodedSize(long value)
{
if (value < 0)
{
ulong unsignedRepresentation = (value == long.MinValue) ? (ulong)long.MaxValue : (ulong)(-value) - 1;
return GetIntegerEncodedSize(unsignedRepresentation);
}
else
{
return GetIntegerEncodedSize((ulong)value);
}
}
internal static int GetIntegerEncodedSize(ulong value)
{
if (value < 24)
{
return 1;
}
else if (value <= byte.MaxValue)
{
return 1 + sizeof(byte);
}
else if (value <= ushort.MaxValue)
{
return 1 + sizeof(ushort);
}
else if (value <= uint.MaxValue)
{
return 1 + sizeof(uint);
}
else
{
return 1 + sizeof(ulong);
}
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.Diagnostics;
using System.Formats.Cbor;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace System.Security.Cryptography.Cose
{
public abstract class CoseMessage
{
internal const string PreviewFeatureMessage = "COSE is in preview.";
private const byte EmptyStringByte = 0xa0;
internal const int SizeOfArrayOfFour = 1;
// COSE tags https://datatracker.ietf.org/doc/html/rfc8152#page-8 Table 1.
internal const CborTag Sign1Tag = (CborTag)18;
......@@ -46,19 +50,58 @@ internal CoseMessage(CoseHeaderMap protectedHeader, CoseHeaderMap unprotectedHea
}
}
public static CoseSign1Message DecodeSign1(byte[] cborPayload)
public static CoseSign1Message DecodeSign1(byte[] cborPayload!!)
=> DecodeCoseSign1Core(new CborReader(cborPayload));
public static CoseSign1Message DecodeSign1(ReadOnlySpan<byte> cborPayload)
{
unsafe
{
fixed (byte* ptr = &MemoryMarshal.GetReference(cborPayload))
{
using (MemoryManager<byte> manager = new PointerMemoryManager<byte>(ptr, cborPayload.Length))
{
return DecodeCoseSign1Core(new CborReader(manager.Memory));
}
}
}
}
private static CoseSign1Message DecodeCoseSign1Core(CborReader reader)
{
try
{
var reader = new CborReader(cborPayload);
CborTag? tag = DecodeTag(reader);
if (tag != null && tag != Sign1Tag)
{
throw new CryptographicException(SR.Format(SR.DecodeSign1IncorrectTag, tag));
}
CoseSign1Message message = DecodeCoseSign1Core(reader);
return reader.BytesRemaining == 0 ? message : throw new CryptographicException(SR.Format(SR.DecodeSign1ErrorWhileDecoding, SR.DecodeSign1MesageContainedTrailingData));
int? arrayLength = reader.ReadStartArray();
if (arrayLength != 4)
{
throw new CryptographicException(SR.Format(SR.DecodeSign1ErrorWhileDecoding, SR.DecodeSign1ArrayLengthMustBeFour));
}
var protectedHeader = new CoseHeaderMap();
DecodeProtectedBucket(reader, protectedHeader, out byte[] protectedHeaderAsBstr);
protectedHeader.IsReadOnly = true;
var unprotectedHeader = new CoseHeaderMap();
DecodeUnprotectedBucket(reader, unprotectedHeader);
ThrowIfDuplicateLabels(protectedHeader, unprotectedHeader);
byte[]? payload = DecodePayload(reader);
byte[] signature = DecodeSignature(reader);
reader.ReadEndArray();
if (reader.BytesRemaining != 0)
{
throw new CryptographicException(SR.Format(SR.DecodeSign1ErrorWhileDecoding, SR.DecodeSign1MesageContainedTrailingData));
}
return new CoseSign1Message(protectedHeader, unprotectedHeader, payload, signature, protectedHeaderAsBstr);
}
catch (Exception ex) when (ex is CborContentException or InvalidOperationException)
{
......@@ -75,30 +118,6 @@ public static CoseSign1Message DecodeSign1(byte[] cborPayload)
};
}
private static CoseSign1Message DecodeCoseSign1Core(CborReader reader)
{
int? arrayLength = reader.ReadStartArray();
if (arrayLength != 4)
{
throw new CryptographicException(SR.Format(SR.DecodeSign1ErrorWhileDecoding, SR.DecodeSign1ArrayLengthMustBeFour));
}
var protectedHeader = new CoseHeaderMap();
DecodeProtectedBucket(reader, protectedHeader, out byte[] protectedHeaderAsBstr);
protectedHeader.IsReadOnly = true;
var unprotectedHeader = new CoseHeaderMap();
DecodeUnprotectedBucket(reader, unprotectedHeader);
ThrowIfDuplicateLabels(protectedHeader, unprotectedHeader);
byte[]? payload = DecodePayload(reader);
byte[] signature = DecodeSignature(reader);
reader.ReadEndArray();
return new CoseSign1Message(protectedHeader, unprotectedHeader, payload, signature, protectedHeaderAsBstr);
}
private static void DecodeProtectedBucket(CborReader reader, CoseHeaderMap headerParameters, out byte[] protectedHeaderAsBstr)
{
protectedHeaderAsBstr = reader.ReadByteString();
......@@ -162,7 +181,7 @@ private static byte[] DecodeSignature(CborReader reader)
return reader.ReadByteString();
}
internal static byte[] CreateToBeSigned(string context, ReadOnlySpan<byte> encodedProtectedHeader, ReadOnlySpan<byte> content)
internal static int CreateToBeSigned(string context, ReadOnlySpan<byte> encodedProtectedHeader, ReadOnlySpan<byte> content, Span<byte> destination)
{
var writer = new CborWriter();
writer.WriteStartArray(4);
......@@ -171,9 +190,19 @@ internal static byte[] CreateToBeSigned(string context, ReadOnlySpan<byte> encod
writer.WriteByteString(Span<byte>.Empty); // external_aad
writer.WriteByteString(content); //payload or content
writer.WriteEndArray();
return writer.Encode();
int bytesWritten = writer.Encode(destination);
Debug.Assert(bytesWritten == writer.BytesWritten && bytesWritten == ComputeToBeSignedEncodedSize(context, encodedProtectedHeader, content));
return bytesWritten;
}
internal static int ComputeToBeSignedEncodedSize(string context, ReadOnlySpan<byte> encodedProtectedHeader, ReadOnlySpan<byte> content)
=> SizeOfArrayOfFour +
CoseHelpers.GetTextStringEncodedSize(context) +
CoseHelpers.GetByteStringEncodedSize(encodedProtectedHeader.Length) +
CoseHelpers.GetByteStringEncodedSize(Span<byte>.Empty.Length) +
CoseHelpers.GetByteStringEncodedSize(content.Length);
// Validate duplicate labels https://datatracker.ietf.org/doc/html/rfc8152#section-3.
internal static void ThrowIfDuplicateLabels(CoseHeaderMap? protectedHeaders, CoseHeaderMap? unprotectedHeaders)
{
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.Diagnostics;
using System.Formats.Cbor;
using System.Runtime.Versioning;
......@@ -11,70 +12,100 @@ public sealed class CoseSign1Message : CoseMessage
{
private const string SigStructureCoxtextSign1 = "Signature1";
private const int Sign1ArrayLegth = 4;
private byte[]? _toBeSigned;
internal CoseSign1Message(CoseHeaderMap protectedHeader, CoseHeaderMap unprotectedHeader, byte[]? content, byte[] signature, byte[] protectedHeaderAsBstr)
: base(protectedHeader, unprotectedHeader, content, signature, protectedHeaderAsBstr) { }
[UnsupportedOSPlatform("browser")]
public static byte[] Sign(byte[] content!!, ECDsa key!!, HashAlgorithmName hashAlgorithm, bool isDetached = false)
{
byte[] encodedProtectedHeader = CreateEncodedProtectedHeader(KeyType.ECDsa, hashAlgorithm);
byte[] toBeSigned = CreateToBeSigned(SigStructureCoxtextSign1, encodedProtectedHeader, content);
byte[] signature = SignWithECDsa(key, toBeSigned, hashAlgorithm);
return SignCore(encodedProtectedHeader, GetEmptyCborMap(), signature, content, isDetached);
}
=> SignCore(content.AsSpan(), key, hashAlgorithm, KeyType.ECDsa, null, null, isDetached);
[UnsupportedOSPlatform("browser")]
public static byte[] Sign(byte[] content!!, RSA key!!, HashAlgorithmName hashAlgorithm, bool isDetached = false)
{
byte[] encodedProtectedHeader = CreateEncodedProtectedHeader(KeyType.RSA, hashAlgorithm);
byte[] toBeSigned = CreateToBeSigned(SigStructureCoxtextSign1, encodedProtectedHeader, content);
byte[] signature = SignWithRSA(key, toBeSigned, hashAlgorithm);
return SignCore(encodedProtectedHeader, GetEmptyCborMap(), signature, content, isDetached);
}
=> SignCore(content.AsSpan(), key, hashAlgorithm, KeyType.RSA, null, null, isDetached);
[UnsupportedOSPlatform("browser")]
public static byte[] Sign(byte[] content!!, AsymmetricAlgorithm key!!, HashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false)
=> SignCore(content.AsSpan(), key, hashAlgorithm, GetKeyType(key), protectedHeaders, unprotectedHeaders, isDetached);
[UnsupportedOSPlatform("browser")]
public static byte[] Sign(ReadOnlySpan<byte> content, AsymmetricAlgorithm key!!, HashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false)
=> SignCore(content, key, hashAlgorithm, GetKeyType(key), protectedHeaders, unprotectedHeaders, isDetached);
[UnsupportedOSPlatform("browser")]
internal static byte[] SignCore(ReadOnlySpan<byte> content, AsymmetricAlgorithm key, HashAlgorithmName hashAlgorithm, KeyType keyType, CoseHeaderMap? protectedHeaders, CoseHeaderMap? unprotectedHeaders, bool isDetached)
{
KeyType keyType = key switch
{
ECDsa => KeyType.ECDsa,
RSA => KeyType.RSA,
_ => throw new CryptographicException(SR.Format(SR.Sign1SignUnsupportedKey, key.GetType()))
};
ValidateBeforeSign(protectedHeaders, unprotectedHeaders, keyType, hashAlgorithm, out int? algHeaderValueToSlip);
ThrowIfDuplicateLabels(protectedHeaders, unprotectedHeaders);
int expectedSize = ComputeEncodedSize(protectedHeaders, unprotectedHeaders, algHeaderValueToSlip, content.Length, isDetached, key.KeySize, keyType);
byte[] buffer = new byte[expectedSize];
int? algHeaderValueToSlip = ValidateOrSlipAlgorithmHeader(protectedHeaders, unprotectedHeaders, keyType, hashAlgorithm);
int bytesWritten = CreateCoseSign1Message(content, buffer, key, hashAlgorithm, protectedHeaders, unprotectedHeaders, isDetached, algHeaderValueToSlip, keyType);
Debug.Assert(expectedSize == bytesWritten);
byte[] encodedProtetedHeaders = CoseHeaderMap.Encode(protectedHeaders, mustReturnEmptyBstrIfEmpty: true, algHeaderValueToSlip);
byte[] toBeSigned = CreateToBeSigned(SigStructureCoxtextSign1, encodedProtetedHeaders, content);
return buffer;
}
byte[] signature;
if (keyType == KeyType.ECDsa)
[UnsupportedOSPlatform("browser")]
public static bool TrySign(
ReadOnlySpan<byte> content,
Span<byte> destination,
AsymmetricAlgorithm key!!,
HashAlgorithmName hashAlgorithm,
out int bytesWritten,
CoseHeaderMap? protectedHeaders = null,
CoseHeaderMap? unprotectedHeaders = null,
bool isDetached = false)
{
KeyType keyType = GetKeyType(key);
ValidateBeforeSign(protectedHeaders, unprotectedHeaders, keyType, hashAlgorithm, out int? algHeaderValueToSlip);
int expectedSize = ComputeEncodedSize(protectedHeaders, unprotectedHeaders, algHeaderValueToSlip, content.Length, isDetached, key.KeySize, keyType);
if (expectedSize > destination.Length)
{
signature = SignWithECDsa((ECDsa)key, toBeSigned, hashAlgorithm);
bytesWritten = 0;
return false;
}
else
bytesWritten = CreateCoseSign1Message(content, destination, key, hashAlgorithm, protectedHeaders, unprotectedHeaders, isDetached, algHeaderValueToSlip, keyType);
Debug.Assert(expectedSize == bytesWritten);
return true;
}
internal static KeyType GetKeyType(AsymmetricAlgorithm key)
{
return key switch
{
signature = SignWithRSA((RSA)key, toBeSigned, hashAlgorithm);
}
ECDsa => KeyType.ECDsa,
RSA => KeyType.RSA,
_ => throw new CryptographicException(SR.Format(SR.Sign1SignUnsupportedKey, key.GetType()))
};
}
return SignCore(encodedProtetedHeaders, CoseHeaderMap.Encode(unprotectedHeaders), signature, content, isDetached);
internal static void ValidateBeforeSign(CoseHeaderMap? protectedHeaders, CoseHeaderMap? unprotectedHeaders, KeyType keyType, HashAlgorithmName hashAlgorithm, out int? algHeaderValueToSlip)
{
ThrowIfDuplicateLabels(protectedHeaders, unprotectedHeaders);
algHeaderValueToSlip = ValidateOrSlipAlgorithmHeader(protectedHeaders, unprotectedHeaders, keyType, hashAlgorithm);
}
private static byte[] SignCore(
ReadOnlySpan<byte> encodedProtectedHeader,
ReadOnlySpan<byte> encodedUnprotectedHeader,
ReadOnlySpan<byte> signature,
ReadOnlySpan<byte> content,
bool isDetached)
[UnsupportedOSPlatform("browser")]
internal static int CreateCoseSign1Message(ReadOnlySpan<byte> content, Span<byte> buffer, AsymmetricAlgorithm key, HashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders, CoseHeaderMap? unprotectedHeaders, bool isDetached, int? algHeaderValueToSlip, KeyType keyType)
{
var writer = new CborWriter();
writer.WriteTag(Sign1Tag);
writer.WriteStartArray(Sign1ArrayLegth);
writer.WriteByteString(encodedProtectedHeader);
writer.WriteEncodedValue(encodedUnprotectedHeader);
int protectedMapBytesWritten = CoseHeaderMap.Encode(protectedHeaders, buffer, true, algHeaderValueToSlip);
ReadOnlySpan<byte> encodedProtectedHeaders = buffer.Slice(0, protectedMapBytesWritten);
// We're going to use the encoded protected headers again after this step (for the toBeSigned construction),
// so don't overwrite them yet.
writer.WriteByteString(encodedProtectedHeaders);
int unprotectedMapBytesWritten = CoseHeaderMap.Encode(unprotectedHeaders, buffer.Slice(protectedMapBytesWritten));
ReadOnlySpan<byte> encodedUnprotectedHeaders = buffer.Slice(protectedMapBytesWritten, unprotectedMapBytesWritten);
writer.WriteEncodedValue(encodedUnprotectedHeaders);
if (isDetached)
{
......@@ -85,36 +116,84 @@ public static byte[] Sign(byte[] content!!, AsymmetricAlgorithm key!!, HashAlgor
writer.WriteByteString(content);
}
writer.WriteByteString(signature);
int expectedToBeSignedSize = ComputeToBeSignedEncodedSize(SigStructureCoxtextSign1, encodedProtectedHeaders, content);
Span<byte> toBeSignedBuffer = buffer;
byte[]? rentedToBeSignedBuffer = null;
int signatureBytesWritten;
// It is possible for toBeSigned to be bigger than the COSE message length that we used to determine the size of our buffer.
// we rent a bigger buffer if that's the case.
if (buffer.Length < expectedToBeSignedSize)
{
rentedToBeSignedBuffer = ArrayPool<byte>.Shared.Rent(expectedToBeSignedSize);
toBeSignedBuffer = rentedToBeSignedBuffer;
}
try
{
int toBeSignedBytesWritten = CreateToBeSigned(SigStructureCoxtextSign1, encodedProtectedHeaders, content, toBeSignedBuffer);
ReadOnlySpan<byte> encodedToBeSigned = buffer.Slice(0, toBeSignedBytesWritten);
if (keyType == KeyType.ECDsa)
{
signatureBytesWritten = SignWithECDsa((ECDsa)key, encodedToBeSigned, hashAlgorithm, buffer);
}
else
{
Debug.Assert(keyType == KeyType.RSA);
signatureBytesWritten = SignWithRSA((RSA)key, encodedToBeSigned, hashAlgorithm, buffer);
}
}
finally
{
if (rentedToBeSignedBuffer != null)
{
ArrayPool<byte>.Shared.Return(rentedToBeSignedBuffer, clearArray: true);
}
}
writer.WriteByteString(buffer.Slice(0, signatureBytesWritten));
writer.WriteEndArray();
return writer.Encode();
return writer.Encode(buffer);
}
[UnsupportedOSPlatform("browser")]
private static byte[] SignWithECDsa(ECDsa key, byte[] data, HashAlgorithmName hashAlgorithm)
=> key.SignData(data, hashAlgorithm);
[UnsupportedOSPlatform("browser")]
private static byte[] SignWithRSA(RSA key, byte[] data, HashAlgorithmName hashAlgorithm)
=> key.SignData(data, hashAlgorithm, RSASignaturePadding.Pss);
internal static byte[] CreateEncodedProtectedHeader(KeyType algType, HashAlgorithmName hashAlgorithm)
private static int SignWithECDsa(ECDsa key, ReadOnlySpan<byte> data, HashAlgorithmName hashAlgorithm, Span<byte> destination)
{
var writer = new CborWriter();
writer.WriteStartMap(1);
writer.WriteInt32(KnownHeaders.Alg);
writer.WriteInt32(GetCoseAlgorithmHeaderFromKeyTypeAndHashAlgorithm(algType, hashAlgorithm));
writer.WriteEndMap();
return writer.Encode();
#if NETSTANDARD2_0 || NETFRAMEWORK
byte[] signature = key.SignData(data.ToArray(), hashAlgorithm);
signature.CopyTo(destination);
return signature.Length;
#else
if (!key.TrySignData(data, destination, hashAlgorithm, out int bytesWritten))
{
Debug.Fail("TrySignData failed with a pre-calculated destination");
throw new CryptographicException();
}
return bytesWritten;
#endif
}
private static byte[] GetEmptyCborMap()
[UnsupportedOSPlatform("browser")]
private static int SignWithRSA(RSA key, ReadOnlySpan<byte> data, HashAlgorithmName hashAlgorithm, Span<byte> destination)
{
var writer = new CborWriter();
writer.WriteStartMap(0);
writer.WriteEndMap();
return writer.Encode();
#if NETSTANDARD2_0 || NETFRAMEWORK
byte[] signature = key.SignData(data.ToArray(), hashAlgorithm, RSASignaturePadding.Pss);
signature.CopyTo(destination);
return signature.Length;
#else
if (!key.TrySignData(data, destination, hashAlgorithm, RSASignaturePadding.Pss, out int bytesWritten))
{
Debug.Fail("TrySignData failed with a pre-calculated destination");
throw new CryptographicException();
}
return bytesWritten;
#endif
}
[UnsupportedOSPlatform("browser")]
......@@ -158,7 +237,35 @@ private void PrepareForVerify(ReadOnlySpan<byte> content, out int alg, out byte[
}
alg = nullableAlg.Value;
toBeSigned = CreateToBeSigned(SigStructureCoxtextSign1, _protectedHeaderAsBstr, content);
if (_content == null)
{
// Never cache toBeSigned if the message has detached content since the passed-in content can be different in each call.
toBeSigned = CreateToBeSignedForVerify(content);
}
else if (_toBeSigned == null)
{
toBeSigned = _toBeSigned = CreateToBeSignedForVerify(content);
}
else
{
toBeSigned = _toBeSigned;
}
byte[] CreateToBeSignedForVerify(ReadOnlySpan<byte> content)
{
byte[] rentedbuffer = ArrayPool<byte>.Shared.Rent(ComputeToBeSignedEncodedSize(SigStructureCoxtextSign1, _protectedHeaderAsBstr, content));
try
{
Span<byte> buffer = rentedbuffer;
int bytesWritten = CreateToBeSigned(SigStructureCoxtextSign1, _protectedHeaderAsBstr, content, buffer);
return buffer.Slice(0, bytesWritten).ToArray();
}
finally
{
ArrayPool<byte>.Shared.Return(rentedbuffer, clearArray: true);
}
}
}
private static ReadOnlyMemory<byte> GetCoseAlgorithmFromProtectedHeaders(CoseHeaderMap protectedHeaders)
......@@ -311,5 +418,40 @@ private void ThrowIfUnsupportedHeaders()
throw new NotSupportedException(SR.Sign1VerifyCriticalAndCounterSignNotSupported);
}
}
private static int ComputeEncodedSize(CoseHeaderMap? protectedHeaders, CoseHeaderMap? unprotectedHeaders, int? algHeaderValueToSlip, int contentLength, bool isDetached, int keySize, KeyType keyType)
{
// tag + array(4) + encoded protected header map + unprotected header map + content + signature.
const int SizeOfTag = 1;
const int SizeOfNull = 1;
int encodedSize = SizeOfTag + SizeOfArrayOfFour +
CoseHelpers.GetByteStringEncodedSize(CoseHeaderMap.ComputeEncodedSize(protectedHeaders, algHeaderValueToSlip)) +
CoseHeaderMap.ComputeEncodedSize(unprotectedHeaders);
if (isDetached)
{
encodedSize += SizeOfNull;
}
else
{
encodedSize += CoseHelpers.GetByteStringEncodedSize(contentLength);
}
int signatureSize;
if (keyType == KeyType.ECDsa)
{
signatureSize = 2 * ((keySize + 7) / 8);
}
else // RSA
{
Debug.Assert(keyType == KeyType.RSA);
signatureSize = (keySize + 7) / 8;
}
encodedSize += CoseHelpers.GetByteStringEncodedSize(signatureSize);
return encodedSize;
}
}
}
......@@ -5,7 +5,7 @@
namespace System.Security.Cryptography.Cose.Tests
{
public partial class CoseSign1MessageTests
public class CoseSign1MessageTests_Verify
{
[Theory]
// https://github.com/cose-wg/Examples/blob/master/RFC8152/Appendix_C_2_1.json
......@@ -27,20 +27,19 @@ public partial class CoseSign1MessageTests
[InlineData((int)ECDsaAlgorithm.ES256, "8443A10126A10442313154546869732069732074686520636F6E74656E742E58408EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36")]
public void Verify(int algorithm, string hexCborMessage)
{
foreach (bool usePublicOnlyKey in new[] { false, true })
foreach (bool useNonPrivateKey in new[] { false, true })
{
CoseSign1Message msg = CoseMessage.DecodeSign1(ByteUtils.HexToByteArray(hexCborMessage));
bool verified;
if (Enum.IsDefined(typeof(ECDsaAlgorithm), algorithm))
{
var ecdsaAlgorithm = (ECDsaAlgorithm)algorithm;
ECDsa key = usePublicOnlyKey ? ECDsaKeysWithoutPrivateKey[ecdsaAlgorithm] : ECDsaKeys[ecdsaAlgorithm];
ECDsa key = GetKeyHashPair<ECDsa>((CoseAlgorithm)algorithm, useNonPrivateKey).Key;
verified = msg.Verify(key);
}
else
{
RSA key = usePublicOnlyKey ? RSAKeyWithoutPrivateKey : RSAKey;
RSA key = GetKeyHashPair<RSA>((CoseAlgorithm)algorithm, useNonPrivateKey).Key;
verified = msg.Verify(key);
}
......@@ -89,6 +88,34 @@ static string ReplaceFirst(string text, string search, string replace)
}
}
[Fact]
public void VerifyReturnsTrueAfterAttemptWithWrongContent()
{
ReadOnlySpan<byte> correctContent = s_sampleContent;
Span<byte> wrongContent = new byte[s_sampleContent.Length];
wrongContent.Fill(42);
ReadOnlySpan<byte> encodedMsg = CoseSign1Message.Sign(correctContent, DefaultKey, DefaultHash, isDetached: true);
CoseSign1Message msg = CoseMessage.DecodeSign1(encodedMsg);
Assert.False(msg.Verify(DefaultKey, wrongContent), "Calling Verify with the wrong content");
Assert.True(msg.Verify(DefaultKey, s_sampleContent), "Calling Verify with the correct content");
}
[Fact]
public void VerifyReturnsFalseAfterAttemptWithCorrectContent()
{
ReadOnlySpan<byte> correctContent = s_sampleContent;
Span<byte> wrongContent = new byte[s_sampleContent.Length];
wrongContent.Fill(42);
ReadOnlySpan<byte> encodedMsg = CoseSign1Message.Sign(correctContent, DefaultKey, DefaultHash, isDetached: true);
CoseSign1Message msg = CoseMessage.DecodeSign1(encodedMsg);
Assert.True(msg.Verify(DefaultKey, s_sampleContent), "Calling Verify with the correct content");
Assert.False(msg.Verify(DefaultKey, wrongContent), "Calling Verify with the wrong content");
}
[Theory]
// https://github.com/cose-wg/Examples/blob/master/sign1-tests/sign-fail-03.json
[InlineData("D28445A1013903E6A10442313154546869732069732074686520636F6E74656E742E58408EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36")]
......
......@@ -37,6 +37,16 @@ public enum RSAAlgorithm
PS512 = -39
}
public enum CoseAlgorithm
{
ES256 = -7,
ES384 = -35,
ES512 = -36,
PS256 = -37,
PS384 = -38,
PS512 = -39
}
public enum ContentTestCase
{
Empty,
......@@ -53,19 +63,19 @@ internal static HashAlgorithmName GetHashAlgorithmNameFromCoseAlgorithm(int algo
_ => throw new InvalidOperationException()
};
internal static CoseHeaderMap GetHeaderMapWithAlgorithm(int algorithm = (int)ECDsaAlgorithm.ES256)
internal static CoseHeaderMap GetHeaderMapWithAlgorithm(CoseAlgorithm algorithm = CoseAlgorithm.ES256)
{
var protectedHeaders = new CoseHeaderMap();
protectedHeaders.SetValue(CoseHeaderLabel.Algorithm, algorithm);
protectedHeaders.SetValue(CoseHeaderLabel.Algorithm, (int)algorithm);
return protectedHeaders;
}
internal static CoseHeaderMap GetEmptyHeaderMap() => new CoseHeaderMap();
internal static List<(CoseHeaderLabel, ReadOnlyMemory<byte>)> GetExpectedProtectedHeaders(int algorithm = (int)ECDsaAlgorithm.ES256)
internal static List<(CoseHeaderLabel, ReadOnlyMemory<byte>)> GetExpectedProtectedHeaders(CoseAlgorithm algorithm)
{
var l = new List<(CoseHeaderLabel, ReadOnlyMemory<byte>)>();
AddEncoded(l, CoseHeaderLabel.Algorithm, algorithm);
AddEncoded(l, CoseHeaderLabel.Algorithm, (int)algorithm);
return l;
}
......@@ -79,6 +89,13 @@ internal static void AddEncoded(List<(CoseHeaderLabel, ReadOnlyMemory<byte>)> li
list.Add((label, writer.Encode()));
}
internal static void AddEncoded(List<(CoseHeaderLabel, ReadOnlyMemory<byte>)> list, CoseHeaderLabel label, string value)
{
var writer = new CborWriter();
writer.WriteTextString(value);
list.Add((label, writer.Encode()));
}
internal static byte[] GetDummyContent(ContentTestCase @case)
{
return @case switch
......@@ -91,33 +108,34 @@ internal static byte[] GetDummyContent(ContentTestCase @case)
}
internal static void AssertSign1Message(
byte[] encodedMsg,
byte[]? expectedContent,
ReadOnlySpan<byte> encodedMsg,
ReadOnlySpan<byte> expectedContent,
AsymmetricAlgorithm signingKey,
CoseAlgorithm algorithm,
List<(CoseHeaderLabel, ReadOnlyMemory<byte>)>? expectedProtectedHeaders = null,
List<(CoseHeaderLabel, ReadOnlyMemory<byte>)>? expectedUnprotectedHeaders = null)
List<(CoseHeaderLabel, ReadOnlyMemory<byte>)>? expectedUnprotectedHeaders = null,
bool expectedDetachedContent = false)
{
Assert.NotNull(encodedMsg);
var reader = new CborReader(encodedMsg);
var reader = new CborReader(encodedMsg.ToArray());
// Start
Assert.Equal((CborTag)18, reader.ReadTag());
Assert.Equal(4, reader.ReadStartArray());
// Protected headers
AssertSign1ProtectedHeaders(reader.ReadByteString(), expectedProtectedHeaders ?? GetExpectedProtectedHeaders());
AssertSign1ProtectedHeaders(reader.ReadByteString(), expectedProtectedHeaders ?? GetExpectedProtectedHeaders(algorithm));
// Unprotected headers
AssertSign1Headers(reader, expectedUnprotectedHeaders ?? GetEmptyExpectedHeaders());
// Content
if (expectedContent != null)
if (expectedDetachedContent)
{
AssertExtensions.SequenceEqual(expectedContent, reader.ReadByteString());
reader.ReadNull();
}
else
{
reader.ReadNull();
AssertExtensions.SequenceEqual(expectedContent, reader.ReadByteString());
}
// Signature
......@@ -132,11 +150,25 @@ internal static byte[] GetDummyContent(ContentTestCase @case)
CoseSign1Message msg = CoseMessage.DecodeSign1(encodedMsg);
if (signingKey is ECDsa ecdsa)
{
Assert.True(msg.Verify(ecdsa), "msg.Verify(ecdsa)");
if (expectedDetachedContent)
{
Assert.True(msg.Verify(ecdsa, expectedContent), "msg.Verify(ecdsa, content)");
}
else
{
Assert.True(msg.Verify(ecdsa), "msg.Verify(ecdsa)");
}
}
else if (signingKey is RSA rsa)
{
Assert.True(msg.Verify(rsa), "msg.Verify(rsa)");
if (expectedDetachedContent)
{
Assert.True(msg.Verify(rsa, expectedContent), "msg.Verify(rsa, content)");
}
else
{
Assert.True(msg.Verify(rsa), "msg.Verify(rsa)");
}
}
else
{
......@@ -222,9 +254,6 @@ private static void AssertSign1Headers(CborReader reader, List<(CoseHeaderLabel,
internal static ECDsa DefaultKey => ES256;
internal static HashAlgorithmName DefaultHash { get; } = GetHashAlgorithmNameFromCoseAlgorithm((int)ECDsaAlgorithm.ES256);
internal static readonly ThreadStaticECDsaDictionary ECDsaKeys = new(true);
internal static readonly ThreadStaticECDsaDictionary ECDsaKeysWithoutPrivateKey = new(false);
[ThreadStatic]
internal static RSA? t_rsaKey;
[ThreadStatic]
......@@ -280,22 +309,28 @@ private static RSA CreateRSA(bool includePrivateKey)
return RSA.Create(rsaParameters);
}
internal class ThreadStaticECDsaDictionary: Dictionary<ECDsaAlgorithm, ECDsa>
internal static (T Key, HashAlgorithmName Hash) GetKeyHashPair<T>(CoseAlgorithm algorithm, bool useNonPrivateKey = false)
{
private bool _includePrivateKeys;
public ThreadStaticECDsaDictionary(bool includePrivateKeys)
return algorithm switch
{
_includePrivateKeys = includePrivateKeys;
}
public new ECDsa this[ECDsaAlgorithm key]
CoseAlgorithm.ES256 => (GetKey(ES256, ES256WithoutPrivateKey, useNonPrivateKey), HashAlgorithmName.SHA256),
CoseAlgorithm.ES384 => (GetKey(ES384, ES384WithoutPrivateKey, useNonPrivateKey), HashAlgorithmName.SHA384),
CoseAlgorithm.ES512 => (GetKey(ES512, ES512WithoutPrivateKey, useNonPrivateKey), HashAlgorithmName.SHA512),
CoseAlgorithm.PS256 => (GetKey(RSAKey, RSAKeyWithoutPrivateKey, useNonPrivateKey), HashAlgorithmName.SHA256),
CoseAlgorithm.PS384 => (GetKey(RSAKey, RSAKeyWithoutPrivateKey, useNonPrivateKey), HashAlgorithmName.SHA384),
CoseAlgorithm.PS512 => (GetKey(RSAKey, RSAKeyWithoutPrivateKey, useNonPrivateKey), HashAlgorithmName.SHA512),
_ => throw new InvalidOperationException()
};
T GetKey(AsymmetricAlgorithm privateKey, AsymmetricAlgorithm nonPrivateKey, bool useNonPrivateKey)
{
get => key switch
if (privateKey is T privateKeyAsT && nonPrivateKey is T nonPrivateKeyAsT)
{
ECDsaAlgorithm.ES256 => _includePrivateKeys ? ES256 : ES256WithoutPrivateKey,
ECDsaAlgorithm.ES384 => _includePrivateKeys ? ES384 : ES384WithoutPrivateKey,
ECDsaAlgorithm.ES512 => _includePrivateKeys ? ES512 : ES512WithoutPrivateKey,
_ => throw new InvalidOperationException()
};
return useNonPrivateKey ? nonPrivateKeyAsT : privateKeyAsT;
}
throw new InvalidOperationException($"Specified algorithm {algorithm} doesn't match the type {typeof(T)}");
}
}
}
......
......@@ -14,6 +14,7 @@
<Compile Include="CoseHeaderLabelTests.cs" />
<Compile Include="CoseHeaderMapTests.cs" />
<Compile Include="CoseMessageTests.DecoseSign1.cs" />
<Compile Include="CoseSign1MessageTests.Sign.CustomHeaderMaps.cs" />
<Compile Include="CoseSign1MessageTests.Verify.cs" />
<Compile Include="CoseSign1MessageTests.Sign.cs" />
<Compile Include="CoseTestHelpers.cs" />
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册