Skip to content

Instantly share code, notes, and snippets.

@Jozkee
Last active February 9, 2022 00:18
Show Gist options
  • Save Jozkee/7a7a27a805ddb44fe1bf102cdd5c7b42 to your computer and use it in GitHub Desktop.
Save Jozkee/7a7a27a805ddb44fe1bf102cdd5c7b42 to your computer and use it in GitHub Desktop.
System.Security.Cryptography.Cose how to use

How to install

  1. Add dotnet7 nuget feed (https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json) in your Package Sources.
  2. Search and install System.Security.Cryptography.Cose. Don't forget to check the "Include prerelease" checkbox.

Encode and Sign

using System.Security.Cryptography;
using System.Security.Cryptography.Cose;
using System.Text;

byte[] content = Encoding.UTF8.GetBytes("This is the content.");

ECDsa myKey = ECDsa.Create();
byte[] encodedMsg = CoseSign1Message.Sign(content, myKey, HashAlgorithmName.SHA256);

// You have a CBOR encoded message signed with ECDSA w/SHA256 (ES256).
// that you can share with anyone and they can verify in with your public key.

Decode and Verify

// Having a CBOR encoded message (encodedMsg) a key (myKey) and a content in case of detached content (myContent),
// you decode it and then verify it using the key and the content in case of detached content.
CoseSign1Message msg = CoseMessage.DecodeSign1(encodedMsg);

// The COSE message doesn't carry the content (required for verification), it needs to be supplied.
bool isContentDetached = !msg.Content.HasValue;
bool verified;

if (isContentDetached)
{
    verified = msg.Verify(myKey, myContent);
}
else
{
    verified = msg.Verify(myKey);
}

if (!verified)
{
    throw new Exception("Failed to verify COSE message.");
}

Console.WriteLine("Message verified!");
Console.WriteLine(isContentDetached ? "COSE message doesn't contain content" : "Content: " + Encoding.UTF8.GetString(msg.Content!.Value.Span));

Read and Write [custom] headers.

ECDsa myKey = ECDsa.Create();
var myArrayHeader = new CoseHeaderLabel("my-array-header");

CoseHeaderMap unprotectedHeaders = new CoseHeaderMap();
unprotectedHeaders.SetValue(CoseHeaderLabel.ContentType, "text/plain; charset=utf-8");
unprotectedHeaders.SetEncodedValue(myArrayHeader, GetCborArray());

// Encode but with user-defined headers.
byte[] encodedMsg = CoseSign1Message.Sign(content, protectedHeaders: new CoseHeaderMap(), unprotectedHeaders, myKey, HashAlgorithmName.SHA256);

// -- handle a message like what we just encoded ------------------------
CoseSign1Message msg = CoseMessage.DecodeSign1(encodedMsg);

// Get info from the unprotected headers.
Console.WriteLine("============ Unprotected headers ============");

// Read header value expecting it to be string (tstr).
Console.WriteLine(msg.UnprotectedHeaders.GetValueAsString(CoseHeaderLabel.ContentType)); 

// Read header value as CBOR encoded bytes, you need to pass the bytes to a CborReader and read it manually.
PrintCborArray(msg.UnprotectedHeaders.GetEncodedValue(myArrayHeader)); 

// Get info from the unprotected headers - using the CoseHeaderMap enumerator.
Console.WriteLine("=== Unprotected headers using enumeration ===");
foreach ((CoseHeaderLabel label, ReadOnlyMemory<byte> encodedValue) in msg.UnprotectedHeaders)
{
    if (label == CoseHeaderLabel.ContentType)
    {
        Console.WriteLine(DecodeCborTextString(encodedValue));
    }
    else if (label == myArrayHeader)
    {
        PrintCborArray(encodedValue);
    }
    else
    {
        throw new Exception("Unknown header found");
    }
}

// Helpers
static ReadOnlySpan<byte> GetCborArray()
{
    var writer = new CborWriter();
    writer.WriteStartArray(definiteLength: 3);
    writer.WriteInt32(42);
    writer.WriteTextString("foo");
    writer.WriteTextString("bar");
    writer.WriteEndArray();

    return writer.Encode();
}

static void PrintCborArray(ReadOnlyMemory<byte> encodedArray)
{
    var reader = new CborReader(encodedArray);
    int? length = reader.ReadStartArray();

    Console.WriteLine($"Array length: {length?.ToString() ?? "Indefinite"}");
    Console.Write("[");

    int elementsCount = 0;
    while (true) 
    {
        CborReaderState state = reader.PeekState();
        if (state == CborReaderState.EndArray) // this wouldn't work with nested arrays.
            break;

        string valueAsStr = state switch
        {
            CborReaderState.NegativeInteger or CborReaderState.UnsignedInteger => reader.ReadInt32().ToString(),
            CborReaderState.TextString => reader.ReadTextString(),
            _ => throw new InvalidOperationException()
        };
        
        Console.Write($"{valueAsStr}{(++elementsCount == length ? "" : ", ")}");
    }

    Console.WriteLine("]");

    if (reader.BytesRemaining > 0)
    {
        throw new Exception("Malformed CBOR - Trailing data after reading array");
    }
}

static string DecodeCborTextString(ReadOnlyMemory<byte> encodedString)
{
    var reader = new CborReader(encodedString);

    string retVal = reader.ReadTextString();

    if (reader.BytesRemaining > 0)
    {
        throw new Exception("Malformed CBOR - Trailing data after reading string");
    }

    return retVal;
}

Output of this code:

============ Unprotected headers ============
content-type: text/plain; charset=utf-8
my-array-header:
Array length: 3
[42, foo, bar]

=== Unprotected headers using enumeration ===
content-type: text/plain; charset=utf-8
my-array-header:
Array length: 3
[42, foo, bar]

Use GetEncodedValue and SetEncodedValue to operate with any CBOR type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment