Closed jozkee closed 2 years ago
Tagging subscribers to this area: @dotnet/area-system-security, @vcsjones See info in area-owners.md if you want to be subscribed.
Author: | Jozkee |
---|---|
Assignees: | Jozkee |
Labels: | `api-suggestion`, `area-System.Security`, `blocking` |
Milestone: | 7.0.0 |
Some thoughts:
Encode
, should we also have a bool TryEncode(Span<byte> destination, out int bytesWritten)
and/or int Encode(Span<byte> destination)
?RSASignaturePadding
anywhere? IANA has both PSS and PKCS1 registered https://www.iana.org/assignments/cose/cose.xhtml. PKCS1 notably is useful in WebAuthN / CTAP scenarios and is registered by RFC 8812, and RFC 8230 registers PSS.Some observations:
Stream
implies detached payloads. Could that be confusing? Should the Stream
overload be like all other overloads?CoseHeaderMap
is an IEnumerable
. I think it would be useful to have a Contains(label)
method. Any reason this can't be an IDictionary
?AddUnprotectedHeader
should use a different label since putting the content type in the unprotected header could cause security problems. I suggest the key identifier example from Sign_WithCustomHeaders
.should we also have a bool TryEncode(Span
destination, out int bytesWritten) and/or int Encode(Span destination)?
@vcsjones, yes, I will include them in the proposal.
Do we need RSASignaturePadding anywhere?
@bartonjs, thoughts?
Should the Stream overload be like all other overloads?
@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?
What about support for using the _Tagged
versions of COSE_Sign1
and COSE_Sign
? Either a method to encode the structure including the enclosing tag, and/or a decoding method that determines the type of the message from the tag, if the caller knows to expect one, just to save the caller dealing with the CBOR of the tag directly.
What about content for the external_aad
member of the Sig_structure
, external data bound to the signature that isn't part of the payload?
Can you explain the thinking behind the CoseSignatureBuilder
? In particular, why the Sign
APIs for CoseMultiSignMessage
take CoseSignatureBuilder
but also a separate set of those same signing parameters?
+1 about the Sign
APIs being clearer about generating embedded versus detached payloads. I'd advocate for a different method name entirely for generating detached signatures, instead of an optional boolean parameter, to avoid confusion. Verify
knows which state it's in, at least, so it can throw if it gets called in the wrong circumstance (without a payload when it's a detached signature, or with a payload when the payload is embedded). In fact I would expect all of those overloads that take content to call that parameter detachedContent
and never just content
as there would be no need if the payload is embedded.
RemoveSignature
in CoseMultiSignMessage
takes an index. I'm assuming this is an index into the collection returned by Signatures
. Would an overload that takes a CoseSignature
also be appropriate?
Nits: The usage of msg.Encode()
is inconsistent. Sometimes it's called as a procedure and other times its return value is referenced. I think you want the latter, and certainly that is what I would advocate for, that the encoded version is returned by that method. You've also got CoseSign1Message
as the type of msg
in DecodeAndVerify_MultiSign
where you want CoseMultiSignMessage
.
Do we need RSASignaturePadding anywhere? ... RFC 8812 ...
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.
CoseSignatureBuilder
Since there's no Build(...)
method on this type it isn't a builder. A more appropriate name seems to be CoseSigner
. And, apparently, it also needs an RSASignaturePadding.
I'd advocate for a different method name entirely for generating detached signatures, instead of an optional boolean parameter, to avoid confusion.
Yeah, the public members being Sign
and SignDetached
might give better cluing as to what's going on. I'd go ahead and split Verify to Verify/VerifyDetached at the same time, to just make it clear what the pairings are.
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
Should the Stream overload be like all other overloads?
@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?
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 SignDetached
methods as suggested by others would also avoid confusion. Then it would be ok if Stream only exists under SignDetached
.
or change it to be overloaded by algorithm type and the RSA overload has a mandatory RSASignaturePadding parameter.
That's similar to what we did with CmsSigner
, so, I would advocate to follow that pattern.
What about support for using the _Tagged versions of COSE_Sign1 and COSE_Sign? Either a method to encode the structure including the enclosing tag
This implementation, currently, only produces tagged versions.
a decoding method that determines the type of the message from the tag, if the caller knows to expect one, just to save the caller dealing with the CBOR of the tag directly.
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 public static CoseMessage [CoseMessage::]DecodeAnyTagged(...)
... and then you have to as-cast it to Sign1/MultiSign/Mac/Mac0/Encrypt/Encrypt0. "Call and cast" always feels like a pit of failure to me.
What about support for using the _Tagged versions of COSE_Sign1 and COSE_Sign? Either a method to encode the structure including the enclosing tag
This implementation, currently, only produces tagged versions.
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.
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.
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.
Yeah, the public members being Sign and SignDetached might give better cluing as to what's going on. I'd go ahead and split Verify to Verify/VerifyDetached at the same time, to just make it clear what the pairings are.
I still advocate for the Verify variants to throw if they're called in the wrong state (providing content when embedded and vice-versa).
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 public static CoseMessage [CoseMessage::]DecodeAnyTagged(...)... and then you have to as-cast it to Sign1/MultiSign/Mac/Mac0/Encrypt/Encrypt0. "Call and cast" always feels like a pit of failure to me.
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.
CoseSignatureBuilder
We renamed CoseSignatureBuilder to CoseSigner, as the type is not really building anything, it is signing with the provided inputs.
RSASignaturePadding
We added RSASignaturePadding
to CoseSigner
and replaced Asymmetricalgorithm
, HashAlgorithmName
, and CoseHeaderMap
s parameters for a Cosesigner signer
for CoseSign1Message.Sign*
and CoseMiltiSignMessage.Sign*
, for Sign1 we will use the headers form the signer as the headers of the document.
Sign APIs being clearer about generating embedded versus detached payloads
We removed the bool isDetached
parameter and instead replaced Sign()
with SignEmbedded
and SignDetached
. This naming convention also applies for Verify*
.
CoseHeaderMap is an IEnumerable. I think it would be useful to have a Contains(label) method. Any reason this can't be an IDictionary?
We think that CoseHeaderMap doesn't need to be a dictionary, if you need a Contains
method, you can use map.TryGetEncodedValue(yourLabel, out _)
. In the hypotetical case where we turn CoseHeaderMap into a dictionary, it will be awkward to implement certain methods, e.g: Add()
, it would need to only accept a CBOR encoded value, having a custom map allow us to expose Get/SetValue(CoseHeaderLabel, int)
, Get/SetValue(CoseHeaderLabel, string)
, etc. and Get/SetEncodedValue(CoseHeaderLabel, ReadOnlySpan<byte>)
, keeping the method name clearer, as how you suggested with SignEmbedded
/SignDetached
, without tying the type to an Add(CoseHeaderLabel, ReadOnlySpan<byte> encodedValue)
.
external_aad
Yes, we will support it; all Sign operations will contain a optional param. ReadOnlySpan<byte> associatedData = default
or byte[] associatedData = null
.
RemoveSignature in CoseMultiSignMessage takes an index. Would an overload that takes a CoseSignature also be appropriate?
Yes, we will support RemoveSignature(CoseSignature signature)
as well.
_Tagged versions of COSE_Sign1 and COSE_Sign
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.
a decoding method that determines the type of the message from the tag, if the caller knows to expect one, just to save the caller dealing with the CBOR of the tag directly.
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. For tagged messages, we could provide a helper that returns the kind of message you have, but the implementation of such method would be to look at the first byte of the CBOR payload (the tag), which isn't that valuable. For now, you need to figure out yourself ahead of time which kind of meesage you are trying to decode.
I suggest changing associatedData
to externalData
just because the RFC calls this "externally supplied data". "Associated" isn't a word used with this in the RFC.
The handling of the two header buckets I find confusing because of the following:
COSE_Sign1
has a single set of these buckets. The API here for signing a single-signer message does not take them as parameters directly, but it takes a CoseSigner
which has these buckets, and so it would seem the headers in that object end up in the Headers
field of the COSE_Sign1
output.
COSE_Sign
has multiple sets of these buckets. There's a top-level set of headers for the entire message, and then each individual signature has its own headers. The Sign*
methods for CoseMultiSignMessage
take a CoseSigner
and optional protected and unprotected header buckets. Looking at this API in isolation, I would expect the two top-level arguments would populate the top-level headers for the COSE_Sign
message, and then the header buckets inside the CoseSigner
would populate the headers for the COSE_Signature
added to the signatures
array for this signer. But this differs from what appears to be the only interpretation of the signing APIs for CoseSign1Message
.
Is my interpretation correct?
public abstract int Encode(Span<byte> destination);
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 ComputeEncodedSize
and TryEncodeCore
as the only things that need to be implemented, then the rest can be implemented in terms of TryEncodeCore
.
public void RemoveSignature(CoseSignature signature) { throw null; }
Out of curiosity, how does this work? CoseSignature
does not implement IEquatable<CoseSignature>
. Should it? Or will the comparison remain a private implementation detail, or will it rely entirely on reference equality?
public void SetEncodedValue(CoseHeaderLabel label, ReadOnlySpan<byte> encodedValue)
This does not have a byte[]
overload like we typically do. I don't know if it's needed, but thought I would call it out as something that stands out to me. I get that the value will need to be cloned regardless of whether it is a span or an array, but we seem to be going out of out way to have byte[]
overloads where possible, except here.
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 set
? Why not just leave them as get
only?
Otherwise you end up having to do a lot of validation again in the set
, not mixing / matching RSASignaturePadding with ECDsa, etc.
Otherwise you end up having to do a lot of validation again in the set, not mixing / matching RSASignaturePadding with ECDsa, etc.
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.
Is my interpretation correct?
@kevinmkane yes it is correct, we removed CoseHeaderMaps from CoseSign1Message.Sign()
in order to avoid duplication with the headers on CoseSigner, it also makes more sense for using CoseSigner in both scenarios (multi and single).
public abstract int Encode(Span
destination); Does this need to be abstract? Can't it just be a regular method that basically does:
@vcsjones yes, [Try]Encode methods can be non-abstract. ComputeEncodedSize is actually implemented internally: https://github.com/dotnet/runtime/blob/89cee6e3fbd21ad79be00a735a017f0c932c1501/src/libraries/System.Security.Cryptography.Cose/src/System/Security/Cryptography/Cose/CoseSign1Message.cs#L99
And TryEncodeCore
can be exposed as you suggest, although I don't know about the naming, is it common to expose methods called DoSomethingCore
? Could we keep TryEncode
as abstract?
CoseSignature does not implement IEquatable
. Should it? Or will the comparison remain a private implementation detail, or will it rely entirely on reference equality?
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
Is my interpretation [of CoseSigner+Headers] correct?
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)
For RSASignaturePadding, at least, the settableness came from CmsSigner.
I... suppose. When we added it to CmsSigner
I had assumed it was done "because everything else on the class is settable" and mostly a consistency thing, not "this needs to be settable"
The scenario for the set on RSASignaturePadding is... slightly convoluted... but exists.
I'm not sure I follow this:
If we broke it up like
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!
};
I suggest changing associatedData to externalData just because the RFC calls this "externally supplied data". "Associated" isn't a word used with this in the RFC.
@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 [AesGcm.Decrypt (..., byte[]? associatedData = default)
](https://docs.microsoft.com/dotnet/api/system.security.cryptography.aesgcm.decrypt?view=net-6.0#system-security-cryptography-aesgcm-decrypt(system-byte()-system-byte()-system-byte()-system-byte()-system-byte())), it seem to me that both terms are used interchangeably, hence the proposed name of the C# parameter.
I think we can leave the constructors as-is. [and then a convincing example]
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 CoseSigner..ctor(Asymmetricalgorithm, ...
can just throw for RSA keys, instead of deferring the exception to a call to Sign.
is it common to expose methods called DoSomethingCore? Could we keep TryEncode as abstract?
In an attempt to please @bartonjs, I will borrow from the Framework Design Guidelines:
“CONSIDER naming protected virtual members that provide extensibility points for nonvirtual members by suffixing the nonvirtual member name with “Core.”
So, hopefully I am interpreting that correctly, but we also did it for the symmetric crypto one-shots.
To clarify the TryEncode
TryEncodeCore
:
TryEncodeCore
would be protected
and do little validation. TryEncode
would be public, validate parameters if needed, and then itself defer to TryEncodeCore
.
And TryEncodeCore can be exposed as you suggest, although I don't know about the naming, is it common to expose methods called DoSomethingCore?
For protected
members, yes. Framework Design Guidelines, 3rd edition, section 9.9 (Template Method (Pattern)).
As @vcsjones just beat me to quoting :smile:.
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 return false;
. And without that check, TryEncode
would just call TryEncodeCore
with no further information. So it seems like
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.
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)
@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 messageProtectedHeaders
and messageUnprotectedHeaders
but I don't have a strong preference either way.
I suggest changing associatedData to externalData just because the RFC calls this "externally supplied data". "Associated" isn't a word used with this in the RFC.
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 [AesGcm.Decrypt (..., byte[]? associatedData = default)](https://docs.microsoft.com/dotnet/api/system.security.cryptography.aesgcm.decrypt?view=net-6.0#system-security-cryptography-aesgcm-decrypt(system-byte()-system-byte()-system-byte()-system-byte()-system-byte())), it seem to me that both terms are used interchangeably, hence the proposed name of the C# parameter.
@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
Authenticated Encryption with Associated Data
https://csrc.nist.gov/glossary/term/aad
Additional Authentication Data
I don't know if the template method is useful.
That's a fair point. There is nothing to validate.
protected bool TryEncode(Span<byte> destination, out int bytesWritten);
Assuming then you meant for that one to be public?
And abstract. Do what I mean, not what I say :grin:
If existing .NET APIs already follow the "associated" naming convention, then I agree it's sensible to stick to that.
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.
[Try]Encode
methods and inclusion of int ComputeEncodedSize()
.Replace:
public abstract byte[] Encode();
public abstract int Encode(Span<byte> destination);
public abstract bool TryEncode(Span<byte> destination, out int bytesWritten);
With:
public abstract int ComputeEncodedSize();
public byte[] Encode() { }
public int Encode(Span<byte> destination) { }
public abstract bool TryEncode(Span<byte> destination, out int bytesWritten);
Add public void SetEncodedValue(CoseHeaderLabel label, byte[] encodedValue)
for parity with other byte-array overloads.
Remove all setters in CoseSigner
properties.
If there's no more feedback, I will label this as api-ready-for-review
.
public abstract int ComputeEncodedSize();
I don't know that that is the right name for a public API. AsnWriter
for example calls it GetEncodedLength
. I don't have super strong feelings on the name, but would perhaps reconsider my suggestion (which was based entirely off the existing internal method).
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.
- Remove all setters in
CoseSigner
properties.
In this case, ProtectedHeaders
and UnprotectedHeaders
shouldn't be nullable. Each CoseHeaderMap
should always be there, but may just be empty. Then a caller can always access it to modify the headers after creation but before doing a signing operation. And in that case, a method on the map to clear it would be useful.
Each
CoseHeaderMap
should always be there, but may just be empty.
new CoseSigner(key, hash, myProtectedHeaders)
?Each CoseHeaderMap should always be there, but may just be empty.
That will create allocations where they can be avoided.
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.
- What's wrong with forcing users to call
new CoseSigner(key, hash, myProtectedHeaders)
?
Since CoseHeaderMap
supports its contents being modified, a caller could reuse a CoseSigner
to add to the unprotected headers bucket by calling signer.UnprotectedHeaders.SetValue(...)
but only if the CoseSigner
had originally been created with a non-null unprotected headers bucket. The comment on the IsReadOnly
property implies UnprotectedHeaders
will be returned with this property false
and so allow this kind of modification (if it exists in the CoseSigner
).
As a result, in order to modify the unprotected headers, either the application always has to new
up a CoseSigner
, or have some logic to see if UnprotectedHeaders
is null, and then either calling SetValue
on the existing object to avoid an unnecessary allocation, or new
ing up a new CoseSigner
.
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 null
, the caller could have provided one rather than copy the contents into a newly allocated CoseSigner
.
If IsReadOnly
will always be true
for header maps returned by CoseSigner
(as opposed to only ProtectedHeaders
and ones allocated by callers), then the above pattern becomes impossible. But then we're deciding there is no in-place modification of unprotected headers and always requires allocating a new CoseSigner
.
[UnsupportedOSPlatform("browser")]
or just the leaf nodes (e.g. CoseSigner
's constructor).byte[]
parameters)
CoseHeaderMap
a dictionary of CoseHeaderKey
/CoseHeaderValue
, which makes the operations more uniform and also solves the problem that all operations can work on byte[]
not just ReadOnlyMemory<byte>
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);
}
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.
- What's wrong with forcing users to call
new CoseSigner(key, hash, myProtectedHeaders)
?Since
CoseHeaderMap
supports its contents being modified, a caller could reuse aCoseSigner
to add to the unprotected headers bucket by callingsigner.UnprotectedHeaders.SetValue(...)
but only if theCoseSigner
had originally been created with a non-null unprotected headers bucket. The comment on theIsReadOnly
property impliesUnprotectedHeaders
will be returned with this propertyfalse
and so allow this kind of modification (if it exists in theCoseSigner
).
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.
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.
CoseHeaderValue
struct.CoseHeaderMap
is now an IDictionary<CoseHeaderLabel, CoseHeaderValue>
, see the API usage section to undestand the usability changes this implies.
CoseHeaderMap.Enumerator
was removed. byte[]? associatedData = null
where they were missing.Sign*/Verify*
still needs to call RSA/ECDsa.TrySignHash
, the unsupported atttibute will still be needed unless we explicitly suppress the warning. CoseHeaderMap
IReadOnlyDictionary<CoseHeaderLabel, CoseHeaderValue>
[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 https://github.com/dotnet/runtime/issues/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.