-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Closed
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-System.Security
Milestone
Description
Background and motivation
In order to comply with the executive order on supply chain security, that includes inventory management (SCIM) and bill of materials (SBOM), .NET needs to implement APIs for signing with COSE (CBOR Object Signing and Encryption).
This proposal address above requirement by adding a new OOB package compatible with netstandard2.0 that contains APIs to work with COSE_Sign1 and COSE_Sign formats.
See also #62600.
API Proposal
[assembly: System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
namespace System.Security.Cryptography.Cose
{
public abstract class CoseMessage
{
internal CoseMessage() { }
public ReadOnlyMemory<byte>? Content { get { throw null; } } // "payload" for COSE_Sign* and COSE_Mac*, or "ciphertext" for COSE_Encrypt*
public CoseHeaderMap ProtectedHeaders { get { throw null; } }
public CoseHeaderMap UnprotectedHeaders { get { throw null; } }
public static CoseSign1Message DecodeSign1(byte[] cborPayload) { throw null; }
public static CoseSign1Message DecodeSign1(ReadOnlySpan<byte> cborPayload) { throw null; }
public static CoseMultiSignMessage DecodeMultiSign(byte[] cborPayload) { throw null; }
public static CoseMultiSignMessage DecodeMultiSign(ReadOnlySpan<byte> cborPayload) { throw null; }
public byte[] Encode() { throw null; }
public int Encode(Span<byte> destination) { throw null; }
public abstract bool TryEncode(Span<byte> destination, out int bytesWritten);
public abstract int GetEncodedLength();
}
public sealed class CoseSign1Message : CoseMessage
{
internal CoseSign1Message() { }
public static byte[] SignDetached(byte[] detachedContent, CoseSigner signer, byte[]? associatedData = null) { throw null; }
public static byte[] SignDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default) { throw null; }
public static byte[] SignDetached(Stream detachedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default) { throw null; }
public static Task<byte[]> SignDetachedAsync(Stream detachedContent, CoseSigner signer, ReadOnlyMemory<byte> associatedData = default, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
public static byte[] SignEmbedded(byte[] embeddedContent, CoseSigner signer, byte[]? associatedData = null) { throw null; }
public static byte[] SignEmbedded(ReadOnlySpan<byte> embeddedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default) { throw null; }
public static bool TrySignEmbedded(ReadOnlySpan<byte> embeddedContent, Span<byte> destination, CoseSigner signer, out int bytesWritten, ReadOnlySpan<byte> associatedData = default) { throw null; }
public static bool TrySignDetached(ReadOnlySpan<byte> detachedContent, Span<byte> destination, CoseSigner signer, out int bytesWritten, ReadOnlySpan<byte> associatedData = default) { throw null; }
public bool VerifyDetached(AsymmetricAlgorithm key, byte[] detachedContent, byte[]? associatedData = null) { throw null; }
public bool VerifyDetached(AsymmetricAlgorithm key, ReadOnlySpan<byte> detachedContent, ReadOnlySpan<byte> associatedData = default) { throw null; }
public bool VerifyDetached(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlySpan<byte> associatedData = default) { throw null; }
public Task<bool> VerifyDetachedAsync(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlyMemory<byte> associatedData = default, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
public bool VerifyEmbedded(AsymmetricAlgorithm key, byte[]? associatedData = null) { throw null; }
public bool VerifyEmbedded(AsymmetricAlgorithm key, ReadOnlySpan<byte> associatedData = default) { throw null; }
public override bool TryEncode(Span<byte> destination, out int bytesWritten) { throw null; }
public override int GetEncodedLength() { throw null; }
}
public sealed class CoseMultiSignMessage : CoseMessage
{
internal CoseMultiSignMessage() { }
public static byte[] SignDetached(byte[] detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, byte[]? associatedData = null) { throw null; }
public static byte[] SignDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default) { throw null; }
public static byte[] SignDetached(Stream detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default) { throw null; }
public static Task<byte[]> SignDetachedAsync(Stream detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlyMemory<byte> associatedData = default, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
public static byte[] SignEmbedded(byte[] embeddedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, byte[]? associatedData = null) { throw null; }
public static byte[] SignEmbedded(ReadOnlySpan<byte> embeddedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default) { throw null; }
public static bool TrySignEmbedded(ReadOnlySpan<byte> embeddedContent, CoseSigner signer, out int bytesWritten, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default) { throw null; }
public static bool TrySignDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, out int bytesWritten, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default) { throw null; }
public ReadOnlyCollection<CoseSignature> Signatures { get; }
public void AddSignature(CoseSigner signer, byte[]? associatedData = null) { throw null; }
public void AddSignature(CoseSigner signer, ReadOnlySpan<byte> associatedData = default) { throw null; }
public void RemoveSignature(CoseSignature signature) { throw null; }
public void RemoveSignature(int index) { throw null; }
public override bool TryEncode(Span<byte> destination, out int bytesWritten) { throw null; }
public override int GetEncodedLength() { throw null; }
}
public sealed class CoseSignature
{
internal CoseSignature() { }
public CoseHeaderMap ProtectedHeaders { get; }
public CoseHeaderMap UnprotectedHeaders { get; }
public bool VerifyEmbedded(AsymmetricAlgorithm key, byte[]? associatedData = null) { throw null; }
public bool VerifyEmbedded(AsymmetricAlgorithm key, ReadOnlySpan<byte> associatedData = default) { throw null; }
public bool VerifyDetached(AsymmetricAlgorithm key, byte[] detachedContent, byte[]? associatedData = null) { throw null; }
public bool VerifyDetached(AsymmetricAlgorithm key, ReadOnlySpan<byte> detachedContent, ReadOnlySpan<byte> associatedData = default) { throw null; }
public bool VerifyDetached(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlySpan<byte> associatedData = default) { throw null; }
public Task<bool> VerifyDetachedAsync(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlyMemory<byte> associatedData = default) { throw null; }
}
public sealed class CoseSigner
{
// Throw for RSA keys as the user needs to be aware of the RSASignaturePadding.
public CoseSigner(AsymmetricAlgorithm key, HashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null) { throw null; }
public CoseSigner(RSA key, RSASignaturePadding signaturePadding, HashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null) { throw null; }
public AsymmetricAlgorithm Key { get; }
public HashAlgorithmName HashAlgorithm { get; }
public CoseHeaderMap ProtectedHeaders { get; }
public CoseHeaderMap UnprotectedHeaders { get; }
public RSASignaturePadding? RSASignaturePadding{ get; }
}
public sealed partial class CoseHeaderMap : IDictionary<CoseHeaderLabel, CoseHeaderValue>
{
public CoseHeaderValue this[CoseHeaderLabel key] { get { throw null; } set { throw null; } }
public ICollection<CoseHeaderLabel> Keys { get { throw null; } }
public ICollection<CoseHeaderValue> Values { get { throw null; } }
public int Count { get { throw null; } }
public bool IsReadOnly { get { throw null; } }
public void Add(CoseHeaderLabel key, CoseHeaderValue value) { throw null; }
public void Add(KeyValuePair<CoseHeaderLabel, CoseHeaderValue> item) { throw null; }
public void Clear() { throw null; }
public bool Contains(KeyValuePair<CoseHeaderLabel, CoseHeaderValue> item) { throw null; }
public bool ContainsKey(CoseHeaderLabel key) { throw null; }
public void CopyTo(KeyValuePair<CoseHeaderLabel, CoseHeaderValue>[] array, int arrayIndex) { throw null; }
public IEnumerator<KeyValuePair<CoseHeaderLabel, CoseHeaderValue>> GetEnumerator() { throw null; }
public bool Remove(CoseHeaderLabel key) { throw null; }
public bool Remove(KeyValuePair<CoseHeaderLabel, CoseHeaderValue> item) { throw null; }
public bool TryGetValue(CoseHeaderLabel key, [MaybeNullWhen(false)] out CoseHeaderValue value) { throw null; }
IEnumerator IEnumerable.GetEnumerator() { throw null; }
// Accelerators
public void Add(CoseHeaderLabel label, int value) { throw null; }
public void Add(CoseHeaderLabel label, string value) { throw null; }
public void Add(CoseHeaderLabel label, byte[] value) { throw null; }
public void Add(CoseHeaderLabel label, ReadOnlySpan<byte> value) { throw null; }
public int GetValueAsInt32(CoseHeaderLabel label) { throw null; }
public string GetValueAsString(CoseHeaderLabel label) { throw null; }
public byte[] GetValueAsBytes(CoseHeaderLabel label) { throw null; }
public int GetValueAsBytes(CoseHeaderLabel label, Span<byte> destination) { throw null; }
}
public readonly struct CoseHeaderLabel : IEquatable<CoseHeaderLabel>
{
public CoseHeaderLabel(int label) { throw null; }
public CoseHeaderLabel(string label) { throw null; }
// Known headers from https://datatracker.ietf.org/doc/html/rfc8152#page-14 Table 2: Common Header Parameters, excluding IV and PartialIV used only for COSE_Encrypt.
public static CoseHeaderLabel Algorithm { get { throw null; } }
public static CoseHeaderLabel ContentType { get { throw null; } }
public static CoseHeaderLabel CounterSignature { get { throw null; } }
public static CoseHeaderLabel CriticalHeaders { get { throw null; } }
public static CoseHeaderLabel KeyIdentifier { get { throw null; } }
public override bool Equals([NotNullWhenAttribute(true)] object? obj) { throw null; }
public bool Equals(CoseHeaderLabel other) { throw null; }
public override int GetHashCode() { throw null; }
public static bool operator ==(CoseHeaderLabel left, CoseHeaderLabel right) { throw null; }
public static bool operator !=(CoseHeaderLabel left, CoseHeaderLabel right) { throw null; }
}
public readonly struct CoseHeaderValue : IEquatable<CoseHeaderValue>
{
public readonly ReadOnlyMemory<byte> EncodedValue { get; }
public static CoseHeaderValue FromEncodedValue(byte[] encodedValue) { throw null; }
public static CoseHeaderValue FromEncodedValue(ReadOnlySpan<byte> encodedValue) { throw null; }
public static CoseHeaderValue FromInt32(int value) { throw null; }
public static CoseHeaderValue FromString(string value) { throw null; }
public static CoseHeaderValue FromBytes(byte[] value) { throw null; }
public static CoseHeaderValue FromBytes(ReadOnlySpan<byte> value) { throw null; }
public int GetValueAsInt32() { throw null; }
public string GetValueAsString() { throw null; }
public byte[] GetValueAsBytes() { throw null; }
public int GetValueAsBytes(Span<byte> destination) { throw null; }
public override bool Equals([NotNullWhenAttribute(true)] object? obj) { throw null; }
public bool Equals(CoseHeaderValue other) { throw null; }
public override int GetHashCode() { throw null; }
public static bool operator ==(CoseHeaderValue left, CoseHeaderValue right) { throw null; }
public static bool operator !=(CoseHeaderValue left, CoseHeaderValue right) { throw null; }
}
}API Usage
Decoding and verifying messages
bool DecodeAndVerify_Sign1(byte[] encodedMsg, AsymmetricAlgorithm key, byte[]? signedContent)
{
CoseSign1Message msg = CoseMessage.DecodeSign1(encodedMsg);
// There are two scenarios for verification,
// embedded content (it was carried in the message and can be used for verification)
// and detached content (it was not part of the message and a CBOR nil terminator was placed instead).
if (msg.Content != null)
{
return msg.VerifyEmbedded(key);
}
else
{
Debug.Assert(signedContent != null);
return msg.VerifyDetached(key, signedContent);
}
}bool DecodeAndVerify_MultiSign(byte[] encodedMsg, AsymmetricAlgorithm key, byte[]? signedContent)
{
CoseMultiSignMessage msg = CoseMessage.DecodeMultiSign(encodedMsg);
ReadOnlyCollection<CoseSignature> signatures = msg.Signatures;
foreach (CoseSignature s in signatures)
{
bool verified = msg.Content != null ? s.VerifyEmbedded(key) : s.VerifyDetached(key, signedContent);
if (verified)
{
// Consider a valid verification if at least one signature verifies.
return true;
}
}
return false;
}Signing and encoding messages
byte[] Sign_WithSign1(byte[] content, AsymmetricAlgorithm key, HashAlgorithmName hash)
{
var signer = new CoseSigner(key, hash);
return CoseSign1Message.SignEmbedded(content, signer);
}byte[] Sign_WithMultiSign(byte[] content, AsymmetricAlgorithm key, HashAlgorithmName hash)
{
var signer = new CoseSigner(key, hash);
return CoseMultiSignMessage.SignEmbedded(content, signer);
}Append unprotected headers or signatures to already signed messages.
byte[] AddUnprotectedHeader(byte[] encodedMsg)
{
CoseSign1Message msg = CoseMessage.DecodeSign1(encodedMsg);
msg.UnprotectedHeaders.Add(CoseHeaderLabel.KeyIdentifier, Encoding.UTF8.GetBytes("11"));
// re-encode
return msg.Encode();
}byte[] AddSignature(byte[] encodedMsg, Asymmetricalgorithm key, HashAlgorithmName hash)
{
CoseMultiSignMessage msg = CoseMessage.DecodeMultiSign(encodedMsg);
msg.AddSignature(new CoseSigner(key, hash));
return msg.Encode();
}Use custom headers
byte[] Sign_WithCustomHeaders(byte[] content, AsymmetricAlgorithm key, HashAlgorithmName hash)
{
var protectedHeaders = new CoseHeaderMap();
var unprotectedHeaders = new coseHeaderMap();
// Key identifier values are hints about which key to use.
unprotectedHeaders.Add(CoseHeaderLabel.KeyIdentifier, Encoding.UTF8.GetBytes("11"));
// This parameter is used to indicate which protected header labels an application that is processing a message is required to understand.
var writer = new CborWriter();
writer.WriteStartArray(definiteLength: 1);
writer.WriteString("42");
writer.WriteEndArray();
protectedHeaders.Add(CoseHeaderLabel.CriticalHeaders, CoseHeaderValue.FromEncodedValue(writer.Encode()));
protectedHeaders.Add(new CoseHeaderLabel("42"), "this is my custom critical header.");
return CoseSign1Message.SignEmbedded(content, key, hash, protectedHeaders, unprotectedHeaders);
}Supply content via a stream in order to sign large contents.
Task<byte[]> Sign_WithStreamContent(AsymmetricAlgorithm key, HashAlgorithmName hash, Cancellationtoken ct)
{
Stream contentStream = File.Open("ubuntu-22.04-desktop-amd64.iso", FileMode.Open);
return CoseSign1Message.SignDetachedAsync(contentStream, key, hash, ct);
}Sign and Verify with external_aad (Externally supplied data).
byte[] Sign_WithSign1AndAssociatedData(byte[] content, AsymmetricAlgorithm key, HashAlgorithmName hash, byte[]? associatedData)
{
var signer = new CoseSigner(key, hash);
return CoseSign1Message.SignEmbedded(content, signer, associatedData);
}byte[] DecodeAndVerify_WithSign1AndAssociatedData(byte[] content, AsymmetricAlgorithm key, byte[]? signedContent, byte[]? associatedData)
{
CoseSign1Message msg = CoseMessage.DecodeSign1(encodedMsg);
if (msg.Content != null)
{
return msg.VerifyEmbedded(key, associatedData);
}
else
{
Debug.Assert(signedContent != null);
return msg.VerifyDetached(key, signedContent, associatedData);
}
}Alternative Designs
Risks
The COSE specification contains other formats, such as COSE_Encrypt and COSE_Mac, that were considered while writing this proposal but that haven't been fully explored, there is a small chance that we could have confilcting APIs if in the future .NET chooses to support those formats as well.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-System.Security