|
// 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.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Formats.Cbor;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace System.Security.Cryptography.Cose
{
/// <summary>
/// Represents a multiple signature COSE_Sign message.
/// </summary>
public sealed class CoseMultiSignMessage : CoseMessage
{
internal const int MultiSignArrayLength = 4;
private const int MultiSignSizeOfCborTag = 2;
internal const int CoseSignatureArrayLength = 3;
private readonly List<CoseSignature> _signatures;
/// <summary>
/// Gets a read-only collection of signatures associated with this message.
/// </summary>
/// <value>A read-only collection of signatures associated with this message.</value>
public ReadOnlyCollection<CoseSignature> Signatures { get; }
internal CoseMultiSignMessage(CoseHeaderMap protectedHeader, CoseHeaderMap unprotectedHeader, byte[]? content, List<CoseSignature> signatures, byte[] encodedProtectedHeader, bool isTagged)
: base(protectedHeader, unprotectedHeader, content, encodedProtectedHeader, isTagged)
{
foreach (CoseSignature s in signatures)
{
s.Message = this;
}
Signatures = new ReadOnlyCollection<CoseSignature>(signatures);
_signatures = signatures;
}
/// <summary>
/// Signs the specified content and encodes it as a COSE_Sign message with detached content.
/// </summary>
/// <param name="detachedContent">The content to sign.</param>
/// <param name="signer">The signer information used to sign <paramref name="detachedContent"/>.</param>
/// <param name="protectedHeaders">The protected header parameters to append to the message's content layer.</param>
/// <param name="unprotectedHeaders">The unprotected header parameters to append to the message's content layer.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <returns>The encoded message.</returns>
/// <exception cref="ArgumentNullException"><paramref name="detachedContent"/> or <paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// The <paramref name="protectedHeaders"/> and <paramref name="unprotectedHeaders"/> collections have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
public static byte[] SignDetached(byte[] detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, byte[]? associatedData = null)
{
if (detachedContent is null)
throw new ArgumentNullException(nameof(detachedContent));
return SignCore(detachedContent, null, signer, protectedHeaders, unprotectedHeaders, associatedData, isDetached: true);
}
/// <summary>
/// Signs the specified content and encodes it as a COSE_Sign message with embedded content.
/// </summary>
/// <param name="embeddedContent">The content to sign and to include in the message.</param>
/// <param name="signer">The signer information used to sign <paramref name="embeddedContent"/>.</param>
/// <param name="protectedHeaders">The protected header parameters to append to the message's content layer.</param>
/// <param name="unprotectedHeaders">The unprotected header parameters to append to the message's content layer.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <returns>The encoded message.</returns>
/// <exception cref="ArgumentNullException"><paramref name="embeddedContent"/> or <paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// The <paramref name="protectedHeaders"/> and <paramref name="unprotectedHeaders"/> collections have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
public static byte[] SignEmbedded(byte[] embeddedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, byte[]? associatedData = null)
{
if (embeddedContent is null)
throw new ArgumentNullException(nameof(embeddedContent));
return SignCore(embeddedContent, null, signer, protectedHeaders, unprotectedHeaders, associatedData, isDetached: false);
}
/// <summary>
/// Signs the specified content and encodes it as a COSE_Sign message with detached content.
/// </summary>
/// <param name="detachedContent">The content to sign.</param>
/// <param name="signer">The signer information used to sign <paramref name="detachedContent"/>.</param>
/// <param name="protectedHeaders">The protected header parameters to append to the message's content layer.</param>
/// <param name="unprotectedHeaders">The unprotected header parameters to append to the message's content layer.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <returns>The encoded message.</returns>
/// <exception cref="ArgumentNullException"><paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// The <paramref name="protectedHeaders"/> and <paramref name="unprotectedHeaders"/> collections have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
public static byte[] SignDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default)
=> SignCore(detachedContent, null, signer, protectedHeaders, unprotectedHeaders, associatedData, isDetached: true);
/// <summary>
/// Signs the specified content and encodes it as a COSE_Sign message with detached content.
/// </summary>
/// <param name="embeddedContent">The content to sign and to include in the message.</param>
/// <param name="signer">The signer information used to sign <paramref name="embeddedContent"/>.</param>
/// <param name="protectedHeaders">The protected header parameters to append to the message's content layer.</param>
/// <param name="unprotectedHeaders">The unprotected header parameters to append to the message's content layer.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <returns>The encoded message.</returns>
/// <exception cref="ArgumentNullException"><paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// The <paramref name="protectedHeaders"/> and <paramref name="unprotectedHeaders"/> collections have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
public static byte[] SignEmbedded(ReadOnlySpan<byte> embeddedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default)
=> SignCore(embeddedContent, null, signer, protectedHeaders, unprotectedHeaders, associatedData, isDetached: false);
/// <summary>
/// Signs the specified content and encodes it as a COSE_Sign message with detached content.
/// </summary>
/// <param name="detachedContent">The content to sign.</param>
/// <param name="signer">The signer information used to sign <paramref name="detachedContent"/>.</param>
/// <param name="protectedHeaders">The protected header parameters to append to the message's content layer.</param>
/// <param name="unprotectedHeaders">The unprotected header parameters to append to the message's content layer.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <returns>The encoded message.</returns>
/// <exception cref="ArgumentNullException"><paramref name="detachedContent"/> or <paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// <paramref name="detachedContent"/> does not support reading or seeking.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <paramref name="protectedHeaders"/> and <paramref name="unprotectedHeaders"/> collections have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
/// <seealso cref="SignDetachedAsync"/>
public static byte[] SignDetached(Stream detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default)
{
if (detachedContent is null)
throw new ArgumentNullException(nameof(detachedContent));
if (!detachedContent.CanRead)
throw new ArgumentException(SR.Sign1ArgumentStreamNotReadable, nameof(detachedContent));
if (!detachedContent.CanSeek)
throw new ArgumentException(SR.Sign1ArgumentStreamNotSeekable, nameof(detachedContent));
return SignCore(default, detachedContent, signer, protectedHeaders, unprotectedHeaders, associatedData, isDetached: true);
}
private static byte[] SignCore(
ReadOnlySpan<byte> content,
Stream? contentStream,
CoseSigner signer,
CoseHeaderMap? protectedHeaders,
CoseHeaderMap? unprotectedHeaders,
ReadOnlySpan<byte> associatedData,
bool isDetached)
{
if (signer is null)
throw new ArgumentNullException(nameof(signer));
ValidateBeforeSign(signer, protectedHeaders, unprotectedHeaders);
int expectedSize = ComputeEncodedSize(signer, protectedHeaders, unprotectedHeaders, content.Length, isDetached);
var buffer = new byte[expectedSize];
int bytesWritten = CreateCoseMultiSignMessage(content, contentStream, buffer, signer, protectedHeaders, unprotectedHeaders, associatedData, isDetached);
Debug.Assert(expectedSize == bytesWritten);
return buffer;
}
/// <summary>
/// Asynchronously signs the specified content and encodes it as a COSE_Sign message with detached content.
/// </summary>
/// <param name="detachedContent">The content to sign.</param>
/// <param name="signer">The signer information used to sign <paramref name="detachedContent"/>.</param>
/// <param name="protectedHeaders">The protected header parameters to append to the message's content layer.</param>
/// <param name="unprotectedHeaders">The unprotected header parameters to append to the message's content layer.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The value of its <see cref="Task{T}.Result"/> property contains the encoded message.</returns>
/// <exception cref="ArgumentNullException"><paramref name="detachedContent"/> or <paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// <paramref name="detachedContent"/> does not support reading or seeking.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <paramref name="protectedHeaders"/> and <paramref name="unprotectedHeaders"/> collections have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
public static Task<byte[]> SignDetachedAsync(
Stream detachedContent,
CoseSigner signer,
CoseHeaderMap? protectedHeaders = null,
CoseHeaderMap? unprotectedHeaders = null,
ReadOnlyMemory<byte> associatedData = default,
CancellationToken cancellationToken = default)
{
if (detachedContent is null)
throw new ArgumentNullException(nameof(detachedContent));
if (signer is null)
throw new ArgumentNullException(nameof(signer));
if (!detachedContent.CanRead)
throw new ArgumentException(SR.Sign1ArgumentStreamNotReadable, nameof(detachedContent));
if (!detachedContent.CanSeek)
throw new ArgumentException(SR.Sign1ArgumentStreamNotSeekable, nameof(detachedContent));
ValidateBeforeSign(signer, protectedHeaders, unprotectedHeaders);
int expectedSize = ComputeEncodedSize(signer, protectedHeaders, unprotectedHeaders, contentLength: 0, isDetached: true);
return SignAsyncCore(expectedSize, detachedContent, signer, protectedHeaders, unprotectedHeaders, associatedData, cancellationToken);
}
private static async Task<byte[]> SignAsyncCore(
int expectedSize,
Stream content,
CoseSigner signer,
CoseHeaderMap? protectedHeaders,
CoseHeaderMap? unprotectedHeaders,
ReadOnlyMemory<byte> associatedData,
CancellationToken cancellationToken)
{
byte[] buffer = new byte[expectedSize];
int bytesWritten = await CreateCoseMultiSignMessageAsync(content, buffer, signer, protectedHeaders, unprotectedHeaders, associatedData, cancellationToken).ConfigureAwait(false);
Debug.Assert(buffer.Length == bytesWritten);
return buffer;
}
/// <summary>
/// Attempts to sign the specified content and encode it as a COSE_Sign message with detached content into the specified buffer.
/// </summary>
/// <param name="detachedContent">The content to sign.</param>
/// <param name="destination">The buffer in which to write the encoded bytes.</param>
/// <param name="signer">The signer information used to sign <paramref name="detachedContent"/>.</param>
/// <param name="bytesWritten">On success, receives the number of bytes written to <paramref name="destination"/>.</param>
/// <param name="protectedHeaders">The protected header parameters to append to the message's content layer.</param>
/// <param name="unprotectedHeaders">The unprotected header parameters to append to the message's content layer.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <returns><see langword="true"/> if <paramref name="destination"/> had sufficient length to receive the encoded message; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// The <paramref name="protectedHeaders"/> and <paramref name="unprotectedHeaders"/> collections have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
public static bool TrySignDetached(ReadOnlySpan<byte> detachedContent, Span<byte> destination, CoseSigner signer, out int bytesWritten, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default)
=> TrySign(detachedContent, destination, signer, protectedHeaders, unprotectedHeaders, out bytesWritten, associatedData, isDetached: true);
/// <summary>
/// Signs the specified content and encodes it as a COSE_Sign message with embedded content.
/// </summary>
/// <param name="embeddedContent">The content to sign and to include in the message.</param>
/// <param name="destination">The buffer in which to write the encoded bytes.</param>
/// <param name="signer">The signer information used to sign <paramref name="embeddedContent"/>.</param>
/// <param name="bytesWritten">On success, receives the number of bytes written to <paramref name="destination"/>.</param>
/// <param name="protectedHeaders">The protected header parameters to append to the message's content layer.</param>
/// <param name="unprotectedHeaders">The unprotected header parameters to append to the message's content layer.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <returns><see langword="true"/> if <paramref name="destination"/> had sufficient length to receive the encoded message; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// The <paramref name="protectedHeaders"/> and <paramref name="unprotectedHeaders"/> collections have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
public static bool TrySignEmbedded(ReadOnlySpan<byte> embeddedContent, Span<byte> destination, CoseSigner signer, out int bytesWritten, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default)
=> TrySign(embeddedContent, destination, signer, protectedHeaders, unprotectedHeaders, out bytesWritten, associatedData, isDetached: false);
private static bool TrySign(ReadOnlySpan<byte> content, Span<byte> destination, CoseSigner signer, CoseHeaderMap? protectedHeaders, CoseHeaderMap? unprotectedHeaders, out int bytesWritten, ReadOnlySpan<byte> associatedData, bool isDetached)
{
if (signer is null)
throw new ArgumentNullException(nameof(signer));
ValidateBeforeSign(signer, protectedHeaders, unprotectedHeaders);
int expectedSize = ComputeEncodedSize(signer, protectedHeaders, unprotectedHeaders, content.Length, isDetached);
if (expectedSize > destination.Length)
{
bytesWritten = 0;
return false;
}
bytesWritten = CreateCoseMultiSignMessage(content, null, destination, signer, protectedHeaders, unprotectedHeaders, associatedData, isDetached);
Debug.Assert(expectedSize == bytesWritten);
return true;
}
private static int CreateCoseMultiSignMessage(
ReadOnlySpan<byte> content,
Stream? contentStream,
Span<byte> buffer,
CoseSigner signer,
CoseHeaderMap? protectedHeaders,
CoseHeaderMap? unprotectedHeaders,
ReadOnlySpan<byte> associatedData,
bool isDetached)
{
var writer = new CborWriter();
writer.WriteTag(MultiSignTag);
writer.WriteStartArray(MultiSignArrayLength);
int protectedMapBytesWritten = CoseHelpers.WriteHeaderMap(buffer, writer, protectedHeaders, isProtected: true, null);
// We're going to use the encoded protected headers again after this step (for the toBeSigned construction),
// so don't overwrite them yet.
CoseHelpers.WriteHeaderMap(buffer.Slice(protectedMapBytesWritten), writer, unprotectedHeaders, isProtected: false, null);
CoseHelpers.WriteContent(writer, content, isDetached);
WriteCoseSignaturesArray(writer, signer, buffer.Slice(protectedMapBytesWritten), buffer.Slice(0, protectedMapBytesWritten), associatedData, content, contentStream);
writer.WriteEndArray();
return writer.Encode(buffer);
}
private static async Task<int> CreateCoseMultiSignMessageAsync(
Stream content,
byte[] buffer,
CoseSigner signer,
CoseHeaderMap? protectedHeaders,
CoseHeaderMap? unprotectedHeaders,
ReadOnlyMemory<byte> associatedData,
CancellationToken cancellationToken)
{
var writer = new CborWriter();
writer.WriteTag(MultiSignTag);
writer.WriteStartArray(MultiSignArrayLength);
int protectedMapBytesWritten = CoseHelpers.WriteHeaderMap(buffer, writer, protectedHeaders, isProtected: true, null);
// We're going to use the encoded protected headers again after this step (for the toBeSigned construction),
// so don't overwrite them yet.
CoseHelpers.WriteHeaderMap(buffer.AsSpan(protectedMapBytesWritten), writer, unprotectedHeaders, isProtected: false, null);
CoseHelpers.WriteContent(writer, default, isDetached: true);
await WriteSignatureAsync(writer, signer, buffer, buffer.AsMemory(0, protectedMapBytesWritten), associatedData, content, cancellationToken).ConfigureAwait(false);
writer.WriteEndArray();
return writer.Encode(buffer);
}
private static void ValidateBeforeSign(CoseSigner signer, CoseHeaderMap? protectedHeaders, CoseHeaderMap? unprotectedHeaders)
{
if (ContainDuplicateLabels(signer._protectedHeaders, signer._unprotectedHeaders))
{
throw new ArgumentException(SR.Sign1SignHeaderDuplicateLabels, nameof(signer));
}
if (ContainDuplicateLabels(protectedHeaders, unprotectedHeaders))
{
throw new ArgumentException(SR.Sign1SignHeaderDuplicateLabels);
}
if (MissingCriticalHeaders(signer._protectedHeaders, out string? labelName))
{
throw new ArgumentException(SR.Format(SR.CriticalHeaderMissing, labelName), nameof(signer));
}
if (MissingCriticalHeaders(protectedHeaders, out labelName))
{
throw new ArgumentException(SR.Format(SR.CriticalHeaderMissing, labelName), nameof(protectedHeaders));
}
}
private static void WriteCoseSignaturesArray(
CborWriter writer,
CoseSigner signer,
Span<byte> buffer,
ReadOnlySpan<byte> bodyProtected,
ReadOnlySpan<byte> associatedData,
ReadOnlySpan<byte> content,
Stream? contentStream)
{
writer.WriteStartArray(1);
writer.WriteStartArray(CoseSignatureArrayLength);
int signProtectedBytesWritten = CoseHelpers.WriteHeaderMap(buffer, writer, signer._protectedHeaders, isProtected: true, signer._algHeaderValueToSlip);
CoseHelpers.WriteHeaderMap(buffer.Slice(signProtectedBytesWritten), writer, signer.UnprotectedHeaders, isProtected: false, null);
using (IncrementalHash hasher = IncrementalHash.CreateHash(signer.HashAlgorithm))
{
AppendToBeSigned(buffer, hasher, SigStructureContext.Signature, bodyProtected, buffer.Slice(0, signProtectedBytesWritten), associatedData, content, contentStream);
CoseHelpers.WriteSignature(buffer, hasher, writer, signer);
}
writer.WriteEndArray();
writer.WriteEndArray();
}
private static async Task WriteSignatureAsync(
CborWriter writer,
CoseSigner signer,
byte[] buffer,
ReadOnlyMemory<byte> bodyProtected,
ReadOnlyMemory<byte> associatedData,
Stream contentStream,
CancellationToken cancellationToken)
{
int start = bodyProtected.Length;
writer.WriteStartArray(1);
writer.WriteStartArray(CoseSignatureArrayLength);
int signProtectedBytesWritten = CoseHelpers.WriteHeaderMap(buffer.AsSpan(start), writer, signer._protectedHeaders, isProtected: true, signer._algHeaderValueToSlip);
CoseHelpers.WriteHeaderMap(buffer.AsSpan(start + signProtectedBytesWritten), writer, signer.UnprotectedHeaders, isProtected: false, null);
HashAlgorithmName hashAlgorithm = signer.HashAlgorithm;
using (IncrementalHash hasher = IncrementalHash.CreateHash(hashAlgorithm))
{
// We can use the whole buffer at this point as the space for bodyProtected and signProtected is consumed.
await AppendToBeSignedAsync(buffer, hasher, SigStructureContext.Signature, bodyProtected, buffer.AsMemory(start, signProtectedBytesWritten), associatedData, contentStream, cancellationToken).ConfigureAwait(false);
CoseHelpers.WriteSignature(buffer, hasher, writer, signer);
}
writer.WriteEndArray();
writer.WriteEndArray();
}
private static int ComputeEncodedSize(CoseSigner signer, CoseHeaderMap? protectedHeaders, CoseHeaderMap? unprotectedHeaders, int contentLength, bool isDetached)
{
// tag + array(4) + encoded protected header map + unprotected header map + content + [+COSE_Signature].
int encodedSize = MultiSignSizeOfCborTag + CoseHelpers.SizeOfArrayOfLessThan24;
int protectedHeadersSize = CoseHeaderMap.ComputeEncodedSize(protectedHeaders);
if (protectedHeadersSize > 1)
{
encodedSize += CoseHelpers.GetByteStringEncodedSize(protectedHeadersSize);
}
else
{
Debug.Assert(protectedHeadersSize == 1);
encodedSize += protectedHeadersSize;
}
encodedSize += CoseHeaderMap.ComputeEncodedSize(unprotectedHeaders);
if (isDetached)
{
encodedSize += CoseHelpers.SizeOfNull;
}
else
{
encodedSize += CoseHelpers.GetByteStringEncodedSize(contentLength);
}
encodedSize += CoseHelpers.SizeOfArrayOfLessThan24;
encodedSize += CoseHelpers.SizeOfArrayOfLessThan24;
encodedSize += CoseHelpers.GetByteStringEncodedSize(CoseHeaderMap.ComputeEncodedSize(signer._protectedHeaders, signer._algHeaderValueToSlip));
encodedSize += CoseHeaderMap.ComputeEncodedSize(signer._unprotectedHeaders);
encodedSize += CoseHelpers.GetByteStringEncodedSize(CoseHelpers.ComputeSignatureSize(signer));
return encodedSize;
}
/// <summary>
/// Calculates the number of bytes produced by encoding this message.
/// </summary>
/// <returns>The number of bytes produced by encoding this message.</returns>
public override int GetEncodedLength()
{
int encodedLength = CoseHelpers.GetCoseSignEncodedLengthMinusSignature(_isTagged, MultiSignSizeOfCborTag, _protectedHeaderAsBstr.Length, UnprotectedHeaders, _content);
encodedLength += CoseHelpers.GetIntegerEncodedSize(Signatures.Count);
foreach (CoseSignature signature in Signatures)
{
encodedLength += CoseHelpers.SizeOfArrayOfLessThan24;
encodedLength += CoseHelpers.GetByteStringEncodedSize(signature._encodedSignProtectedHeaders.Length);
encodedLength += CoseHeaderMap.ComputeEncodedSize(signature.UnprotectedHeaders);
encodedLength += CoseHelpers.GetByteStringEncodedSize(signature._signature.Length);
}
return encodedLength;
}
/// <summary>
/// Attempts to encode this message into the specified buffer.
/// </summary>
/// <param name="destination">The buffer in which to write the encoded value.</param>
/// <param name="bytesWritten">On success, receives the number of bytes written to <paramref name="destination"/>.</param>
/// <returns><see langword="true"/> if <paramref name="destination"/> had sufficient length to receive the value; otherwise, <see langword="false"/>.</returns>
/// <remarks>Use <see cref="GetEncodedLength()"/> to determine how many bytes result in encoding this message.</remarks>
/// <exception cref="InvalidOperationException">
/// <para>
/// The <see cref="CoseMessage.ProtectedHeaders"/> and <see cref="CoseMessage.UnprotectedHeaders"/> collections have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// The message does not contain at least one signature.
/// </para>
/// </exception>
/// <seealso cref="GetEncodedLength()"/>
public override bool TryEncode(Span<byte> destination, out int bytesWritten)
{
if (ContainDuplicateLabels(ProtectedHeaders, UnprotectedHeaders))
{
throw new InvalidOperationException(SR.Sign1SignHeaderDuplicateLabels);
}
if (Signatures.Count < 1)
{
throw new InvalidOperationException(SR.MultiSignMessageMustCarryAtLeastOneSignature);
}
if (destination.Length < GetEncodedLength())
{
bytesWritten = 0;
return false;
}
var writer = new CborWriter();
if (_isTagged)
{
writer.WriteTag(MultiSignTag);
}
writer.WriteStartArray(MultiSignArrayLength);
writer.WriteByteString(_protectedHeaderAsBstr);
CoseHelpers.WriteHeaderMap(destination, writer, UnprotectedHeaders, isProtected: false, null);
CoseHelpers.WriteContent(writer, Content.GetValueOrDefault().Span, !Content.HasValue);
writer.WriteStartArray(Signatures.Count);
foreach (CoseSignature signature in Signatures)
{
if (ContainDuplicateLabels(signature.ProtectedHeaders, signature.UnprotectedHeaders))
{
throw new InvalidOperationException(SR.Sign1SignHeaderDuplicateLabels);
}
writer.WriteStartArray(CoseSignatureArrayLength);
writer.WriteByteString(signature._encodedSignProtectedHeaders);
CoseHelpers.WriteHeaderMap(destination, writer, signature.UnprotectedHeaders, false, null);
writer.WriteByteString(signature._signature);
writer.WriteEndArray();
}
writer.WriteEndArray();
writer.WriteEndArray();
bytesWritten = writer.Encode(destination);
Debug.Assert(bytesWritten == GetEncodedLength());
return true;
}
/// <summary>
/// Adds a signature for the content embedded in this message.
/// </summary>
/// <param name="signer">The signer information used to sign the content.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <exception cref="ArgumentNullException"><paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
/// <exception cref="InvalidOperationException">The content is detached from this message, use an overload that accepts a detached content.</exception>
public void AddSignatureForEmbedded(CoseSigner signer, byte[]? associatedData = null)
=> AddSignatureForEmbedded(signer, associatedData.AsSpan());
/// <summary>
/// Adds a signature for the content embedded in this message.
/// </summary>
/// <param name="signer">The signer information used to sign the content.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <exception cref="ArgumentNullException"><paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
/// <exception cref="InvalidOperationException">The content is detached from this message, use an overload that accepts a detached content.</exception>
public void AddSignatureForEmbedded(CoseSigner signer, ReadOnlySpan<byte> associatedData)
{
if (signer == null)
{
throw new ArgumentNullException(nameof(signer));
}
if (IsDetached)
{
throw new InvalidOperationException(SR.ContentWasDetached);
}
AddSignatureCore(_content, null, signer, associatedData);
}
/// <summary>
/// Adds a signature for the specified content to this message.
/// </summary>
/// <param name="detachedContent">The content to sign.</param>
/// <param name="signer">The signer information used to sign the content.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <exception cref="ArgumentNullException"><paramref name="detachedContent"/> or <paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
/// <exception cref="InvalidOperationException">The content is embedded on this message, use an overload that uses embedded content.</exception>
public void AddSignatureForDetached(byte[] detachedContent, CoseSigner signer, byte[]? associatedData = null)
{
if (detachedContent == null)
{
throw new ArgumentNullException(nameof(detachedContent));
}
AddSignatureForDetached(detachedContent.AsSpan(), signer, associatedData);
}
/// <summary>
/// Adds a signature for the specified content to this message.
/// </summary>
/// <param name="detachedContent">The content to sign.</param>
/// <param name="signer">The signer information used to sign the content.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <exception cref="ArgumentNullException"><paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
/// <exception cref="InvalidOperationException">The content is embedded on this message, use an overload that uses embedded content.</exception>
public void AddSignatureForDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default)
{
if (signer == null)
{
throw new ArgumentNullException(nameof(signer));
}
if (!IsDetached)
{
throw new InvalidOperationException(SR.ContentWasEmbedded);
}
AddSignatureCore(detachedContent, null, signer, associatedData);
}
/// <summary>
/// Adds a signature for the specified content to this message.
/// </summary>
/// <param name="detachedContent">The content to sign.</param>
/// <param name="signer">The signer information used to sign the content.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <exception cref="ArgumentNullException"><paramref name="detachedContent"/> or <paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// <paramref name="detachedContent"/> does not support reading or seeking.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
/// <exception cref="InvalidOperationException">The content is embedded on this message, use an overload that uses embedded content.</exception>
public void AddSignatureForDetached(Stream detachedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default)
{
if (detachedContent == null)
{
throw new ArgumentNullException(nameof(detachedContent));
}
if (!IsDetached)
{
throw new InvalidOperationException(SR.ContentWasEmbedded);
}
AddSignatureCore(default, detachedContent, signer, associatedData);
}
private void AddSignatureCore(ReadOnlySpan<byte> contentBytes, Stream? contentStream, CoseSigner signer, ReadOnlySpan<byte> associatedData)
{
ValidateBeforeSign(signer, null, null);
CoseHeaderMap signProtectedHeaders = signer.ProtectedHeaders;
int? algHeaderValueToSlip = signer._algHeaderValueToSlip;
int signProtectedEncodedLength = CoseHeaderMap.ComputeEncodedSize(signProtectedHeaders, algHeaderValueToSlip);
int toBeSignedLength = ComputeToBeSignedEncodedSize(
SigStructureContext.Signature,
_protectedHeaderAsBstr.Length,
signProtectedEncodedLength,
associatedData.Length,
contentLength: 0);
byte[] buffer = ArrayPool<byte>.Shared.Rent(Math.Max(toBeSignedLength, CoseHelpers.ComputeSignatureSize(signer)));
try
{
Span<byte> bufferSpan = buffer;
int bytesWritten = CoseHeaderMap.Encode(signProtectedHeaders, bufferSpan, isProtected: true, algHeaderValueToSlip);
byte[] encodedSignProtected = bufferSpan.Slice(0, bytesWritten).ToArray();
using (IncrementalHash hasher = IncrementalHash.CreateHash(signer.HashAlgorithm))
{
AppendToBeSigned(bufferSpan, hasher, SigStructureContext.Signature, _protectedHeaderAsBstr, encodedSignProtected, associatedData, contentBytes, contentStream);
bytesWritten = CoseHelpers.SignHash(signer, hasher, buffer);
byte[] signature = bufferSpan.Slice(0, bytesWritten).ToArray();
_signatures.Add(new CoseSignature(this, signProtectedHeaders, signer.UnprotectedHeaders, encodedSignProtected, signature));
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
}
}
/// <summary>
/// Asynchronously adds a signature for the specified content to this message.
/// </summary>
/// <param name="detachedContent">The content to sign.</param>
/// <param name="signer">The signer information used to sign the content.</param>
/// <param name="associatedData">The extra data associated with the signature, which must also be provided during verification.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="detachedContent"/> or <paramref name="signer"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para>
/// <paramref name="detachedContent"/> does not support reading or seeking.
/// </para>
/// <para>-or-</para>
/// <para>
/// The <see cref="CoseSigner.ProtectedHeaders"/> and <see cref="CoseSigner.UnprotectedHeaders"/> collections in <paramref name="signer"/> have one or more labels in common.
/// </para>
/// <para>-or-</para>
/// <para>
/// One or more of the labels specified in a <see cref="CoseHeaderLabel.CriticalHeaders"/> header is missing.
/// </para>
/// </exception>
/// <exception cref="InvalidOperationException">The content is embedded on this message, use an overload that uses embedded content.</exception>
public Task AddSignatureForDetachedAsync(Stream detachedContent, CoseSigner signer, ReadOnlyMemory<byte> associatedData = default, CancellationToken cancellationToken = default)
{
if (detachedContent == null)
{
throw new ArgumentNullException(nameof(detachedContent));
}
if (!IsDetached)
{
throw new InvalidOperationException(SR.ContentWasEmbedded);
}
return AddSignatureCoreAsync(detachedContent, signer, associatedData, cancellationToken);
}
private async Task AddSignatureCoreAsync(Stream content, CoseSigner signer, ReadOnlyMemory<byte> associatedData, CancellationToken cancellationToken)
{
ValidateBeforeSign(signer, null, null);
CoseHeaderMap signProtectedHeaders = signer.ProtectedHeaders;
int? algHeaderValueToSlip = signer._algHeaderValueToSlip;
int signProtectedEncodedLength = CoseHeaderMap.ComputeEncodedSize(signProtectedHeaders, algHeaderValueToSlip);
int toBeSignedLength = ComputeToBeSignedEncodedSize(
SigStructureContext.Signature,
_protectedHeaderAsBstr.Length,
signProtectedEncodedLength,
associatedData.Length,
contentLength: 0);
byte[] buffer = ArrayPool<byte>.Shared.Rent(Math.Max(toBeSignedLength, CoseHelpers.ComputeSignatureSize(signer)));
int bytesWritten = CoseHeaderMap.Encode(signProtectedHeaders, buffer, isProtected: true, algHeaderValueToSlip);
byte[] encodedSignProtected = buffer.AsSpan(0, bytesWritten).ToArray();
using (IncrementalHash hasher = IncrementalHash.CreateHash(signer.HashAlgorithm))
{
await AppendToBeSignedAsync(buffer, hasher, SigStructureContext.Signature, _protectedHeaderAsBstr, encodedSignProtected, associatedData, content, cancellationToken).ConfigureAwait(false);
bytesWritten = CoseHelpers.SignHash(signer, hasher, buffer);
byte[] signature = buffer.AsSpan(0, bytesWritten).ToArray();
_signatures.Add(new CoseSignature(this, signProtectedHeaders, signer.UnprotectedHeaders, encodedSignProtected, signature));
}
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
}
/// <summary>
/// Removes the specified signature from the message.
/// </summary>
/// <param name="signature">The signature to remove from the message.</param>
/// <exception cref="ArgumentNullException"><paramref name="signature"/> is <see langword="null"/>.</exception>
/// <seealso cref="Signatures"/>
public void RemoveSignature(CoseSignature signature)
{
if (signature == null)
{
throw new ArgumentNullException(nameof(signature));
}
_signatures.Remove(signature);
}
/// <summary>
/// Removes the signature at the specified index from the message.
/// </summary>
/// <param name="index">The zero-based index of the signature to remove.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// <para>
/// <paramref name="index"/> is less than 0.
/// </para>
/// <para>-or-</para>
/// <para>
/// <paramref name="index"/> is equal to or greater than the number of elements in <see cref="Signatures"/>.
/// </para>
/// </exception>
/// <seealso cref="Signatures"/>
public void RemoveSignature(int index)
=> _signatures.RemoveAt(index);
}
}
|