File: System\Security\Cryptography\Cose\CoseSignature.cs
Web Access
Project: src\src\libraries\System.Security.Cryptography.Cose\src\System.Security.Cryptography.Cose.csproj (System.Security.Cryptography.Cose)
// 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.IO;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Security.Cryptography.Cose
{
    /// <summary>
    /// Represents a COSE_Signature that carries one signature and information about that signature associated with a <see cref="CoseMultiSignMessage"/>.
    /// </summary>
    public sealed class CoseSignature
    {
        internal readonly byte[] _encodedSignProtectedHeaders;
        internal readonly byte[] _signature;
        private CoseMultiSignMessage? _message;
 
        /// <summary>
        /// Gets the protected header parameters associated with this instance.
        /// </summary>
        /// <value>A collection of protected header parameters associated with this instance.</value>
        public CoseHeaderMap ProtectedHeaders { get; }
 
        /// <summary>
        /// Gets the unprotected header parameters associated with this instance.
        /// </summary>
        /// <value>A collection of unprotected header parameters associated with this instance.</value>
        public CoseHeaderMap UnprotectedHeaders { get; }
 
        /// <summary>
        /// Gets the raw bytes of the protected header parameters associated with this instance.
        /// </summary>
        /// <value>A region of memory that contains the raw bytes of the protected header parameters associated with this instance.</value>
        public ReadOnlyMemory<byte> RawProtectedHeaders => _encodedSignProtectedHeaders;
 
        /// <summary>
        /// Gets the digital signature.
        /// </summary>
        /// <value>A region of memory that contains the digital signature.</value>
        public ReadOnlyMemory<byte> Signature => _signature;
 
        internal CoseSignature(CoseMultiSignMessage message, CoseHeaderMap protectedHeaders, CoseHeaderMap unprotectedHeaders, byte[] encodedSignProtectedHeaders, byte[] signature)
            : this(protectedHeaders, unprotectedHeaders, encodedSignProtectedHeaders, signature)
        {
            Message = message;
        }
 
        internal CoseSignature(CoseHeaderMap protectedHeaders, CoseHeaderMap unprotectedHeaders, byte[] encodedSignProtectedHeaders, byte[] signature)
        {
            ProtectedHeaders = protectedHeaders;
            UnprotectedHeaders = unprotectedHeaders;
            _encodedSignProtectedHeaders = encodedSignProtectedHeaders;
            _signature = signature;
        }
 
        internal CoseMultiSignMessage Message
        {
            get
            {
                Debug.Assert(_message != null);
                return _message;
            }
            set
            {
                _message = value;
            }
        }
 
        /// <summary>
        /// Verifies that the signature is valid for the message's content using the specified key.
        /// </summary>
        /// <param name="key">The private key used to sign the content.</param>
        /// <param name="associatedData">The extra data associated with the signature, which must match the value provided during signing.</param>
        /// <returns><see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException"><paramref name="key"/> is of an unsupported type.</exception>
        /// <exception cref="InvalidOperationException">The content is detached from the associated message, use an overload that accepts a detached content.</exception>
        /// <exception cref="CryptographicException">
        ///   <para>
        ///     <see cref="CoseMessage.ProtectedHeaders"/> does not have a value for the <see cref="CoseHeaderLabel.Algorithm"/> header.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was incorrectly formatted.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was not one of the values supported by this implementation.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header doesn't match with the algorithms supported by the specified <paramref name="key"/>.
        ///   </para>
        /// </exception>
        /// <seealso cref="VerifyDetached(AsymmetricAlgorithm, ReadOnlySpan{byte}, ReadOnlySpan{byte})"/>
        /// <seealso cref="CoseMessage.Content"/>
        public bool VerifyEmbedded(AsymmetricAlgorithm key, ReadOnlySpan<byte> associatedData)
        {
            if (key is null)
            {
                throw new ArgumentNullException(nameof(key));
            }
 
            if (Message.IsDetached)
            {
                throw new InvalidOperationException(SR.ContentWasDetached);
            }
 
            return VerifyCore(key, Message.Content.Value.Span, null, associatedData, CoseHelpers.GetKeyType(key));
        }
 
        /// <summary>
        /// Verifies that the signature is valid for the message's content using the specified key.
        /// </summary>
        /// <param name="key">The private key used to sign the content.</param>
        /// <param name="associatedData">The extra data associated with the signature, which must match the value provided during signing.</param>
        /// <returns><see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException"><paramref name="key"/> is of an unsupported type.</exception>
        /// <exception cref="InvalidOperationException">The content is detached from the associated message, use an overload that accepts a detached content.</exception>
        /// <exception cref="CryptographicException">
        ///   <para>
        ///     <see cref="CoseMessage.ProtectedHeaders"/> does not have a value for the <see cref="CoseHeaderLabel.Algorithm"/> header.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was incorrectly formatted.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was not one of the values supported by this implementation.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header doesn't match with the algorithms supported by the specified <paramref name="key"/>.
        ///   </para>
        /// </exception>
        /// <seealso cref="VerifyDetached(AsymmetricAlgorithm, byte[], byte[])"/>
        /// <seealso cref="CoseMessage.Content"/>
        public bool VerifyEmbedded(AsymmetricAlgorithm key, byte[]? associatedData = null)
        {
            if (key is null)
            {
                throw new ArgumentNullException(nameof(key));
            }
 
            if (Message.IsDetached)
            {
                throw new InvalidOperationException(SR.ContentWasDetached);
            }
 
            return VerifyCore(key, Message.Content.Value.Span, null, associatedData, CoseHelpers.GetKeyType(key));
        }
 
        /// <summary>
        /// Verifies that the signature is valid for the message's content using the specified key.
        /// </summary>
        /// <param name="key">The private key used to sign the content.</param>
        /// <param name="detachedContent">The content that was previously signed.</param>
        /// <param name="associatedData">The extra data associated with the signature, which must match the value provided during signing.</param>
        /// <returns><see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="key"/> or <paramref name="detachedContent"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException"><paramref name="key"/> is of an unsupported type.</exception>
        /// <exception cref="InvalidOperationException">The content is embedded on the associated message, use an overload that uses embedded content.</exception>
        /// <exception cref="CryptographicException">
        ///   <para>
        ///     <see cref="CoseMessage.ProtectedHeaders"/> does not have a value for the <see cref="CoseHeaderLabel.Algorithm"/> header.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was incorrectly formatted.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was not one of the values supported by this implementation.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header doesn't match with the algorithms supported by the specified <paramref name="key"/>.
        ///   </para>
        /// </exception>
        /// <seealso cref="VerifyEmbedded(AsymmetricAlgorithm, byte[])"/>
        /// <seealso cref="CoseMessage.Content"/>
        public bool VerifyDetached(AsymmetricAlgorithm key, byte[] detachedContent, byte[]? associatedData = null)
        {
            if (key is null)
            {
                throw new ArgumentNullException(nameof(key));
            }
 
            if (detachedContent is null)
            {
                throw new ArgumentNullException(nameof(detachedContent));
            }
 
            if (!Message.IsDetached)
            {
                throw new InvalidOperationException(SR.ContentWasEmbedded);
            }
 
            return VerifyCore(key, detachedContent, null, associatedData, CoseHelpers.GetKeyType(key));
        }
 
        /// <summary>
        /// Verifies that the signature is valid for the message's content using the specified key.
        /// </summary>
        /// <param name="key">The private key used to sign the content.</param>
        /// <param name="detachedContent">The content that was previously signed.</param>
        /// <param name="associatedData">The extra data associated with the signature, which must match the value provided during signing.</param>
        /// <returns><see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException"><paramref name="key"/> is of an unsupported type.</exception>
        /// <exception cref="InvalidOperationException">The content is embedded on the associated message, use an overload that uses embedded content.</exception>
        /// <exception cref="CryptographicException">
        ///   <para>
        ///     <see cref="CoseMessage.ProtectedHeaders"/> does not have a value for the <see cref="CoseHeaderLabel.Algorithm"/> header.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was incorrectly formatted.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was not one of the values supported by this implementation.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header doesn't match with the algorithms supported by the specified <paramref name="key"/>.
        ///   </para>
        /// </exception>
        /// <seealso cref="VerifyEmbedded(AsymmetricAlgorithm, ReadOnlySpan{byte})"/>
        /// <seealso cref="CoseMessage.Content"/>
        public bool VerifyDetached(AsymmetricAlgorithm key, ReadOnlySpan<byte> detachedContent, ReadOnlySpan<byte> associatedData = default)
        {
            if (key is null)
            {
                throw new ArgumentNullException(nameof(key));
            }
 
            if (!Message.IsDetached)
            {
                throw new InvalidOperationException(SR.ContentWasEmbedded);
            }
 
            return VerifyCore(key, detachedContent, null, associatedData, CoseHelpers.GetKeyType(key));
        }
 
        /// <summary>
        /// Verifies that the signature is valid for the message's content using the specified key.
        /// </summary>
        /// <param name="key">The private key used to sign the content.</param>
        /// <param name="detachedContent">The content that was previously signed.</param>
        /// <param name="associatedData">The extra data associated with the signature, which must match the value provided during signing.</param>
        /// <returns><see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="key"/> or <paramref name="detachedContent"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException">
        ///   <para>
        ///     <paramref name="key"/> is of an unsupported type.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     <paramref name="detachedContent"/> does not support reading or seeking.
        ///   </para>
        /// </exception>
        /// <exception cref="InvalidOperationException">The content is embedded on the associated message, use an overload that uses embedded content.</exception>
        /// <exception cref="CryptographicException">
        ///   <para>
        ///     <see cref="CoseMessage.ProtectedHeaders"/> does not have a value for the <see cref="CoseHeaderLabel.Algorithm"/> header.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was incorrectly formatted.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was not one of the values supported by this implementation.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header doesn't match with the algorithms supported by the specified <paramref name="key"/>.
        ///   </para>
        /// </exception>
        /// <seealso cref="VerifyDetachedAsync(AsymmetricAlgorithm, Stream, ReadOnlyMemory{byte}, CancellationToken)"/>
        /// <seealso cref="CoseMessage.Content"/>
        public bool VerifyDetached(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlySpan<byte> associatedData = default)
        {
            if (key is null)
            {
                throw new ArgumentNullException(nameof(key));
            }
 
            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));
            }
 
            if (!Message.IsDetached)
            {
                throw new InvalidOperationException(SR.ContentWasEmbedded);
            }
 
            return VerifyCore(key, default, detachedContent, associatedData, CoseHelpers.GetKeyType(key));
        }
 
        /// <summary>
        /// Asynchronously verifies that the signature is valid for the message's content using the specified key.
        /// </summary>
        /// <param name="key">The private key used to sign the content.</param>
        /// <param name="detachedContent">The content that was previously signed.</param>
        /// <param name="associatedData">The extra data associated with the signature, which must match the value provided during signing.</param>
        /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
        /// <returns>A task whose <see cref="Task{TResult}"/> property is <see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="key"/> or <paramref name="detachedContent"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException">
        ///   <para>
        ///     <paramref name="key"/> is of an unsupported type.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     <paramref name="detachedContent"/> does not support reading or seeking.
        ///   </para>
        /// </exception>
        /// <exception cref="InvalidOperationException">The content is embedded on the associated message, use an overload that uses embedded content.</exception>
        /// <exception cref="CryptographicException">
        ///   <para>
        ///     <see cref="CoseMessage.ProtectedHeaders"/> does not have a value for the <see cref="CoseHeaderLabel.Algorithm"/> header.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was incorrectly formatted.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header was not one of the values supported by this implementation.
        ///   </para>
        ///   <para>-or-</para>
        ///   <para>
        ///     The algorithm protected header doesn't match with the algorithms supported by the specified <paramref name="key"/>.
        ///   </para>
        /// </exception>
        /// <seealso cref="VerifyDetached(AsymmetricAlgorithm, Stream, ReadOnlySpan{byte})"/>
        /// <seealso cref="CoseMessage.Content"/>
        public Task<bool> VerifyDetachedAsync(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlyMemory<byte> associatedData = default, CancellationToken cancellationToken = default)
        {
            if (key is null)
            {
                throw new ArgumentNullException(nameof(key));
            }
            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));
            }
 
            if (!Message.IsDetached)
            {
                throw new InvalidOperationException(SR.ContentWasEmbedded);
            }
 
            return VerifyAsyncCore(key, detachedContent, associatedData, CoseHelpers.GetKeyType(key), cancellationToken);
        }
 
        private async Task<bool> VerifyAsyncCore(AsymmetricAlgorithm key, Stream content, ReadOnlyMemory<byte> associatedData, KeyType keyType, CancellationToken cancellationToken)
        {
            ReadOnlyMemory<byte> encodedAlg = CoseHelpers.GetCoseAlgorithmFromProtectedHeaders(ProtectedHeaders);
 
            int? nullableAlg = CoseHelpers.DecodeCoseAlgorithmHeader(encodedAlg);
            if (nullableAlg == null)
            {
                throw new CryptographicException(SR.Sign1VerifyAlgHeaderWasIncorrect);
            }
 
            HashAlgorithmName hashAlgorithm = CoseHelpers.GetHashAlgorithmFromCoseAlgorithmAndKeyType(nullableAlg.Value, keyType, out RSASignaturePadding? padding);
 
            using (IncrementalHash hasher = IncrementalHash.CreateHash(hashAlgorithm))
            {
                int bufferLength = CoseMessage.ComputeToBeSignedEncodedSize(
                    SigStructureContext.Signature,
                    Message.RawProtectedHeaders.Length,
                    _encodedSignProtectedHeaders.Length,
                    associatedData.Length,
                    contentLength: 0);
                byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferLength);
 
                try
                {
                    await CoseMessage.AppendToBeSignedAsync(buffer, hasher, SigStructureContext.Signature, Message.RawProtectedHeaders, _encodedSignProtectedHeaders, associatedData, content, cancellationToken).ConfigureAwait(false);
                    return VerifyHash(key, hasher, hashAlgorithm, keyType, padding);
                }
                finally
                {
                    ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
                }
            }
        }
 
        private bool VerifyCore(AsymmetricAlgorithm key, ReadOnlySpan<byte> contentBytes, Stream? contentStream, ReadOnlySpan<byte> associatedData, KeyType keyType)
        {
            ReadOnlyMemory<byte> encodedAlg = CoseHelpers.GetCoseAlgorithmFromProtectedHeaders(ProtectedHeaders);
            int? nullableAlg = CoseHelpers.DecodeCoseAlgorithmHeader(encodedAlg);
            if (nullableAlg == null)
            {
                throw new CryptographicException(SR.Sign1VerifyAlgHeaderWasIncorrect);
            }
 
            HashAlgorithmName hashAlgorithm = CoseHelpers.GetHashAlgorithmFromCoseAlgorithmAndKeyType(nullableAlg.Value, keyType, out RSASignaturePadding? padding);
            using (IncrementalHash hasher = IncrementalHash.CreateHash(hashAlgorithm))
            {
                int bufferLength = CoseMessage.ComputeToBeSignedEncodedSize(
                    SigStructureContext.Signature,
                    Message.RawProtectedHeaders.Length,
                    _encodedSignProtectedHeaders.Length,
                    associatedData.Length,
                    contentLength: 0);
                byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferLength);
 
                try
                {
                    CoseMessage.AppendToBeSigned(buffer, hasher, SigStructureContext.Signature, Message.RawProtectedHeaders.Span, _encodedSignProtectedHeaders, associatedData, contentBytes, contentStream);
                    return VerifyHash(key, hasher, hashAlgorithm, keyType, padding);
                }
                finally
                {
                    ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
                }
            }
        }
 
        private bool VerifyHash(AsymmetricAlgorithm key, IncrementalHash hasher, HashAlgorithmName hashAlgorithm, KeyType keyType, RSASignaturePadding? padding)
        {
#if NET
            Debug.Assert(hasher.HashLengthInBytes <= 512 / 8); // largest hash we can get (SHA512).
            Span<byte> hash = stackalloc byte[hasher.HashLengthInBytes];
            hasher.GetHashAndReset(hash);
#else
            byte[] hash = hasher.GetHashAndReset();
#endif
            if (keyType == KeyType.ECDsa)
            {
                var ecdsa = (ECDsa)key;
                return ecdsa.VerifyHash(hash, _signature);
            }
            else
            {
                Debug.Assert(keyType == KeyType.RSA);
                Debug.Assert(padding != null);
                var rsa = (RSA)key;
                return rsa.VerifyHash(hash, _signature, hashAlgorithm, padding);
            }
        }
    }
}