-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This implements C3ID computation that we will need soon. The implementation differs from the one in https://github.com/microsoft/security-utilities and this seeks to become the new standard. Changes: - There is no use of "Microsoft" - There is no byte array to hex conversion in the intermediate computation - Optimized to avoid allocations - Raw value is truncated to 12 bytes instead of 15 - "C3ID" is prefixed to the canonical representation in base64 Tests include a naive, unoptimized implementation that is easier to understand, and we check that both implementations match. We also check that we produce the same deterministic value for a sampling of hard-coded test input. This change also fixes edge cases with empty span input in encoding polyfills.
- Loading branch information
Showing
8 changed files
with
308 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.Security.Cryptography; | ||
using System.Text; | ||
|
||
using static CommonAnnotatedSecurityKeys.Limits; | ||
|
||
namespace CommonAnnotatedSecurityKeys; | ||
|
||
/// <summary> | ||
/// Cross-Company Correlating Id (C3ID) a 12-byte value used to correlate a | ||
/// high-entropy keys with other data. The canonical textual representation is | ||
/// base64 encoded and prefixed with "C3ID". | ||
/// </summary> | ||
public static class CrossCompanyCorrelatingId | ||
{ | ||
/// <summary> | ||
/// The size of a C3ID in raw bytes. | ||
/// </summary> | ||
public const int RawSizeInBytes = 12; | ||
|
||
/// <summary> | ||
/// The byte sequence prepended to the input for the first SHA256 hash. It | ||
/// is defined as the UTF-8 encoding of "C3ID". | ||
/// </summary> | ||
private static ReadOnlySpan<byte> Prefix => "C3ID"u8; | ||
|
||
/// <summary> | ||
/// The byte sequence prepended to the to the output of the | ||
/// base64-encoding. It is defined as the base64-decoding of "C3ID". This | ||
/// results in all canonical base64 encoded C3IDs starting with "C3ID". | ||
/// </summary> | ||
private static ReadOnlySpan<byte> PrefixBase64Decoded => [0x0B, 0x72, 0x03]; | ||
|
||
/// <summary> | ||
/// Computes the C3ID for the given text in canonical textual form. | ||
/// </summary> | ||
public static string Compute(string text) | ||
{ | ||
ThrowIfNullOrEmpty(text); | ||
Span<byte> bytes = stackalloc byte[PrefixBase64Decoded.Length + RawSizeInBytes]; | ||
PrefixBase64Decoded.CopyTo(bytes); | ||
ComputeRaw(text, bytes[PrefixBase64Decoded.Length..]); | ||
return Convert.ToBase64String(bytes); | ||
} | ||
|
||
/// <summary> | ||
/// Computes the raw C3ID bytes for the given text and writes them to the | ||
/// destination span. | ||
/// </summary> | ||
public static void ComputeRaw(string text, Span<byte> destination) | ||
{ | ||
ThrowIfNull(text); | ||
ComputeRaw(text.AsSpan(), destination); | ||
} | ||
|
||
/// <summary> | ||
/// Computes the raw C3ID bytes for the given UTF-16 encoded text sequence | ||
/// and writes them to the destination span. | ||
/// </summary> | ||
public static void ComputeRaw(ReadOnlySpan<char> text, Span<byte> destination) | ||
{ | ||
ThrowIfEmpty(text); | ||
ThrowIfDestinationTooSmall(destination, RawSizeInBytes); | ||
|
||
int byteCount = Encoding.UTF8.GetByteCount(text); | ||
Span<byte> textUtf8 = byteCount <= MaxStackAlloc ? stackalloc byte[byteCount] : new byte[byteCount]; | ||
Encoding.UTF8.GetBytes(text, textUtf8); | ||
ComputeRawUtf8(textUtf8, destination); | ||
} | ||
|
||
/// <summary> | ||
/// Computes the raw C3ID bytes for the given UTF-8 encoded text sequence | ||
/// and writes them to the destination span. | ||
/// </summary>> | ||
public static void ComputeRawUtf8(ReadOnlySpan<byte> textUtf8, Span<byte> destination) | ||
{ | ||
ThrowIfEmpty(textUtf8); | ||
ThrowIfDestinationTooSmall(destination, RawSizeInBytes); | ||
|
||
// Produce input for second hash: "C3ID"u8 + SHA256(text) | ||
Span<byte> input = stackalloc byte[Prefix.Length + SHA256.HashSizeInBytes]; | ||
Prefix.CopyTo(input); | ||
SHA256.HashData(textUtf8, input[Prefix.Length..]); | ||
|
||
// Perform second hash, truncate, and copy to destination. | ||
Span<byte> sha = stackalloc byte[SHA256.HashSizeInBytes]; | ||
SHA256.HashData(input, sha); | ||
sha[..RawSizeInBytes].CopyTo(destination); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.Security.Cryptography; | ||
using System.Text; | ||
|
||
using Xunit; | ||
|
||
namespace CommonAnnotatedSecurityKeys.Tests; | ||
|
||
public class CrossCompanyCorrelatingIdTests | ||
{ | ||
[Theory] | ||
[InlineData("Hello world", "C3IDnw4dY6uIibYownZw")] | ||
[InlineData("😁", "C3IDF8FaWr4yMPcwOOxM")] | ||
[InlineData("y_-KPF3BQb2-VHZeqrp28c6dgiL9y7H9TRJmQ5jJe9OvJQQJTESTBAU4AAB5mIhC", "C3IDKx9aukbRgOnPEyeu")] | ||
[InlineData("Kq03wDtdCGWvs3sPgbH84H5MDADIJMZEERRhUN73CaGBJQQJTESTBAU4AADqe9ge", "C3IDO93RBPyuaA6ZRK8+")] | ||
public void C3Id_Basic(string text, string expected) | ||
{ | ||
string actual = ComputeC3Id(text); | ||
Assert.Equal(expected, actual); | ||
} | ||
|
||
[Fact] | ||
public void C3Id_LargeText() | ||
{ | ||
string actual = ComputeC3Id(text: new string('x', 300)); | ||
Assert.Equal("C3IDs+pSKJ1FmRW+7EZk", actual); | ||
} | ||
|
||
[Fact] | ||
public void C3Id_Null_Throws() | ||
{ | ||
Assert.Throws<ArgumentNullException>("text", () => CrossCompanyCorrelatingId.Compute(null!)); | ||
} | ||
|
||
[Fact] | ||
public void C3Id_Empty_Throws() | ||
{ | ||
Assert.Throws<ArgumentException>("text", () => CrossCompanyCorrelatingId.Compute("")); | ||
} | ||
|
||
[Fact] | ||
public void C3Id_EmptyRaw_Throws() | ||
{ | ||
byte[] destination = new byte[CrossCompanyCorrelatingId.RawSizeInBytes]; | ||
Assert.Throws<ArgumentException>("text", () => CrossCompanyCorrelatingId.ComputeRaw("", destination)); | ||
} | ||
|
||
[Fact] | ||
public void C3Id_EmptyRawSpan_Throws() | ||
{ | ||
byte[] destination = new byte[CrossCompanyCorrelatingId.RawSizeInBytes]; | ||
Assert.Throws<ArgumentException>("text", () => CrossCompanyCorrelatingId.ComputeRaw([], destination)); | ||
} | ||
|
||
[Fact] | ||
public void C3Id_EmptyRawUtf8_Throws() | ||
{ | ||
byte[] destination = new byte[CrossCompanyCorrelatingId.RawSizeInBytes]; | ||
Assert.Throws<ArgumentException>("textUtf8", () => CrossCompanyCorrelatingId.ComputeRawUtf8([], destination)); | ||
} | ||
|
||
[Fact] | ||
public void C3Id_DestinationTooSmall_Throws() | ||
{ | ||
byte[] destination = new byte[CrossCompanyCorrelatingId.RawSizeInBytes - 1]; | ||
Assert.Throws<ArgumentException>( | ||
"destination", | ||
() => CrossCompanyCorrelatingId.ComputeRaw("test", destination)); | ||
} | ||
|
||
[Fact] | ||
public void C3Id_DestinationTooSmallUtf8_Throws() | ||
{ | ||
byte[] destination = new byte[CrossCompanyCorrelatingId.RawSizeInBytes - 1]; | ||
Assert.Throws<ArgumentException>( | ||
"destination", | ||
() => CrossCompanyCorrelatingId.ComputeRawUtf8("test"u8, destination)); | ||
} | ||
|
||
private static string ComputeC3Id(string text) | ||
{ | ||
string reference = ReferenceCrossCompanyCorrelatingId.Compute(text); | ||
string actual = CrossCompanyCorrelatingId.Compute(text); | ||
|
||
Assert.True( | ||
actual == reference, | ||
$""" | ||
Actual implementation did not match reference implementation for '{text}'. | ||
reference: {reference} | ||
actual: {actual} | ||
"""); | ||
|
||
return actual; | ||
} | ||
|
||
/// <summary> | ||
/// A trivial reference implementation of C3ID that is easy to understand, | ||
/// but not optimized for performance. We compare this to the production | ||
/// implementation to ensure that it remains equivalent to this. | ||
/// </summary> | ||
private static class ReferenceCrossCompanyCorrelatingId | ||
{ | ||
public static string Compute(string text) | ||
{ | ||
// Compute the SHA-256 hash of the UTF8-encoded text | ||
Span<byte> hash = SHA256.HashData(Encoding.UTF8.GetBytes(text)); | ||
|
||
// Prefix the result with "C3ID" UTF-8 bytes and hash again | ||
hash = SHA256.HashData([.. "C3ID"u8, .. hash]); | ||
|
||
// Truncate to 12 bytes | ||
hash = hash[..12]; | ||
|
||
// Convert to base64 and prepend "C3ID" | ||
return "C3ID" + Convert.ToBase64String(hash); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters