Skip to content

Commit

Permalink
Convert CASK fixed signature from JQQJ to QJJQ (#39)
Browse files Browse the repository at this point in the history
Tracked in #38. 

Our CASK work is stomping on some historical exploration of these key
format techniques. This change allows a clean separation. Eventually all
historic use of `JQQJ` in other projects s/be upgraded to CASK and any
historical detections of these keys will be deprecated.
  • Loading branch information
michaelcfanning authored Feb 25, 2025
1 parent 556f9b6 commit 4345ab1
Show file tree
Hide file tree
Showing 7 changed files with 19 additions and 19 deletions.
2 changes: 1 addition & 1 deletion docs/GenerateKeyPseudoCode.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

1. Generate 256 bits of cryptographically secure random data. Store the result at the beginning of the generated key.
1. Write sensitive data size to next byte, e.g., 0 to indicate 256-bits.
1. Write CASK signature [0x25, 0x04, 0x09] ("JQQJ", base64-decoded) to the next 3 bytes.
1. Write CASK signature [0x40, 0x92, 0x50] ("QJJQ", base64-decoded) to the next 3 bytes.
1. Base64url-decode provider signature and store the result in the next 3 bytes.
1. Write 0x00 to the next byte to indicate a CASK primary key kind.
1. Left-shift the provider key kind by 2 bits and store the result in the next byte.
Expand Down
6 changes: 3 additions & 3 deletions docs/PrimaryKey.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
; characters x 6 bit bits of random data = 252 bits and
; 1 character providing 4 bits of random data padded with 00b.
<sensitive-data-size> ::= 'A' ; 'A' indicates a 256-bit key.
<cask-signature> ::= 'JQQJ' ; Fixed signature identifying the CASK key.
<cask-signature> ::= 'QJJQ' ; Fixed signature identifying the CASK key.
<provider-signature> ::= 4 * <base64url> ; Provider identifier (24 bits).
<provider-kind> ::= <base64url> ; Provider-defined key kind.
<cask-kind> ::= <primary-key> | <derived-key> | <hmac>
Expand Down Expand Up @@ -36,7 +36,7 @@
|-|-|-|-|-|
|decodedKey[..31]|0...255|0x0...0xFF|00000000b...11111111b|256 bits of random data produced by a cryptographically secure RNG|
|decodedKey[32]|0|0x00|00000000b| 2 bits of reserved padding + the key size.
|decodedKey[33..36]| 37, 4, 9 |0x25, 0x04, 0x09| 00100101b, 00000100b, 00001001b | Decoded 'JQQJ' signature.
|decodedKey[33..36]| 37, 4, 9 |0x40, 0x92, 0x50| 00100000b, 10010010b, 01010000b | Decoded 'QJJQ' signature.
|decodedKey[36..39]|0...255|0x0...0xFF|00000000b...11111111b| Provider identifier, e.g. , '0x4c', '0x44', '0x93' (base64 encoded as 'TEST')
|decodedKey[39]|||| 6-bit provider key kind + 2 bits of reserved padding.
|decodedKey[40]|||| 4-bit CASK provider key kind + 4 bits padding.
Expand All @@ -51,7 +51,7 @@
|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' | The 6-bit encoded sensitive component size.
|encodedKey[44..48]|'JQQJ'| Fixed CASK signature.
|encodedKey[44..48]|'QJJQ'| Fixed CASK signature.
|encodedKey[48..52]|('A'...'Z'-\_)\|('a'...'z'-\_)| | The provider signature. Optionally, this data can be encoded to distinguish user- vs. service-managed keys.
|encodedKey[52]|'A'...'_'|Provider key kind or 'A' if not populated|
|encodedKey[53]|'P'|CASK key kind, a primary key. |
Expand Down
6 changes: 3 additions & 3 deletions src/Cask/Cask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static bool IsCask(ReadOnlySpan<char> key)
return false;
}

// Check for CASK signature, "JQQJ".
// Check for CASK signature, "QJJQ".
if (!key[CaskSignatureCharRange].SequenceEqual(CaskSignature))
{
return false;
Expand Down Expand Up @@ -73,7 +73,7 @@ public static bool IsCaskUtf8(ReadOnlySpan<byte> keyUtf8)
return false;
}

// Check for CASK signature, "JQQJ" UTF-8 encoded.
// Check for CASK signature, "QJJQ" UTF-8 encoded.
if (!keyUtf8[CaskSignatureCharRange].SequenceEqual(CaskSignatureUtf8))
{
return false;
Expand Down Expand Up @@ -119,7 +119,7 @@ public static bool IsCaskBytes(ReadOnlySpan<byte> keyBytes)
return false;
}

// Check for CASK signature. "JQQJ" base64-decoded.
// Check for CASK signature. "QJJQ" base64-decoded.
if (!keyBytes[CaskSignatureByteRange].SequenceEqual(CaskSignatureBytes))
{
return false;
Expand Down
2 changes: 1 addition & 1 deletion src/Cask/CaskKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ private void ThrowIfNotInitialized()
}

// language=regex
private const string RegexPattern = """(^|[^A-Za-z0-9+\/\-_])[A-Za-z0-9\-_]{43}AJQQJ[A-Za-z0-9\-_]{5}(D|H|P)[A-Za-z0-9\-_]{23}[A-L][A-Za-e][A-X][A-Za-z0-7][A-Za-z0-9\-_]{3,27}($|[^A-Za-z0-9+\/\-_])""";
private const string RegexPattern = """(^|[^A-Za-z0-9+\/\-_])[A-Za-z0-9\-_]{43}AQJJQ[A-Za-z0-9\-_]{5}(D|H|P)[A-Za-z0-9\-_]{23}[A-L][A-Za-e][A-X][A-Za-z0-7][A-Za-z0-9\-_]{3,27}($|[^A-Za-z0-9+\/\-_])""";
private const RegexOptions RegexFlags = RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant;

[GeneratedRegex(RegexPattern, RegexFlags)]
Expand Down
12 changes: 6 additions & 6 deletions src/Cask/InternalConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ namespace CommonAnnotatedSecurityKeys;
internal static partial class InternalConstants
{
/// <summary>
/// The base64-encoded CASK signature "JQQJ" in UTF-16.)
/// The base64-encoded CASK signature "QJJQ" in UTF-16.)
/// </summary>
public static ReadOnlySpan<char> CaskSignature => "JQQJ".AsSpan();
public static ReadOnlySpan<char> CaskSignature => "QJJQ".AsSpan();

/// <summary>
/// The base64-encoded CASK signature "JQQJ" in UTF-8.
/// The base64-encoded CASK signature "QJJQ" in UTF-8.
/// </summary>
public static ReadOnlySpan<byte> CaskSignatureUtf8 => "JQQJ"u8;
public static ReadOnlySpan<byte> CaskSignatureUtf8 => "QJJQ"u8;

/// <summary>
/// The base64-decoded CASK signature "JQQJ" in bytes.
/// The base64-decoded CASK signature "QJJQ" in bytes.
/// </summary>
public static ReadOnlySpan<byte> CaskSignatureBytes => [0x25, 0x04, 0x09];
public static ReadOnlySpan<byte> CaskSignatureBytes => [0x40, 0x92, 0x50];

/// <summary>
/// The number of bytes in a CASK signature
Expand Down
2 changes: 1 addition & 1 deletion src/Tests/Cask.Tests/CaskKeyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public void CaskKey_TryEncodeInvalidKey()
Base64Url.DecodeFromChars(key.ToString().AsSpan(), decoded);

const int caskSignatureByteIndex = 33;
Assert.Equal(0x25, decoded[caskSignatureByteIndex]);
Assert.Equal(0x40, decoded[caskSignatureByteIndex]);

// Break the key by invaliding the CASK signature.
decoded[caskSignatureByteIndex] = (byte)'X';
Expand Down
8 changes: 4 additions & 4 deletions src/Tests/Cask.Tests/CaskSecretsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public void CaskSecrets_IsCask()
}

[Theory]
[InlineData("Y7G_WqVrIxJ9y3kqLdX6OOhTwC1kTF0eWQidLckLqfEAJQQJTESTMPAlrkxagZHvE1rmbBnVwEHZBBRVnAAA_NG_", CaskKeyKind.PrimaryKey)]
[InlineData("V5ja_SGw4_eyqKw-mBfx8DlqjJfea4Qs5B6AR3HjlgwAJQQJTESTMPCK8K_4JYG3ppYTmdnSS4TcBBQXDAAA_NG_", CaskKeyKind.PrimaryKey, "CK8K_4JYG3ppYTmdnSS4Tc")]
[InlineData("Y7G_WqVrIxJ9y3kqLdX6OOhTwC1kTF0eWQidLckLqfEAQJJQTESTMPAlrkxagZHvE1rmbBnVwEHZBBRVnAAA_NG_", CaskKeyKind.PrimaryKey)]
[InlineData("V5ja_SGw4_eyqKw-mBfx8DlqjJfea4Qs5B6AR3HjlgwAQJJQTESTMPCK8K_4JYG3ppYTmdnSS4TcBBQXDAAA_NG_", CaskKeyKind.PrimaryKey, "CK8K_4JYG3ppYTmdnSS4Tc")]
public void CaskSecrets_EncodedMatchesDecoded(string encodedKey, CaskKeyKind expectedKeyKind, string expectedC2Id = "")
{
TestEncodedMatchedDecoded(encodedKey, expectedKeyKind, expectedC2Id);
Expand Down Expand Up @@ -138,7 +138,7 @@ public void CaskSecrets_IsCask_InvalidKey_InvalidCaskSignature()
expiryInFiveMinuteIncrements: 12 * 6, // 6 hours.
providerData: "-__-");
Span<char> keyChars = key.ToCharArray().AsSpan();
Span<char> caskSignatureBytes = "JQQJ".ToCharArray().AsSpan();
Span<char> caskSignatureBytes = "QJJQ".ToCharArray().AsSpan();

bool valid;

Expand Down Expand Up @@ -336,7 +336,7 @@ public void CaskSecrets_GenerateKey_DeterministicUsingMocks()
using Mock mockTimestamp = Cask.MockUtcNow(() => new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero));

string key = Cask.GenerateKey("TEST", "M", 0, "ABCD");
Assert.Equal("AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAJQQJTESTMPABAQEBAQEBAQEBAQEBAQEBAAAAAAAAABCD", key);
Assert.Equal("AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAQJJQTESTMPABAQEBAQEBAQEBAQEBAQEBAAAAAAAAABCD", key);
}

[Theory]
Expand Down

0 comments on commit 4345ab1

Please sign in to comment.