-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[API Proposal]: Add APIs for COSE signing #69896
Comments
Tagging subscribers to this area: @dotnet/area-system-security, @vcsjones Issue DetailsBackground and motivationIn 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). See also #62600. API Proposalnamespace System.Security.Cryptography.Cose
{
public abstract partial 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 abstract byte[] Encode();
}
public sealed partial class CoseSign1Message : CoseMessage
{
internal CoseSign1Message() { }
[UnsupportedOSPlatformAttribute("browser")]
public static byte[] Sign(byte[] content, SAsymmetricAlgorithm key, SHashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public static byte[] Sign(Stream detachedContent, SAsymmetricAlgorithm key, SHashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public static byte[] Sign(ReadOnlySpan<byte> content, SAsymmetricAlgorithm key, SHashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public static Threading.Tasks.Task<byte[]> SignAsync(Stream detachedContent, SAsymmetricAlgorithm key, SHashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, Threading.CancellationToken cancellationToken = default(Threading.CancellationToken)) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public static bool TrySign(ReadOnlySpan<byte> content, Span<byte> destination, SAsymmetricAlgorithm key, SHashAlgorithmName hashAlgorithm, out int bytesWritten, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public bool Verify(SAsymmetricAlgorithm key) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public bool Verify(SAsymmetricAlgorithm key, byte[] content) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public bool Verify(SAsymmetricAlgorithm key, Stream detachedContent) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public bool Verify(SAsymmetricAlgorithm key, ReadOnlySpan<byte> content) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public Threading.Tasks.Task<bool> VerifyAsync(SAsymmetricAlgorithm key, Stream detachedContent, Threading.CancellationToken cancellationToken = default(Threading.CancellationToken)) { throw null; }
public override byte[] Encode() { throw null; }
}
public sealed partial class CoseMultiSignMessage : CoseMessage
{
internal CoseMultiSignMessage() { }
[UnsupportedOSPlatformAttribute("browser")]
public static byte[] Sign(byte[] content, CoseSignatureBuilder signatureBuilder, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public static byte[] Sign(Stream detachedContent, CoseSignatureBuilder signatureBuilder, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public static byte[] Sign(ReadOnlySpan<byte> content, CoseSignatureBuilder signatureBuilder, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public static Threading.Tasks.Task<byte[]> SignAsync(Stream detachedContent, CoseSignatureBuilder signatureBuilder, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, Threading.CancellationToken cancellationToken = default(Threading.CancellationToken)) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public static bool TrySign(ReadOnlySpan<byte> content, CoseSignatureBuilder signatureBuilder, SHashAlgorithmName hashAlgorithm, out int bytesWritten, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, bool isDetached = false) { throw null; }
public ObjectModel.ReadOnlyCollection<CoseSignature> Signatures { get; }
public void AddSignature(CoseSignatureBuilder signatureBuilder) { throw null; }
public void RemoveSignature(int index) { throw null; }
public override byte[] Encode() { throw null; }
}
public sealed class CoseSignature
{
internal CoseSignature() { }
public CoseHeaderMap ProtectedHeaders { get; }
public CoseHeaderMap UnprotectedHeaders { get; }
[UnsupportedOSPlatform("browser")]
public bool Verify(AsymmetricAlgorithm key) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public bool Verify(AsymmetricAlgorithm key, byte[] content) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public bool Verify(AsymmetricAlgorithm key, Stream detachedContent) { throw null; }
[UnsupportedOSPlatformAttribute("browser")]
public bool Verify(AsymmetricAlgorithm key, ReadOnlySpan<byte> content) { throw null; }
}
public sealed class CoseSignatureBuilder
{
public CoseSignatureBuilder(AsymmetricAlgorithm key, HashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null) { throw null; }
public AsymmetricAlgorithm Key { get; set; }
public HashAlgorithmName HashAlgorithm { get; set; }
public CoseHeaderMap? ProtectedHeaders { get; set; }
public CoseHeaderMap? UnprotectedHeaders { get; set; }
}
public sealed partial class CoseHeaderMap : IEnumerable<(CoseHeaderLabel Label, ReadOnlyMemory<byte> EncodedValue)>, IEnumerable
{
public CoseHeaderMap() { }
public bool IsReadOnly { get { throw null; } } // true for decoded protected headers; otherwise, false.
public ReadOnlyMemory<byte> GetEncodedValue(CoseHeaderLabel label) { throw null; }
public ReadOnlySpan<byte> GetValueAsBytes(CoseHeaderLabel label) { throw null; }
public int GetValueAsInt32(CoseHeaderLabel label) { throw null; }
public string GetValueAsString(CoseHeaderLabel label) { throw null; }
public bool TryGetEncodedValue(CoseHeaderLabel label, out ReadOnlyMemory<byte> encodedValue) { throw null; }
public void SetEncodedValue(CoseHeaderLabel label, ReadOnlySpan<byte> encodedValue) { }
public void SetValue(CoseHeaderLabel label, int value) { }
public void SetValue(CoseHeaderLabel label, ReadOnlySpan<byte> value) { }
public void SetValue(CoseHeaderLabel label, string value) { }
public void Remove(CoseHeaderLabel label) { }
public CoseHeaderMap.Enumerator GetEnumerator() { throw null; }
IEnumerator IEnumerable.GetEnumerator() { throw null; }
IEnumerator<(CoseHeaderLabel Label, ReadOnlyMemory<byte> EncodedValue)> IEnumerable<(CoseHeaderLabel Label, ReadOnlyMemory<Byte> EncodedValue)>.GetEnumerator() { throw null; }
public partial struct Enumerator : IEnumerator<(CoseHeaderLabel Label, ReadOnlyMemory<byte> EncodedValue)>, IEnumerator, IDisposable
{
public readonly (CoseHeaderLabel Label, ReadOnlyMemory<byte> EncodedValue) Current { get { throw null; } }
object IEnumerator.Current { get { throw null; } }
public void Dispose() { }
public bool MoveNext() { throw null; }
public void Reset() { }
}
}
public readonly partial 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
public static CoseHeaderLabel Algorithm { get { throw null; } }
public static CoseHeaderLabel ContentType { get { throw null; } }
public static CoseHeaderLabel CounterSignature { get { throw null; } }
public static CoseHeaderLabel Critical { get { throw null; } }
public static CoseHeaderLabel IV { get { throw null; } }
public static CoseHeaderLabel KeyIdentifier { get { throw null; } }
public static CoseHeaderLabel PartialIV { 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; }
}
} API UsageDecoding and verifying messagesbool 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.Verify(key);
}
else
{
Debug.Assert(signedContent != null);
return msg.Verify(key, signedContent);
}
} bool DecodeAndVerify_MultiSign(byte[] encodedMsg, AsymmetricAlgorithm key, byte[]? signedContent)
{
CoseSign1Message msg = CoseMessage.DecodeMultiSign(encodedMsg);
ReadOnlyCollection<CoseSignature> signatures = msg.Sginatures;
foreach (CoseSignature s in signatures)
{
bool verified = msg.Content != null ? s.Verify(key) : s.Verify(key, signedContent);
if (verified)
{
// Consider a valid verification if at least one signature verifies.
return true;
}
}
return false;
} Signing and encoding messagesbyte[] Sign_WithSign1(byte[] content, AsymmetricAlgorithm key, HashAlgorithmName hash)
{
return CoseSign1Message.Sign(content, key, hash);
} byte[] Sign_WithMultiSign(byte[] content, AsymmetricAlgorithm key, HashAlgorithmName hash)
{
// COSE_Sign uses COSE_Signature for transporting signature, that's why we use CoseSignatureBuilder, which can also accept headers.
var signatureBuilder = new CoseSignatureBuilder(key, hash);
return CoseMultiSignMessage.Sign(content, signatureBuilder);
} Append unprotected headers or signatures to already signed messages.byte[] AddUnprotectedHeader(byte[] encodedMsg)
{
CoseSign1Message msg = CoseMessage.DecoseSign1(encodedMsg);
msg.UnprotectedHeaders.SetValue(CoseHeaderLabel.ContentType, "application/json");
// re-encode
msg.Encode();
} byte[] AddSignature(byte[] encodedMsg, Asymmetricalgorithm key, HashAlgorithmName hash)
{
CoseMultiSignMessage msg = CoseMessage.DecoseMultiSign(encodedMsg);
msg.AddSignature(new CoseSignatureBuilder(key, hash));
return msg.Encode();
} Use custom headersbyte[] 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.SetValue(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.SetEncodedValue(CoseHeaderLabel.Critical, writer.Encode());
protectedHeaders.SetValue(new CoseHeaderLabel("42"), "this is my custom critical header.");
return CoseSign1Message.Sign(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.SignAsync(contentStream, key, hash, ct);
} Alternative DesignsNo response RisksThe 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 in the future if .NET chooses to support those formats as well.
|
Some thoughts:
|
Some observations:
|
@vcsjones, yes, I will include them in the proposal.
@bartonjs, thoughts?
@letmaik, I thought about it when I was implementing those overloads, it seems that wanting embedded content in Stream scenarios defies the whole purpose of using a Stream because it will load in memory the whole content, which is what motivated the overload to exists, don't you think? |
Nits: The usage of |
Somehow RFC 8812 shows up for me as a previously visited page, but the contents were not in my brain. I had internalized that COSE was RSA-PSS only. That means Sign1 needs either an optional parameter with AsymmetricAlgorithm (and RSA throws if the padding type wasn't specified) or change it to be overloaded by algorithm type and the RSA overload has a mandatory RSASignaturePadding parameter.
Since there's no
Yeah, the public members being Splitting verify would probably become more relevant with AAD. (Ooh, it doesn't make a distinction between "no AAD" and "empty AAD", so we can use default spans) public bool Verify(AsymmetricAlgorithm key, ReadOnlySpan<byte> someNameForAad = default);
public bool VerifyDetached(AsymmetricAlgorithm key, ReadOnlySpan<byte> detachedContent, ReadOnlySpan<byte> someNameForAad = default);
public bool VerifyDetached(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlySpan<byte> someNameForAad = default);
public bool VerifyDetachedAsync(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlyMemory<byte> someNameForAad = default, CancellationToken cancellationToken = default);
// plus byte[] overloads |
I don't have experience with Stream, but I think you're saying that code using Stream always assumes potentially large data. Is that really true? Separate |
That's similar to what we did with |
This implementation, currently, only produces tagged versions.
We did talk about that early on. It's probably not terrible, but does come with a bit of an annoyance. It'd be something like |
Is tagged the common usage? It's what we're doing in the application I'm working on, but I don't have visibility into the broader COSE-using ecosystem.
I think once upon a time the hope was COSE could leave behind the legacy of PKCS#1v1.5 padding but it just has a way of worming its way back in. Indeed, I think COSE hoped to leave RSA behind entirely, but then RFC8230 came along and specified PSS for it.
I still advocate for the Verify variants to throw if they're called in the wrong state (providing content when embedded and vice-versa).
It's grating but hard to avoid when you're dealing with variant types. It's either that, or reference the superclass and have a bunch of "if (is subtype A) { ... } else if (is subtype B) { ... }" logic. Right now the proposal assumes the caller knows which to expect ahead of time, and if they don't, they'll have to do the peeking themselves. |
@ everyone, please take another look at the API proposal, I've updated it based on the feedback received.
We renamed CoseSignatureBuilder to CoseSigner, as the type is not really building anything, it is signing with the provided inputs.
We added
We removed the
We think that CoseHeaderMap doesn't need to be a dictionary, if you need a
Yes, we will support it; all Sign operations will contain a optional param.
Yes, we will support
We still don't have a case where it makes sense to produce an untagged message. For decoding, we accept both, tagged and untagged variants.
For untagged messages, you can't really distinguish between messages, because the COSE structures are ambiguous, so it depends on the caller on how they choose to interpret the message. |
I suggest changing The handling of the two header buckets I find confusing because of the following:
Is my interpretation correct? |
Does this need to be abstract? Can't it just be a regular method that basically does: public int Encode(Span<byte> destination) {
if (!TryEncode(destination, out int written)) {
throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination));
}
return written;
} I think, ideally, the APIs for encode would look like: public byte[] Encode();
public int Encode(Span<byte> destination);
public bool TryEncode(Span<byte> destination, out int bytesWritten);
public abstract int ComputeEncodedSize();
protected abstract TryEncodeCore(Span<byte> destination, out int bytesWritten); I don't know how feasible it is to up-front compute the length of a COSE message. But if we had
Out of curiosity, how does this work?
This does not have a public sealed class CoseSigner { public AsymmetricAlgorithm Key { get; set; } public HashAlgorithmName HashAlgorithm { get; set; } public CoseHeaderMap? ProtectedHeaders { get; set; } public CoseHeaderMap? UnprotectedHeaders { get; set; } public RSASignaturePadding? RSASignaturePadding{ get; set; } } What's the thought behind all of these having a Otherwise you end up having to do a lot of validation again in the |
For RSASignaturePadding, at least, the settableness came from CmsSigner. In SignedCms we just ignore the property if the key wasn't RSA (99% confident on that statement). And there, we predated PSS, so it defaults to PKCS1 if the padding wasn't specified... whereas here it's required. The scenario for the set on RSASignaturePadding is... slightly convoluted... but exists.
If we broke it up like public partial class CoseSigner
{
public CoseSigner(RSA key, RSASignaturePadding signaturePadding, ...);
public CoseSigner(ECDsa key, ...);
public CoseSigner(EdDSA key, ...);
} then this library wouldn't be able to pass through an EdDSA key without split compiling. Instead, they can do CoseSigner signer = new CoseSigner(key, ...)
{
RSASignaturePadding = RSASignaturePadding.IHaveOpinionsHere,
}; and that works until/unless EdDSA (or whatever) also needs a sidecar parameter. But, I agree, the rest don't seem to have justification for having setters. |
@kevinmkane yes it is correct, we removed CoseHeaderMaps from
@vcsjones yes, [Try]Encode methods can be non-abstract. Line 99 in 89cee6e
And
Reference equality should suffice. Users are not able to instantiate the type themselves, so it shouldn't be an issue. I can also see that CmsSigner doesn't implement IEquatable either |
Yeah. Really the question was "how do we fix the RSASignaturePadding problem for Sign1 now that we fixed it for MultiSign?", and that's what I came up with as a solution. Here's the logic. If you don't agree with it after, we'd be happy to entertain suggestions of how to cleanly solve it another way (I can't figure out how to reword that with no sarcasm tone implied, but there's only sincerity).
The next best alternative was to keep the document headers and either
This slightly wonky approach seemed the least awkward... but feels somewhat intuitive if you design Sign before Sign1. (The problem is we all came to it from the other way around) |
I... suppose. When we added it to
I'm not sure I follow this:
I think we can leave the constructors as-is. I think what I am suggesting is, if a library author is super opinionated... using AsymmetricAlgorithm key = GetKey();
CoseSigner signer = key switch {
RSA rsa => new CoseSigner(rsa, RSASignaturePadding.Pss, ...), // Opinions!
AsymmetricAlgorithm alg => new CoseSigner(alg, ...), // Pass though
null => throw new ArgumentNullException(nameof(key)); // Agh!
}; |
@kevinmkane the RFC 8152 also calls it external_add, where aad stands for Additional Associated Data. We have implemented AEAD (Authenticated Encryption with Associated Data) before and it has been called associatedData in other .NET APIs (see |
That's.... fair. I guess we can get away with none of the properties being settable for now. |
It's helpful, I guess, in that |
In an attempt to please @bartonjs, I will borrow from the Framework Design Guidelines:
So, hopefully I am interpreting that correctly, but we also did it for the symmetric crypto one-shots. |
To clarify the
|
For As @vcsjones just beat me to quoting 😄. |
In this case, since there are no parameters other than the destination span, I don't know if the template method is useful. If the non-virtual Try calls ComputeEncodedSize as a precondition, then the Core method would be the int-returning Encode, since it would never have a public byte[] Encode()
{
byte[] dest = new byte[ComputeEncodedSize()];
int written = Encode(dest);
Debug.Assert(written == dest.Length);
return dest;
}
public int Encode(Span<byte> destination)
{
if (TryEncode(destination, out int written))
{
return written;
}
throw new ArgumentException(SR.WhateverSpanTooSmallIs);
}
public abstract bool TryEncode(Span<byte> destination, out int bytesWritten); does seem to be the full complement. |
@bartonjs I can appreciate the logic, and I'm not advocating for a change here. I think this is a problem that can be solved by good API documentation for the multi-sign signing APIs. You could rename the parameters to
@jozkee Now that I look closely, RFC 8152 erroneously calls AEAD "Authenticated Encryption with Authenticated Data", and in section 5.3 defines AAD as "Additional Authenticated Data". So this terminology looks like it may have been a mistake. If existing .NET APIs already follow the "associated" naming convention, then I agree it's sensible to stick to that. |
Sadly, "A" means either Associated or Additional depending on which abbreviation you base it on. https://csrc.nist.gov/glossary/term/aead
https://csrc.nist.gov/glossary/term/aad
|
That's a fair point. There is nothing to validate.
Assuming then you meant for that one to be public? |
And abstract. Do what I mean, not what I say 😁 |
Yeah, I agree it's ok to stick with existing conventions. My (weak) preference is that we don't choose the name additionalData. The term "additional" here could imply that the payload is carrying the data in some form, which is not the case. "Associated" doesn't carry this connotation. At least to me. :) In the end I imagine it's not going to matter all that much. The docs will point to https://cose-wg.github.io/cose-spec/#rfc.section.4.3 or whatever other section is relevant, and the caller will say "oh, I get it!" |
This is what I captured from today's feedback.
Replace:
With:
If there's no more feedback, I will label this as |
I don't know that that is the right name for a public API. |
There are not many existing .NET APIs using GetEncodedLength and GetEncodedSize (only one result for each, both in the crypto space). So I guess either one is fine. For ComputeEncodedSize there were no results. |
In this case, |
|
The allocations could be avoided by using a backing field that only gets allocated if a caller calls the property getter when the object hadn't been originally created with the map present.
Since As a result, in order to modify the unprotected headers, either the application always has to It just seems to lead to clunky application code because, even though there's no setter, the property allows its contents to be modified. When there was a setter, if it was If |
namespace System.Security.Cryptography.Cose;
public abstract class CoseMessage
{
public ReadOnlyMemory<byte>? Content { get; }
public CoseHeaderMap ProtectedHeaders { get; }
public CoseHeaderMap UnprotectedHeaders { get; }
public static CoseSign1Message DecodeSign1(byte[] cborPayload);
public static CoseSign1Message DecodeSign1(ReadOnlySpan<byte> cborPayload);
public static CoseMultiSignMessage DecodeMultiSign(byte[] cborPayload);
public static CoseMultiSignMessage DecodeMultiSign(ReadOnlySpan<byte> cborPayload);
public byte[] Encode();
public int Encode(Span<byte> destination);
public abstract bool TryEncode(Span<byte> destination, out int bytesWritten);
public abstract int GetEncodedLength();
}
public sealed class CoseSign1Message : CoseMessage
{
[UnsupportedOSPlatform("browser")]
public static byte[] SignDetached(byte[] detachedContent, CoseSigner signer, byte[]? associatedData = null);
[UnsupportedOSPlatform("browser")]
public static byte[] SignDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public static byte[] SignDetached(Stream detachedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public static Task<byte[]> SignDetachedAsync(Stream detachedContent, CoseSigner signer, ReadOnlyMemory<byte> associatedData = default, CancellationToken cancellationToken = default(CancellationToken));
[UnsupportedOSPlatform("browser")]
public static byte[] SignEmbedded(byte[] embeddedContent, CoseSigner signer, byte[]? associatedData = null);
[UnsupportedOSPlatform("browser")]
public static byte[] SignEmbedded(ReadOnlySpan<byte> embeddedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public static bool TrySignEmbedded(ReadOnlySpan<byte> embeddedContent, Span<byte> destination, CoseSigner signer, out int bytesWritten, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public static bool TrySignDetached(ReadOnlySpan<byte> detachedContent, Span<byte> destination, CoseSigner signer, out int bytesWritten, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public bool VerifyDetached(AsymmetricAlgorithm key, byte[] detachedContent, byte[]? associatedData = default);
[UnsupportedOSPlatform("browser")]
public bool VerifyDetached(AsymmetricAlgorithm key, ReadOnlySpan<byte> detachedContent, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public bool VerifyDetached(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public Task<bool> VerifyDetachedAsync(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlyMemory<byte> associatedData, CancellationToken cancellationToken = default(CancellationToken));
[UnsupportedOSPlatform("browser")]
public bool VerifyEmbedded(AsymmetricAlgorithm key, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public bool VerifyEmbedded(AsymmetricAlgorithm key, byte[]? associatedData = null);
public override bool TryEncode(Span<byte> destination, out int bytesWritten);
public override int GetEncodedLength();
}
public sealed class CoseMultiSignMessage : CoseMessage
{
[UnsupportedOSPlatform("browser")]
public static byte[] SignDetached(byte[] detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, byte[]? associatedData = null);
[UnsupportedOSPlatform("browser")]
public static byte[] SignDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public static byte[] SignDetached(Stream detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public static Task<byte[]> SignDetachedAsync(Stream detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlyMemory<byte> associatedData = default, CancellationToken cancellationToken = default(CancellationToken));
[UnsupportedOSPlatform("browser")]
public static byte[] SignEmbedded(byte[] embeddedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, byte[]? associatedData = null);
[UnsupportedOSPlatform("browser")]
public static byte[] SignEmbedded(ReadOnlySpan<byte> embeddedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public static bool TrySignEmbedded(ReadOnlySpan<byte> embeddedContent, CoseSigner signer, out int bytesWritten, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public static bool TrySignDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, out int bytesWritten, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default);
public ReadOnlyCollection<CoseSignature> Signatures { get; }
[UnsupportedOSPlatform("browser")]
public void AddSignature(CoseSigner signer, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public void AddSignature(CoseSigner signer, byte[]? associatedData = null);
public void RemoveSignature(CoseSignature signature);
public void RemoveSignature(int index);
public override bool TryEncode(Span<byte> destination, out int bytesWritten);
public override int GetEncodedLength();
}
public sealed class CoseSignature
{
public CoseHeaderMap ProtectedHeaders { get; }
public CoseHeaderMap UnprotectedHeaders { get; }
[UnsupportedOSPlatform("browser")]
public bool VerifyEmbedded(AsymmetricAlgorithm key, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public bool VerifyDetached(AsymmetricAlgorithm key, byte[] detachedContent, byte[]? associatedData = null);
[UnsupportedOSPlatform("browser")]
public bool VerifyDetached(AsymmetricAlgorithm key, ReadOnlySpan<byte> detachedContent, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public bool VerifyDetached(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlySpan<byte> associatedData = default);
[UnsupportedOSPlatform("browser")]
public Task<bool> VerifyDetachedAsync(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlyMemory<byte> associatedData = default);
}
public sealed class CoseSigner
{
public CoseSigner(AsymmetricAlgorithm key, HashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null);
public CoseSigner(RSA key, RSASignaturePadding signaturePadding, HashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null);
public AsymmetricAlgorithm Key { get; }
public HashAlgorithmName HashAlgorithm { get; }
public CoseHeaderMap? ProtectedHeaders { get; }
public CoseHeaderMap? UnprotectedHeaders { get; }
public RSASignaturePadding? RSASignaturePadding { get; }
}
public sealed class CoseHeaderMap : IEnumerable<(CoseHeaderLabel Label, ReadOnlyMemory<byte> EncodedValue)>, IEnumerable
{
public CoseHeaderMap();
public bool IsReadOnly { get; }
public ReadOnlyMemory<byte> GetEncodedValue(CoseHeaderLabel label);
public ReadOnlySpan<byte> GetValueAsBytes(CoseHeaderLabel label);
public int GetValueAsInt32(CoseHeaderLabel label);
public string GetValueAsString(CoseHeaderLabel label);
public bool TryGetEncodedValue(CoseHeaderLabel label, out ReadOnlyMemory<byte> encodedValue);
public void SetEncodedValue(CoseHeaderLabel label, ReadOnlySpan<byte> encodedValue);
public void SetEncodedValue(CoseHeaderLabel label, byte[] encodedValue);
public void SetValue(CoseHeaderLabel label, int value);
public void SetValue(CoseHeaderLabel label, ReadOnlySpan<byte> value);
public void SetValue(CoseHeaderLabel label, string value);
public void Remove(CoseHeaderLabel label);
public CoseHeaderMap.Enumerator GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
IEnumerator<(CoseHeaderLabel Label, ReadOnlyMemory<byte> EncodedValue)> IEnumerable<(CoseHeaderLabel Label, ReadOnlyMemory<Byte> EncodedValue)>.GetEnumerator();
public struct Enumerator : IEnumerator<(CoseHeaderLabel Label, ReadOnlyMemory<byte> EncodedValue)>, IEnumerator, IDisposable
{
public readonly (CoseHeaderLabel Label, ReadOnlyMemory<byte> EncodedValue) Current { get; }
object IEnumerator.Current { get; }
public void Dispose();
public bool MoveNext();
public void Reset();
}
}
public readonly struct CoseHeaderLabel : IEquatable<CoseHeaderLabel>
{
public CoseHeaderLabel(int label);
public CoseHeaderLabel(string label);
public static CoseHeaderLabel Algorithm { get; }
public static CoseHeaderLabel ContentType { get; }
public static CoseHeaderLabel CounterSignature { get; }
public static CoseHeaderLabel Critical { get; }
public static CoseHeaderLabel IV { get; }
public static CoseHeaderLabel KeyIdentifier { get; }
public static CoseHeaderLabel PartialIV { get; }
public override bool Equals([NotNullWhenAttribute(true)] object? obj);
public bool Equals(CoseHeaderLabel other);
public override int GetHashCode();
public static bool operator ==(CoseHeaderLabel left, CoseHeaderLabel right);
public static bool operator !=(CoseHeaderLabel left, CoseHeaderLabel right);
} |
Allright, that makes sense, we can enable that scenario as you suggest. Just keep in mind that reusing CoseSigner and changing items in the CoseHeaderMaps on async scenarios may incur into race conditions. |
I would hope callers of async APIs in general know they shouldn't change an input to an async call while the task is still pending. If you think that might be too error-prone for callers, there's still the option of always making both returned header maps read-only, at the cost of the caller having to make a couple of allocations in order to accomplish the aforementioned scenario. Whatever results in an API that minimizes the chances of callers shooting themselves in the foot! |
Update: addressed feedback from API review.
|
[assembly: System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
namespace System.Security.Cryptography.Cose;
public abstract class CoseMessage
{
public ReadOnlyMemory<byte>? Content { get; }
public CoseHeaderMap ProtectedHeaders { get; }
public CoseHeaderMap UnprotectedHeaders { get; }
public static CoseSign1Message DecodeSign1(byte[] cborPayload);
public static CoseSign1Message DecodeSign1(ReadOnlySpan<byte> cborPayload);
public static CoseMultiSignMessage DecodeMultiSign(byte[] cborPayload);
public static CoseMultiSignMessage DecodeMultiSign(ReadOnlySpan<byte> cborPayload);
public byte[] Encode();
public int Encode(Span<byte> destination);
public abstract bool TryEncode(Span<byte> destination, out int bytesWritten);
public abstract int GetEncodedLength();
}
public sealed class CoseSign1Message : CoseMessage
{
public static byte[] SignDetached(byte[] detachedContent, CoseSigner signer, byte[]? associatedData = null);
public static byte[] SignDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default);
public static byte[] SignDetached(Stream detachedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default);
public static Task<byte[]> SignDetachedAsync(Stream detachedContent, CoseSigner signer, ReadOnlyMemory<byte> associatedData = default, CancellationToken cancellationToken = default(CancellationToken));
public static byte[] SignEmbedded(byte[] embeddedContent, CoseSigner signer, byte[]? associatedData = null);
public static byte[] SignEmbedded(ReadOnlySpan<byte> embeddedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default);
public static bool TrySignEmbedded(ReadOnlySpan<byte> embeddedContent, Span<byte> destination, CoseSigner signer, out int bytesWritten, ReadOnlySpan<byte> associatedData = default);
public static bool TrySignDetached(ReadOnlySpan<byte> detachedContent, Span<byte> destination, CoseSigner signer, out int bytesWritten, ReadOnlySpan<byte> associatedData = default);
public bool VerifyDetached(AsymmetricAlgorithm key, byte[] detachedContent, byte[]? associatedData = null);
public bool VerifyDetached(AsymmetricAlgorithm key, ReadOnlySpan<byte> detachedContent, ReadOnlySpan<byte> associatedData = default);
public bool VerifyDetached(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlySpan<byte> associatedData = default);
public Task<bool> VerifyDetachedAsync(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlyMemory<byte> associatedData = default, CancellationToken cancellationToken = default(CancellationToken));
public bool VerifyEmbedded(AsymmetricAlgorithm key, byte[]? associatedData = null);
public bool VerifyEmbedded(AsymmetricAlgorithm key, ReadOnlySpan<byte> associatedData = default);
public override bool TryEncode(Span<byte> destination, out int bytesWritten);
public override int GetEncodedLength();
}
public sealed class CoseMultiSignMessage : CoseMessage
{
public static byte[] SignDetached(byte[] detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, byte[]? associatedData = null);
public static byte[] SignDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default);
public static byte[] SignDetached(Stream detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default);
public static Task<byte[]> SignDetachedAsync(Stream detachedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlyMemory<byte> associatedData = default, CancellationToken cancellationToken = default(CancellationToken));
public static byte[] SignEmbedded(byte[] embeddedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, byte[]? associatedData = null);
public static byte[] SignEmbedded(ReadOnlySpan<byte> embeddedContent, CoseSigner signer, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default);
public static bool TrySignEmbedded(ReadOnlySpan<byte> embeddedContent, CoseSigner signer, out int bytesWritten, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default);
public static bool TrySignDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, out int bytesWritten, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null, ReadOnlySpan<byte> associatedData = default);
public ReadOnlyCollection<CoseSignature> Signatures { get; }
public void AddSignature(CoseSigner signer, byte[]? associatedData = null);
public void AddSignature(CoseSigner signer, ReadOnlySpan<byte> associatedData = default);
public void RemoveSignature(CoseSignature signature);
public void RemoveSignature(int index);
public override bool TryEncode(Span<byte> destination, out int bytesWritten);
public override int GetEncodedLength();
}
public sealed class CoseSignature
{
public CoseHeaderMap ProtectedHeaders { get; }
public CoseHeaderMap UnprotectedHeaders { get; }
public bool VerifyEmbedded(AsymmetricAlgorithm key, byte[]? associatedData = null);
public bool VerifyEmbedded(AsymmetricAlgorithm key, ReadOnlySpan<byte> associatedData = default);
public bool VerifyDetached(AsymmetricAlgorithm key, byte[] detachedContent, byte[]? associatedData = null);
public bool VerifyDetached(AsymmetricAlgorithm key, ReadOnlySpan<byte> detachedContent, ReadOnlySpan<byte> associatedData = default);
public bool VerifyDetached(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlySpan<byte> associatedData = default);
public Task<bool> VerifyDetachedAsync(AsymmetricAlgorithm key, Stream detachedContent, ReadOnlyMemory<byte> associatedData = default);
}
public sealed class CoseSigner
{
public CoseSigner(AsymmetricAlgorithm key, HashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = null);
public CoseSigner(RSA key, RSASignaturePadding signaturePadding, HashAlgorithmName hashAlgorithm, CoseHeaderMap? protectedHeaders = null, CoseHeaderMap? unprotectedHeaders = 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; set; }
public ICollection<CoseHeaderLabel> Keys { get; }
public ICollection<CoseHeaderValue> Values { get; }
public int Count { get; }
public bool IsReadOnly { get; }
public void Add(CoseHeaderLabel key, CoseHeaderValue value);
public void Add(KeyValuePair<CoseHeaderLabel, CoseHeaderValue> item);
public void Clear();
public bool Contains(KeyValuePair<CoseHeaderLabel, CoseHeaderValue> item);
public bool ContainsKey(CoseHeaderLabel key);
public void CopyTo(KeyValuePair<CoseHeaderLabel, CoseHeaderValue>[] array, int arrayIndex);
public IEnumerator<KeyValuePair<CoseHeaderLabel, CoseHeaderValue>> GetEnumerator();
public bool Remove(CoseHeaderLabel key);
public bool Remove(KeyValuePair<CoseHeaderLabel, CoseHeaderValue> item);
public bool TryGetValue(CoseHeaderLabel key, [MaybeNullWhen(false)] out CoseHeaderValue value);
IEnumerator IEnumerable.GetEnumerator();
public void Add(CoseHeaderLabel label, int value);
public void Add(CoseHeaderLabel label, string value);
public void Add(CoseHeaderLabel label, byte[] value);
public void Add(CoseHeaderLabel label, ReadOnlySpan<byte> value);
public int GetValueAsInt32(CoseHeaderLabel label);
public string GetValueAsString(CoseHeaderLabel label);
public byte[] GetValueAsBytes(CoseHeaderLabel label);
public int GetValueAsBytes(CoseHeaderLabel label, Span<byte> destination);
}
public readonly struct CoseHeaderLabel : IEquatable<CoseHeaderLabel>
{
public CoseHeaderLabel(int label);
public CoseHeaderLabel(string label);
public static CoseHeaderLabel Algorithm { get; }
public static CoseHeaderLabel ContentType { get; }
public static CoseHeaderLabel CounterSignature { get; }
public static CoseHeaderLabel CriticalHeaders { get; }
public static CoseHeaderLabel KeyIdentifier { get; }
public override bool Equals([NotNullWhenAttribute(true)] object? obj);
public bool Equals(CoseHeaderLabel other);
public override int GetHashCode();
public static bool operator ==(CoseHeaderLabel left, CoseHeaderLabel right);
public static bool operator !=(CoseHeaderLabel left, CoseHeaderLabel right);
}
public readonly struct CoseHeaderValue : IEquatable<CoseHeaderValue>
{
public readonly ReadOnlyMemory<byte> EncodedValue { get; }
public static CoseHeaderValue FromEncodedValue(byte[] encodedValue);
public static CoseHeaderValue FromEncodedValue(ReadOnlySpan<byte> encodedValue);
public static CoseHeaderValue FromInt32(int value);
public static CoseHeaderValue FromString(string value);
public static CoseHeaderValue FromBytes(byte[] value);
public static CoseHeaderValue FromBytes(ReadOnlySpan<byte> value);
public int GetValueAsInt32();
public string GetValueAsString();
public byte[] GetValueAsBytes();
public int GetValueAsBytes(Span<byte> destination);
public override bool Equals([NotNullWhenAttribute(true)] object? obj);
public bool Equals(CoseHeaderValue other);
public override int GetHashCode();
public static bool operator ==(CoseHeaderValue left, CoseHeaderValue right);
public static bool operator !=(CoseHeaderValue left, CoseHeaderValue right);
} |
We also approved the following change via email: public class CoseMultiSignMessage
{
- public void AddSignature(CoseSigner signer, byte[]? associatedData = null) { }
- public void AddSignature(CoseSigner signer, ReadOnlySpan<byte> associatedData) { }
+ public void AddSignatureForEmbedded(CoseSigner signer, byte[]? associatedData = null) { }
+ public void AddSignatureForEmbedded(CoseSigner signer, ReadOnlySpan<byte> associatedData) { }
+ public void AddSignatureForDetached(byte[] detachedContent, CoseSigner signer, byte[]? associatedData = null)
+ public void AddSignatureForDetached(ReadOnlySpan<byte> detachedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default)
+ public void AddSignatureForDetached(Stream detachedContent, CoseSigner signer, ReadOnlySpan<byte> associatedData = default)
+ public Task AddSignatureForDetachedAsync(Stream detachedContent, CoseSigner signer, ReadOnlyMemory<byte> associatedData = default, CancellationToken token = default)
}
AddSignature needed to expand to also support detached content scenarios, similar to what we have with the rest of operations in CoseSign1Message and CoseMultiSignMessage types. |
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
API Usage
Decoding and verifying messages
Signing and encoding messages
Append unprotected headers or signatures to already signed messages.
Use custom headers
Supply content via a stream in order to sign large contents.
Sign and Verify with external_aad (Externally supplied data).
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.
The text was updated successfully, but these errors were encountered: