Skip to content
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

Add support for indefinite length arrays #74215

Merged
merged 5 commits into from
Aug 29, 2022

Conversation

jozkee
Copy link
Member

@jozkee jozkee commented Aug 19, 2022

No description provided.

@jozkee jozkee added this to the 8.0.0 milestone Aug 19, 2022
@jozkee jozkee requested a review from bartonjs August 19, 2022 03:58
@jozkee jozkee self-assigned this Aug 19, 2022
@ghost
Copy link

ghost commented Aug 19, 2022

Tagging subscribers to this area: @dotnet/area-system-security, @vcsjones
See info in area-owners.md if you want to be subscribed.

Issue Details

null

Author: Jozkee
Assignees: Jozkee
Labels:

area-System.Security

Milestone: 8.0.0

…urity/Cryptography/Cose/CoseHeaderMap.cs

Co-authored-by: campersau <buchholz.bastian@googlemail.com>
@@ -157,7 +157,7 @@
<value>Critical Headers must be a definite-length CBOR array of at least one element.</value>
</data>
<data name="DecodeCoseSignatureMustBeArrayOfThree" xml:space="preserve">
<value>COSE Signature must be a definite-length array of 3 elements.</value>
<value>COSE Signature must be an array of 3 elements.</value>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<value>COSE Signature must be an array of 3 elements.</value>
<value>COSE Signature must be an array of three elements.</value>

Since DecodeSign1ArrayLengthMustBeFour wrote out the word, and I think there's some sort of guideline somewhere that numbers less than 10 should use words instead of digits.

@@ -322,19 +322,12 @@ private static byte[] DecodeSignature(CborReader reader)
private static List<CoseSignature> DecodeCoseSignaturesArray(CborReader reader, byte[] bodyProtected)
{
int? signaturesLength = reader.ReadStartArray();
List<CoseSignature> signatures = new List<CoseSignature>(signaturesLength ?? 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 1 the best fallback value here? Given that it's a multi, at least one is expected, and two or three doesn't seem unheard of.

I think I'd expect one of

  • 0: Let List decide its own default (4)
  • 4: List's default, bigger than the number we expect.
  • 10: Much bigger than we expect.
  • Doing some pre-counting to just get the right answer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 1 the best fallback value here?

Yes, this is reading a [+COSE_Signature] i.e: the array of signatures, which is required to have at least one. If we have more, the list will grow, if we have less (zero), we will throw.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, at least one item is expected, but why are we putting the List into its worst possible state of growth?

If you make a List(1), then it has 3 entries, it goes

  • new List(1)
    • storage = new T[1];
  • Add 1
  • Add 2
    • Too small! Double in size. new T[2]
  • Add 3
    • Too small! Double in size. new T[4]

new List<T>(0) says "choose your own default size" (which is 4); and any number other than 1 skips the 1->2 growth phase.

I agree that 1 is the most likely value for Count; but I think we should use literally any other number for initial capacity.

Copy link
Member Author

@jozkee jozkee Aug 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new List(0) says "choose your own default size" (which is 4); and any number other than 1 skips the 1->2 growth phase.

Ah, I didn't know that was how List's growth worked. In that case, we're better off using the pre-count since we're going to do it anyway.

{
int? length = reader.ReadStartArray();

if (length != CoseMultiSignMessage.CoseSignatureArrayLength)
if (length != null && length != CoseMultiSignMessage.CoseSignatureArrayLength)
{
throw new CryptographicException(SR.Format(SR.DecodeErrorWhileDecoding, SR.DecodeCoseSignatureMustBeArrayOfThree));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A definite array that is the wrong length will get a nice exception message.

An indefinite array that is the wrong length will presumably get a CBOR exception of some sort.

The best way I'm seeing to unify on the informative message, off the top of my head, would be to keep peeking for EndArray before (or in) the field reader helper methods. Or to just precount the values for indefinite arrays.

(Right now precounting looks to be hard with CborReader, since it doesn't have PeekEncodedValue, but we could add one)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think is worth adding extra logic for validation of indefinite-length arrays, I don't envision that they will be commonly used for COSE objects given that the spec is very clear about the length of the arrays used and they are not unreasonably large to not use a definite length in most cases.

However, if we wanted to check up-front the length of indefinite-length arrays, we could use ReadEncodedValue() and then create another reader (new CborReader(encodedValue)) and use SkipValue() to count the elements. Then just throw away said reader and continue with the original one.

{
byte[] cborPayload = ByteUtils.HexToByteArray("D29F43A10126A10442313154546869732069732074686520636F6E74656E742E58408EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB3640FF");
CryptographicException ex = Assert.Throws<CryptographicException>(() => CoseMessage.DecodeSign1(cborPayload));
Assert.IsType<InvalidOperationException>(ex.InnerException);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels to me like this exception should look the same as the definite array case.

Definite:

System.Security.Cryptography.CryptographicException: Error while decoding COSE message. COSE_Sign1 must be an array of four elements.

Indefinite:

System.Security.Cryptography.CryptographicException: Error while decoding COSE message. See the inner exception for details.
----- Inner Exception -----
  System.InvalidOperationException: Not at end of the indefinite-length data item.

And for the "one too short case" (which could use a test) we get the same informative exception with the definite length, but indefinite becomes

System.Security.Cryptography.CryptographicException: Error while decoding COSE message. See the inner exception for details.
----- Inner Exception -----
  System.InvalidOperationException: Cannot perform the requested operation, the next CBOR data item is of major type '7'

(I think '7' is what'll report, at least).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar concern as your previous comment #74215 (comment).
Is it worth to check up-front indefinite-length arrays length just to get nice exceptions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple of potential reasons someone gets here, but one of them is getting the method call backwards between COSE_Sign1 and COSE_Sign formats. In the case of definite length arrays there's a "you might be the wrong item" message. For indefinite the message is way more obscure.

Assuming the developer trying to read a file is not the one who wrote it, they don't control the definite or indefinite encoding; so it seems wrong to punish them for being in a corner case. So, yeah, I think we should check the length up front.

I agree that the only way to really pre-count it is to use ReadEncodedValue() and make a second reader (and, for indefinite, a third one), but ideally we won't do that when it's a definite length. So my recommendation is probably

  • Use pre-counting now.
  • Noodle around in the CBOR project to make sure that adding a PeekEncodedValue() member is as easy as I think it should be
  • API proposal for PeekEncodedValue()
  • When implementing PeekEncodedValue() also come here and make use of it:
ReadOnlyMemory<byte> encodedArray = reader.PeekEncodedValue();
int? len = reader.ReadStartArray();

if (!len.HasValue)
{
    // make a reader over encodedArray, count how many items were in it.
}

if (len != TheAppropriateConstant)
{
    throw new ...
}

That makes definite arrays continue as they are (with a minor hit for double-reading the start array and saving the ReadOnlyMemory), and only indefinite arrays pay the allocation of a second reader, and there's never a third reader.

…urity/Cryptography/Cose/CoseMessage.cs

Co-authored-by: Jeremy Barton <jbarton@microsoft.com>
@jozkee jozkee merged commit ab38010 into dotnet:main Aug 29, 2022
@jozkee jozkee deleted the cose_indefinitelength branch August 29, 2022 15:44
@ghost ghost locked as resolved and limited conversation to collaborators Sep 28, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants