Skip to content

Commit

Permalink
Merge branch 'main' into c3id
Browse files Browse the repository at this point in the history
  • Loading branch information
nguerrera committed Jan 15, 2025
2 parents 6797275 + 01ac458 commit 35e69e5
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 27 deletions.
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
# Project
# Common Annotated Security Key (CASK) Standard and Reference Implementation

> This repo has been populated by an initial template to help get you started. Please
> make sure to update the content to build a great experience for community-building.
CASK seeks to define an open standard for representing security keys such that they can are highly identifiable by secret scanning tools.

As the maintainer of this project, please make a few updates:
This repository will contain the standard as well as reference implementations in various languages.

- Improving this README.MD file to provide a great experience
- Updating SUPPORT.MD with content about this project's support experience
- Understanding the security reporting process in SECURITY.MD
- Remove this section from the README
NOTE: This project is just getting started, incomplete, and not yet ready for use.

## Contributing

Expand Down
16 changes: 2 additions & 14 deletions SUPPORT.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
# TODO: The maintainer of this repo has not yet edited this file

**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?

- **No CSS support:** Fill out this template with information about how to file issues and get help.
- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps.
- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide.

*Then remove this first heading from this SUPPORT.MD file before publishing your repo.*

# Support

## How to file issues and get help
Expand All @@ -16,10 +6,8 @@ This project uses GitHub Issues to track bugs and feature requests. Please searc
issues before filing new issues to avoid duplicates. For new issues, file your bug or
feature request as a new Issue.

For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
For help and questions about using this project, please use GitHub Discussions.

## Microsoft Support Policy

Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
Support for this project is limited to the resources listed above.
87 changes: 87 additions & 0 deletions docs/PrimaryKey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# CASK 256-bit Primary Keys
## Standard Backus-Naur Form (BNF)
```
<key> ::= <payload-data> <checksum>
<payload-data> ::= <random-data> <reserved> [<optional-fields>] <cask-signature> <provider-id> <timestamp> <key-type> <version>
<random-data> ::= 42 * <base64url> <base64-two-zeros-suffix> ; The total random data comprises 256 bits encoded as 42
; characters x 6 bit bits of random data = 252 bits and
; 1 character providing 4 bits of random data padded with 00b.
<reserved> ::= '0' ; Reserved for future use.
<optional-fields> ::= { <optional-field> } ; Zero or more 4-character (24 bit) sequences of optional data.
<optional-field> ::= 4 * <base64url> ; Each optional field is 4 characters (24 bits). This keeps
; data cleanly aligned along 3-byte/4-encoded character boundaries
; facilitating readability of encoded form as well as byte-wise use.
<cask-signature> ::= 'JQQJ' ; Fixed signature identifying the CASK key
<provider-id> ::= 4 * <base64url> ; Provider identifier (24 bits)
<timestamp> ::= <year> <month> <day> <hour> ; Timestamp components
<year> ::= <base64url> ; Represents the year, 'A' (2024) to '_' (2087)
<month> ::= 'A'..'L' ; For months January to December
<day> ::= 'A'..'Z' | 'a'..'e' ; 'A' = day 1, 'B' = day 2, ... 'e' = day 30, ... 'e' = day 31
<hour> ::= 'A'..'X' ; Represents hours 0-23. 'A' = hour 0 (midnight), ... 'X' = hour 23.
<key-type> ::= <256-bit-key> | <256-bit-hash> | <384-bit-hash>
<256-bit-key> ::= 'A'
<256-bit-hash> ::= 'H'
<384-bit-hash> ::= 'I'
<version> ::= 'A'
<checksum> ::= <four-zeros-prefix-base64> 5 * <base64url> ; The checksum is 32 bits total encoded in six 6-bit characters.
; The data starts with 0000b (four leading zero bit) and 2 bits
; of checksum data followed by the remaining 30 bits of checksum.
<base64url> ::= 'A'..'Z' | 'a'..'z' | '0'..'9' | '-' | '_'
<four-zeros-prefix-base64> ::= 'A'..'D' ; Base64 characters starting with 0000b (indices 0-3).
<base64-two-zeros-suffix> ::= 'A' | 'E' | 'I' | 'M' | 'Q' | 'U' | 'Y' | 'c' ; Base64 characters ending in 00b. These indices are all
| 'g' | 'k' | 'o' | 's' | 'w' | '0' | '4' | '8' ; multiple of 4 (or the value of 0b), a fact that may be
; useful in some contexts.
```
## Byte-wise Rendering
|Byte Range|Decimal|Hex|Binary|Description|
|-|-|-|-|-|
|decodedKey.Substring(32)|0...255|0x0...0xFF|00000000b...11111111b|256 bits of random data produced by a cryptographically secure RNG|
|decodedKey[32]|0|0x00|00000000b| A reserved byte to enforce 3-byte alignment, set to zero.
|decodedKey[33..^15]|0...255|0x0...0xFF|00000000b...11111111b|Provider-defined data, comprising 0 or more 3-byte sequences, of arbitrary interpretation.
|decodedKey[^15..^12]| 37, 4, 9 |0x25, 0x04, 0x09| 00100101b, 00000100b, 00001001b | Decoded 'JQQJ' signature.
|decodedKey[^12..^9]|0...255|0x0...0xFF|00000000b...11111111b| Provider identifier, e.g. , '0x4c', '0x44', '0x93' (base64 encoded as 'TEST')
|decodedKey[^9..^6]||||Time stamp data encoded in 4 six-bit blocks for YMDH.
|decodedKey[^6] >> 2|0, 28, 32|0x00, 0x1c, 0x20|00000000b...11111100b| Leading 6 bits comprises kind enum followed by 2 bits of reserved padding. key[key.Length - 6] & 0xfc == 0.
|decodedKey[^5] >> 4]|0|0xFF|00000000b...11110000b| Leading 4 bits comprise 4 bits of reserved version information + 4 bits of zero padding (to preserve consistent rendering of the subsequence checksum data).
|decodedKey[^4..]|0...255|0x0...0xFF|00000000b..11111111b|CRC32(key[..^4])

## Primary 256-bit Key Base64-Encoded Rendering
|String Range|Text Value|Description|
|-|-|-|
|encodedKey[..42] | 'A'...'_' | 252 bits of randomized data generated by cryptographically secure RNG
|encodedKey[42] | <base64-two-zeros-suffix> | 4 bits of randomized data followed by 2 zero bits. See the <base64-two-zeros-suffix> definition for legal values.
|encodedKey[43] | 'A' | 6 bits of reserved data specified as 'A', base64 character index zero.
|encodedKey[44..^20]|'A'...'_'|0 or more 4-character sequences comprising provider optional data, of arbitrary interpretation.
|encodedKey[^20..^16]|'JQQJ'| Fixed CASK signature.
|encodedKey[^16]|'A'...'Z'\|'a'...'z'| The first character of the provider signature which must be alphabetic (upper-case indicating a customer-managed as opposed to service managed secret).
|encodedKey[^15..^12]|('A'...'Z'-\_)\|('a'...'z'-\_)| | The remaining three encoded characters. Any alphabetic characters must be consistently upper- or lower-case (to distinguish customer- vs. service-managed secrets).
|encodedKey[^12]|'A'...'_'|Represents the year of key allocation, 'A' (2024) to '_' (2087)|
|encodedKey[^11]|'A'...'L'|Represents the month of key allocation, 'A' (January) to 'L' (December)|
|encodedKey[^10]|'A'...'Z'\|'a'..'e'|Represents the day of key allocation, 'A' (0) to 'e' (31)|
|encodedKey[^9]|'A'...'X'|Represents the hour of key allocation, 'A' (hour 0 or midnight) to 'X' (hour 23). [TBD: This value could be used for half hour increments instead].
|encodedKey[^8]|'A', 'H', 'I'|Represents the key kind, a 256-bit primary key, HMAC256 or HMAC384.
|encodedKey[^7]|'A'|Cask version 1.0
|encodedKey[^6]| 'A'...'D'| Four leading zero bits followed by the
|encodedKey[^5..]|'A'...'_'|The final five encoded checksum characters.
13 changes: 13 additions & 0 deletions src/Cask/CaskVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace CommonAnnotatedSecurityKeys;
internal enum CaskVersion : byte
{
/// <summary>
/// Specifies version 1.0.0 of CASK.
/// </summary>
OneZeroZero,

Reserved = 0xfe,
}
51 changes: 51 additions & 0 deletions src/Cask/KeyKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace CommonAnnotatedSecurityKeys;

internal enum BytewiseKeyKind : byte
{
/// <summary>
/// Specifies a 256-bit primary API or other security key. The enum
/// value '0' comprises the base64 index that maps to character 'A'.
/// </summary>
Key256Bit = ('A' - 'A') << 2, // Base64 index 0 == 'A'

/// <summary>
/// Specifies a CASK hashed signature that incorporates a Message
/// Authentication code of a derivation input generated by SHA-256.
/// </summary>
Hash256Bit = ('H' - 'A') << 2, // Base64 index 7 == 'H', << 2 == 28, 0x1c, 0b000 11100

/// <summary>
/// Specifies a CASK hashed signature that incorporates a Message
/// Authentication code of a derivation input generated by SHA-384.
/// </summary>
Hash384Bit = ('I' - 'A') << 2, // Base64 index 7 == 'H', << 2 == 32, 0x20, 0b0010 0000
}

internal enum EncodedKeyKind : byte
{
/// <summary>
/// Specifies a 256-bit primary API or other security key. The enum
/// value '0' comprises the base64 index that maps to character 'A'.
/// </summary>
Key256Bit = 'A' - 'A', // Base64 index 0 == 'A'

/// <summary>
/// Specifies a CASK hashed signature that incorporates a Keyed Hash
/// Message Authenticode of a derivation input generated by SHA-256
/// (HMAC-SHA-256) along with other CASK data such as the generated
/// C3ID of the CASK key used to initialize the HMAC instance.
/// </summary>
Hash256Bit = 'H' - 'A', // Base64 index 7 == 'H', 7, 0x7, 0b0111

/// <summary>
/// Specifies a CASK hashed signature that incorporates a Keyed Hash
/// Message Authenticode of a derivation input generated by SHA-384
/// (HMAC-SHA-384) along with other CASK data such as the generated
/// C3ID of the CASK key used to initialize the HMAC instance.
/// </summary>
Hash384Bit = 'I' - 'A', // Base64 index 7 == 'I', 8, 0x8, 0b1000
}

9 changes: 9 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,14 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

<PropertyGroup Label="Build Environment Helpers">
<_OSIsWindows>false</_OSIsWindows>
<_OSIsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</_OSIsWindows>
<_OSIsX64>false</_OSIsX64>
<_OSIsX64 Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">true</_OSIsX64>
<_MSBuildIsNETFramework>false</_MSBuildIsNETFramework>
<_MSBuildIsNETFramework Condition="'$(MSBuildRuntimeType)' == 'full'">true</_MSBuildIsNETFramework>
</PropertyGroup>

<Import Project="Directory.WarningsAsErrors.props" />
</Project>
10 changes: 5 additions & 5 deletions src/Tests/Cask.Tests/Cask.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net472</TargetFrameworks>
<OSIsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</OSIsWindows>
<OSIsX64 Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">true</OSIsX64>
<MSBuildIsNETFramework Condition="'$(MSBuildRuntimeType)' == 'full'">true</MSBuildIsNETFramework>
<BuildCpp Condition="'$(OSIsWindows)' == 'true' and '$(MSBuildIsNETFramework)' == 'true' and '$(OSIsX64)' == 'true'">true</BuildCpp>
<!-- Don't try to run net472 tests on non-Windows using Mono. We do not support Mono. -->
<TargetFrameworks Condition="!$(_OSIsWindows)">net8.0</TargetFrameworks>
<!-- TODO: Our custom C++ build logic is breaking fast-up-to-date for this project. -->
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
<EnableUnmanagedDebugging>true</EnableUnmanagedDebugging>
<_BuildCpp>false</_BuildCpp>
<_BuildCpp Condition="$(_OSIsWindows) and $(_MSBuildIsNETFramework) and $(_OSIsX64)">true</_BuildCpp>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Cask\Cask.csproj" />
<None Update="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup Condition="'$(BuildCpp)' == 'true'">
<ItemGroup Condition="$(_BuildCpp)">
<CppProjectReference Include="..\..\libcask\libcask.vcxproj" Properties="Platform=x64" />
<Content Include="$(ArtifactsPath)\bin\libcask\$(Configuration.ToLowerInvariant())_x64\libcask.dll" CopyToOutputDirectory="PreserveNewest" Visible="false" />
<AssemblyMetadata Include="BuiltWithCppSupport" Value="true" />
Expand Down
95 changes: 95 additions & 0 deletions src/Tests/Cask.Tests/CaskSecretsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,101 @@ public void CaskSecrets_IsCask(int secretEntropyInBytes)
IsCaskValidate(key);
}

