Skip to content

Commit a29a334

Browse files
authored
S.S.C.Cose: Add Async Stream-based signatures (#67786)
* Add SignAsync(stream) and VerifyAsync(Stream) * Address src feedback * * Add sync Stream APIs and tests * Address feedback * * Add newlines to long signatures * Remove try-finally form ArrayPool rents on async context
1 parent bd29496 commit a29a334

13 files changed

+857
-226
lines changed

src/libraries/System.Security.Cryptography.Cose/ref/System.Security.Cryptography.Cose.cs

+8
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,22 @@ internal CoseSign1Message() { }
6868
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
6969
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; }
7070
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
71+
public static byte[] Sign(System.IO.Stream detachedContent, System.Security.Cryptography.AsymmetricAlgorithm key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.Cose.CoseHeaderMap? protectedHeaders = null, System.Security.Cryptography.Cose.CoseHeaderMap? unprotectedHeaders = null) { throw null; }
72+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
7173
public static byte[] Sign(System.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; }
7274
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
75+
public static System.Threading.Tasks.Task<byte[]> SignAsync(System.IO.Stream detachedContent, System.Security.Cryptography.AsymmetricAlgorithm key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.Cose.CoseHeaderMap? protectedHeaders = null, System.Security.Cryptography.Cose.CoseHeaderMap? unprotectedHeaders = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
76+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
7377
public static bool TrySign(System.ReadOnlySpan<byte> content, System.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; }
7478
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
7579
public bool Verify(System.Security.Cryptography.AsymmetricAlgorithm key) { throw null; }
7680
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
7781
public bool Verify(System.Security.Cryptography.AsymmetricAlgorithm key, byte[] content) { throw null; }
7882
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
83+
public bool Verify(System.Security.Cryptography.AsymmetricAlgorithm key, System.IO.Stream detachedContent) { throw null; }
84+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
7985
public bool Verify(System.Security.Cryptography.AsymmetricAlgorithm key, System.ReadOnlySpan<byte> content) { throw null; }
86+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
87+
public System.Threading.Tasks.Task<bool> VerifyAsync(System.Security.Cryptography.AsymmetricAlgorithm key, System.IO.Stream detachedContent, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
8088
}
8189
}

src/libraries/System.Security.Cryptography.Cose/src/Resources/Strings.resx

+6
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@
156156
<data name="Sign1AlgDoesNotMatchWithTheOnesSupportedByTypeOfKey" xml:space="preserve">
157157
<value>COSE algorithm '{0}' doesn't match with the supported algorithms of '{1}'.</value>
158158
</data>
159+
<data name="Sign1ArgumentStreamNotReadable" xml:space="preserve">
160+
<value>Stream was not readable.</value>
161+
</data>
162+
<data name="Sign1ArgumentStreamNotSeekable" xml:space="preserve">
163+
<value>Stream does not support seeking.</value>
164+
</data>
159165
<data name="Sign1SignAlgMustBeProtected" xml:space="preserve">
160166
<value>If specified, Algorithm (alg) must be a protected header.</value>
161167
</data>

src/libraries/System.Security.Cryptography.Cose/src/System.Security.Cryptography.Cose.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
<ItemGroup>
1717
<Compile Include="$(CommonPath)System\Memory\PointerMemoryManager.cs" Link="Common\System\Memory\PointerMemoryManager.cs" />
18+
<Compile Include="$(LibrariesProjectRoot)System.Formats.Cbor\src\System\Formats\Cbor\CborInitialByte.cs" Link="System\Formats\Cbor\CborInitialByte.cs" />
1819
<Compile Include="System\Security\Cryptography\Cose\CoseHeaderLabel.cs" />
1920
<Compile Include="System\Security\Cryptography\Cose\CoseHeaderMap.cs" />
2021
<Compile Include="System\Security\Cryptography\Cose\CoseHelpers.cs" />
@@ -45,4 +46,9 @@
4546
<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
4647
<Reference Include="System.Security.Cryptography" />
4748
</ItemGroup>
49+
50+
<!-- For IncrementalHash -->
51+
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
52+
<PackageReference Include="System.Security.Cryptography.Algorithms" Version="$(SystemSecurityCryptographyAlgorithmsVersion)" />
53+
</ItemGroup>
4854
</Project>

src/libraries/System.Security.Cryptography.Cose/src/System/Security/Cryptography/Cose/CoseHelpers.cs

+96
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers.Binary;
5+
using System.Diagnostics;
6+
using System.Formats.Cbor;
7+
using System.Runtime.Versioning;
48
using System.Text;
59

610
namespace System.Security.Cryptography.Cose
@@ -33,6 +37,47 @@ internal static int GetIntegerEncodedSize(long value)
3337
}
3438
}
3539

40+
internal static void WriteByteStringLength(IncrementalHash hasher, ulong value)
41+
{
42+
const CborMajorType MajorType = CborMajorType.ByteString;
43+
CborInitialByte initialByte;
44+
45+
if (value < (byte)CborAdditionalInfo.Additional8BitData)
46+
{
47+
initialByte = new CborInitialByte(MajorType, (CborAdditionalInfo)value);
48+
hasher.AppendData(stackalloc byte[] { initialByte.InitialByte });
49+
}
50+
else if (value <= byte.MaxValue)
51+
{
52+
initialByte = new CborInitialByte(MajorType, CborAdditionalInfo.Additional8BitData);
53+
hasher.AppendData(stackalloc byte[] { initialByte.InitialByte, (byte)value });
54+
}
55+
else if (value <= ushort.MaxValue)
56+
{
57+
initialByte = new CborInitialByte(MajorType, CborAdditionalInfo.Additional16BitData);
58+
Span<byte> buffer = stackalloc byte[1 + sizeof(ushort)];
59+
buffer[0] = initialByte.InitialByte;
60+
BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(1), (ushort)value);
61+
hasher.AppendData(buffer);
62+
}
63+
else if (value <= uint.MaxValue)
64+
{
65+
initialByte = new CborInitialByte(MajorType, CborAdditionalInfo.Additional32BitData);
66+
Span<byte> buffer = stackalloc byte[1 + sizeof(uint)];
67+
buffer[0] = initialByte.InitialByte;
68+
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(1), (uint)value);
69+
hasher.AppendData(buffer);
70+
}
71+
else
72+
{
73+
initialByte = new CborInitialByte(MajorType, CborAdditionalInfo.Additional64BitData);
74+
Span<byte> buffer = stackalloc byte[1 + sizeof(ulong)];
75+
buffer[0] = initialByte.InitialByte;
76+
BinaryPrimitives.WriteUInt64BigEndian(buffer.Slice(1), value);
77+
hasher.AppendData(buffer);
78+
}
79+
}
80+
3681
internal static int GetIntegerEncodedSize(ulong value)
3782
{
3883
if (value < 24)
@@ -56,5 +101,56 @@ internal static int GetIntegerEncodedSize(ulong value)
56101
return 1 + sizeof(ulong);
57102
}
58103
}
104+
105+
[UnsupportedOSPlatform("browser")]
106+
internal static int SignHashWithECDsa(ECDsa key, IncrementalHash hasher, Span<byte> destination)
107+
{
108+
#if NETSTANDARD2_0 || NETFRAMEWORK
109+
byte[] signature = key.SignHash(hasher.GetHashAndReset());
110+
signature.CopyTo(destination);
111+
return signature.Length;
112+
#else
113+
Debug.Assert(hasher.HashLengthInBytes <= 512 / 8); // largest hash we can get (SHA512).
114+
Span<byte> hash = stackalloc byte[hasher.HashLengthInBytes];
115+
hasher.GetHashAndReset(hash);
116+
117+
if (!key.TrySignHash(hash, destination, out int bytesWritten))
118+
{
119+
Debug.Fail("TrySignData failed with a pre-calculated destination");
120+
throw new CryptographicException();
121+
}
122+
123+
return bytesWritten;
124+
#endif
125+
}
126+
127+
[UnsupportedOSPlatform("browser")]
128+
internal static int SignHashWithRSA(RSA key, IncrementalHash hasher, HashAlgorithmName hashAlgorithm, Span<byte> destination)
129+
{
130+
#if NETSTANDARD2_0 || NETFRAMEWORK
131+
byte[] signature = key.SignHash(hasher.GetHashAndReset(), hashAlgorithm, RSASignaturePadding.Pss);
132+
signature.CopyTo(destination);
133+
return signature.Length;
134+
#else
135+
Debug.Assert(hasher.HashLengthInBytes <= 512 / 8); // largest hash we can get (SHA512).
136+
Span<byte> hash = stackalloc byte[hasher.HashLengthInBytes];
137+
hasher.GetHashAndReset(hash);
138+
139+
if (!key.TrySignHash(hash, destination, hashAlgorithm, RSASignaturePadding.Pss, out int bytesWritten))
140+
{
141+
Debug.Fail("TrySignData failed with a pre-calculated destination");
142+
throw new CryptographicException();
143+
}
144+
145+
return bytesWritten;
146+
#endif
147+
}
148+
149+
#if NETSTANDARD2_0 || NETFRAMEWORK
150+
internal static void AppendData(this IncrementalHash hasher, ReadOnlySpan<byte> data)
151+
{
152+
hasher.AppendData(data.ToArray());
153+
}
154+
#endif
59155
}
60156
}

src/libraries/System.Security.Cryptography.Cose/src/System/Security/Cryptography/Cose/CoseMessage.cs

+79-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
using System.Buffers;
55
using System.Diagnostics;
66
using System.Formats.Cbor;
7+
using System.IO;
78
using System.Runtime.InteropServices;
89
using System.Runtime.Versioning;
10+
using System.Threading;
11+
using System.Threading.Tasks;
912

1013
namespace System.Security.Cryptography.Cose
1114
{
@@ -181,19 +184,80 @@ private static byte[] DecodeSignature(CborReader reader)
181184
return reader.ReadByteString();
182185
}
183186