[Fact]
public void CaskSecrets_EncodedMatchesDecoded()
{
// The purpose of this test is to actually produce useful notes in documentation
// as far as decomposing a CASK key, both from its url-safe base64 form and from
// the raw bytes.

// The test key below introduces a single, optional 3-byte data value, encoded
// as the opaque, uninterpreted value of '----'. The remainder of the code
// demonstrates the core CASK technique of obtaining metadata from the right
// end of the key, obtaining size information from the key kind enum, and
// based on that data isolating the randomized component from the optional data.

string encodedKey = "VOSUKmuLLhv-Fb8czyM-SLRVC5A1orq0vlp65S3fBNEA----JQQJTESTBACDHACcJrXz";
byte[] keyBytes = Base64Url.DecodeFromUtf8(Encoding.UTF8.GetBytes(encodedKey));

string encodedCaskSignature = encodedKey[^20..^16];
Span<byte> bytewiseCaskSignature = keyBytes.AsSpan()[^15..^12];
Assert.Equal(Base64Url.EncodeToString(bytewiseCaskSignature), encodedCaskSignature);

string encodedProviderId = encodedKey[^16..^12];
Span<byte> bytewiseProviderId = keyBytes.AsSpan()[^12..^9];
Assert.Equal(Base64Url.EncodeToString(bytewiseProviderId), encodedProviderId);

string encodedTimestamp = encodedKey[^12..^8];
Span<byte> bytewiseTimestamp = keyBytes.AsSpan()[^9..^6];
Assert.Equal(Base64Url.EncodeToString(bytewiseTimestamp), encodedTimestamp);

// The final 2 bits of this byte are reserved.
char encodedKeyKind = encodedKey[^8];
var kind = (BytewiseKeyKind)(keyBytes.AsSpan()[^6]);
Assert.Equal(BytewiseKeyKind.Hash256Bit, kind);
Assert.Equal(Base64Url.EncodeToString([(byte)kind]), $"{encodedKeyKind}A");

int optionalDataIndex = GetOptionalDataByteIndex(kind) + 1;
int encodedOptionalDataIndex = (optionalDataIndex / 3) * 4;
string encodedOptionalData = encodedKey[encodedOptionalDataIndex..^20];
Span<byte> optionalData = keyBytes.AsSpan()[(optionalDataIndex)..^15];
Assert.Equal(Base64Url.EncodeToString(optionalData), encodedOptionalData);

char encodedVersion = encodedKey[^7];
var version = (CaskVersion)(keyBytes.AsSpan()[^5]);
Assert.Equal(CaskVersion.OneZeroZero, version);

// Our checksum buffer here is 6 bytes because the 4-byte checksum
// must itself be decoded from a buffer that properly pads the
// initial byte. We simulate this by zeroing the first two bytes
// of the buffer and using the last 4 for the checksum
string encodedChecksum = encodedKey[^6..];
Span<byte> crc32 = stackalloc byte[6];
Crc32.Hash(keyBytes.AsSpan()[..^4], crc32[2..]);
Assert.Equal(Base64Url.EncodeToString(crc32)[2..], encodedChecksum);

// This follow-on example demonstrates obtaining a three-byte sequence and
// obtain one of its four constituent 6-bit sequences. This is useful in
// the core definition to obtain the size and version information, which are
// 12 bits that precede a 4-bit zero padding sequence followed by the first
// byte of the checksum.
//
// Obtaining the textual value of a 6-bit sequence is trivial from the
// encoded form but even in this case, it is inconvenient to convert that
// data into programmatic constructs such as enums. The 'ThreeByteSequence'
// struct is intended to assist with this problem.

Span<byte> sizeAndVersionSequence = keyBytes.AsSpan()[^6..^3];
var sequence = new ThreeByteSequence(sizeAndVersionSequence);

var encodedKind = (EncodedKeyKind)sequence.FirstSixBits;
Assert.Equal(BytewiseKeyKind.Hash256Bit, kind);

version = (CaskVersion)sequence.SecondSixBits;
Assert.Equal(CaskVersion.OneZeroZero, version);
}

private int GetOptionalDataByteIndex(BytewiseKeyKind kind)
{
switch (kind)
{
case BytewiseKeyKind.Key256Bit:
case BytewiseKeyKind.Hash256Bit:
return 32;

case BytewiseKeyKind.Hash384Bit:
return 48;
}

throw new InvalidOperationException();
}

byte GetSingleEncodedChar(char input)
{
byte[] arg = Encoding.UTF8.GetBytes($"{input}A==");
return Base64Url.DecodeFromUtf8(arg).First();
}

[Fact]
public void CaskSecrets_IsCask_InvalidKey_Null()
{
Expand Down
31 changes: 31 additions & 0 deletions src/Tests/Cask.Tests/ThreeByteSequence.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Buffers.Text;

namespace CommonAnnotatedSecurityKeys;

internal readonly struct ThreeByteSequence
{
public ThreeByteSequence(ReadOnlySpan<byte> bytes)
{
if (bytes.Length != 3)
{
throw new ArgumentException("Three-byte sequence must be exactly three bytes long.", nameof(bytes));
}
Bytes = bytes.ToArray();
Encoded = Convert.ToBase64String(Bytes);
}

public byte[] Bytes { get; }

public string Encoded { get; }

public byte FirstSixBits => (byte)(Bytes[0] >> 2);

public byte SecondSixBits => (byte)(((Bytes[0] & 0b00000011) << 4) | Bytes[1] >> 4);

public byte ThirdSixBits => (byte)(((Bytes[1] & 0b00001111) << 2) | Bytes[2] >> 6);

public byte FourthSixBits => (byte)(Bytes[2] & 0b00111111);
}
Loading

0 comments on commit 35e69e5

Please sign in to comment.