184-
internal static int CreateToBeSigned(string context, ReadOnlySpan<byte> encodedProtectedHeader, ReadOnlySpan<byte> content, Span<byte> destination)
187+
internal static void AppendToBeSigned(Span<byte> buffer, IncrementalHash hasher, string context, ReadOnlySpan<byte> encodedProtectedHeader, ReadOnlySpan<byte> contentBytes, Stream? contentStream, HashAlgorithmName hashAlgorithm)
188+
{
189+
int bytesWritten = CreateToBeSigned(buffer, context, encodedProtectedHeader, ReadOnlySpan<byte>.Empty);
190+
bytesWritten -= 1; // Trim the empty bstr content, it is just a placeholder.
191+
192+
hasher.AppendData(buffer.Slice(0, bytesWritten));
193+
194+
if (contentStream == null)
195+
{
196+
// content length
197+
CoseHelpers.WriteByteStringLength(hasher, (ulong)contentBytes.Length);
198+
199+
//content
200+
hasher.AppendData(contentBytes);
201+
}
202+
else
203+
{
204+
// content length
205+
CoseHelpers.WriteByteStringLength(hasher, (ulong)(contentStream.Length - contentStream.Position));
206+
207+
//content
208+
byte[] contentBuffer = ArrayPool<byte>.Shared.Rent(4096);
209+
int bytesRead;
210+
211+
try
212+
{
213+
while ((bytesRead = contentStream.Read(contentBuffer, 0, contentBuffer.Length)) > 0)
214+
{
215+
hasher.AppendData(contentBuffer, 0, bytesRead);
216+
}
217+
}
218+
finally
219+
{
220+
ArrayPool<byte>.Shared.Return(contentBuffer, clearArray: true);
221+
}
222+
}
223+
}
224+
225+
internal static async Task AppendToBeSignedAsync(byte[] buffer, IncrementalHash hasher, string context, ReadOnlyMemory<byte> encodedProtectedHeader, Stream content, HashAlgorithmName hashAlgorithm, CancellationToken cancellationToken)
226+
{
227+
int bytesWritten = CreateToBeSigned(buffer, context, encodedProtectedHeader.Span, ReadOnlySpan<byte>.Empty);
228+
bytesWritten -= 1; // Trim the empty bstr content, it is just a placeholder.
229+
230+
hasher.AppendData(buffer, 0, bytesWritten);
231+
232+
//content length
233+
CoseHelpers.WriteByteStringLength(hasher, (ulong)(content.Length - content.Position));
234+
235+
// content
236+
byte[] contentBuffer = ArrayPool<byte>.Shared.Rent(4096);
237+
int bytesRead;
238+
#if NETSTANDARD2_0 || NETFRAMEWORK
239+
while ((bytesRead = await content.ReadAsync(contentBuffer, 0, contentBuffer.Length, cancellationToken).ConfigureAwait(false)) > 0)
240+
#else
241+
while ((bytesRead = await content.ReadAsync(contentBuffer, cancellationToken).ConfigureAwait(false)) > 0)
242+
#endif
243+
{
244+
hasher.AppendData(contentBuffer, 0, bytesRead);
245+
}
246+
247+
ArrayPool<byte>.Shared.Return(contentBuffer, clearArray: true);
248+
}
249+
250+
internal static int CreateToBeSigned(Span<byte> destination, string context, ReadOnlySpan<byte> encodedProtectedHeader, ReadOnlySpan<byte> content)
185251
{
186252
var writer = new CborWriter();
187253
writer.WriteStartArray(4);
188254
writer.WriteTextString(context); // context
189255
writer.WriteByteString(encodedProtectedHeader); // body_protected
190256
writer.WriteByteString(Span<byte>.Empty); // external_aad
191-
writer.WriteByteString(content); //payload or content
257+
writer.WriteByteString(content); // content
192258
writer.WriteEndArray();
193-
int bytesWritten = writer.Encode(destination);
194259

195-
Debug.Assert(bytesWritten == writer.BytesWritten && bytesWritten == ComputeToBeSignedEncodedSize(context, encodedProtectedHeader, content));
196-
return bytesWritten;
260+
return writer.Encode(destination);
197261
}
198262

199263
internal static int ComputeToBeSignedEncodedSize(string context, ReadOnlySpan<byte> encodedProtectedHeader, ReadOnlySpan<byte> content)
@@ -225,5 +289,15 @@ internal enum KeyType
225289
ECDsa,
226290
RSA,
227291
}
292+
293+
internal static KeyType GetKeyType(AsymmetricAlgorithm key)
294+
{
295+
return key switch
296+
{
297+
ECDsa => KeyType.ECDsa,
298+
RSA => KeyType.RSA,
299+
_ => throw new CryptographicException(SR.Format(SR.Sign1UnsupportedKey, key.GetType()))
300+
};
301+
}
228302
}
229303
}

0 commit comments

Comments
 (0)