|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | +// See the LICENSE file in the project root for more information. |
| 4 | + |
| 5 | +using System; |
| 6 | +using System.Diagnostics; |
| 7 | +using System.Runtime.CompilerServices; |
| 8 | +using System.Security.Cryptography; |
| 9 | +using System.Text; |
| 10 | + |
| 11 | +#nullable enable |
| 12 | + |
| 13 | +namespace Microsoft.Data.SqlClient.AlwaysEncrypted; |
| 14 | + |
| 15 | +/// <summary> |
| 16 | +/// Represents metadata about the column master key, to be signed or verified by an enclave. |
| 17 | +/// </summary> |
| 18 | +/// <remarks> |
| 19 | +/// This metadata is a lower-case string which is laid out in the following format: |
| 20 | +/// <list type="number"> |
| 21 | +/// <item> |
| 22 | +/// Provider name. This is always <see cref="SqlColumnEncryptionCertificateStoreProvider.ProviderName"/>. |
| 23 | +/// </item> |
| 24 | +/// <item> |
| 25 | +/// Master key path. This will be in the format [LocalMachine|CurrentUser]/My/[SHA1 thumbprint]. |
| 26 | +/// </item> |
| 27 | +/// <item> |
| 28 | +/// Boolean to indicate whether the CMK supports enclave computations. This is either <c>true</c> or <c>false</c>. |
| 29 | +/// </item> |
| 30 | +/// </list> |
| 31 | +/// <para> |
| 32 | +/// This takes ownership of the RSA instance supplied to it, disposing of it when Dispose is called. |
| 33 | +/// </para> |
| 34 | +/// </remarks> |
| 35 | +internal readonly ref struct ColumnMasterKeyMetadata // : IDisposable |
| 36 | +{ |
| 37 | + private static readonly HashAlgorithmName s_hashAlgorithm = HashAlgorithmName.SHA256; |
| 38 | + |
| 39 | +#if NET |
| 40 | + [InlineArray(SHA256.HashSizeInBytes)] |
| 41 | + private struct Sha256Hash |
| 42 | + { |
| 43 | + private byte _elementTemplate; |
| 44 | + } |
| 45 | + |
| 46 | + private readonly Sha256Hash _hash; |
| 47 | +#else |
| 48 | + private readonly byte[] _hash; |
| 49 | +#endif |
| 50 | + private readonly RSA _rsa; |
| 51 | + |
| 52 | + // @TODO: SqlColumnEncryptionCertificateStoreProvider.SignMasterKeyMetadata and .VerifyMasterKeyMetadata should use this type. |
| 53 | + /// <summary> |
| 54 | + /// Represents metadata associated with a column master key, including its cryptographic hash, path, provider name, |
| 55 | + /// and enclave computation settings. |
| 56 | + /// </summary> |
| 57 | + /// <remarks> |
| 58 | + /// This struct is used to encapsulate the metadata required for signing or verifying a column master key. The metadata includes |
| 59 | + /// the provider name, the master key path, and whether enclave computations are allowed. The metadata is hashed using SHA-256 |
| 60 | + /// to ensure integrity. |
| 61 | + /// </remarks> |
| 62 | + /// <param name="rsa">The RSA cryptographic provider used for signing or verifying the metadata.</param> |
| 63 | + /// <param name="masterKeyPath">The path to the column master key. This must be a valid path in one of the following formats: |
| 64 | + /// <list type="bullet"> |
| 65 | + /// <item>[LocalMachine|CurrentUser]/My/[40-character SHA1 thumbprint]</item> |
| 66 | + /// <item>My/[40-character SHA1 thumbprint]</item> |
| 67 | + /// <item>[40-character SHA1 thumbprint]</item> |
| 68 | + /// </list> |
| 69 | + /// The path is case-insensitive and will be converted to lowercase for processing.</param> |
| 70 | + /// <param name="providerName">The name of the provider associated with the column master key.</param> |
| 71 | + /// <param name="allowEnclaveComputations">A value indicating whether enclave computations are allowed for this column master key.</param> |
| 72 | + public ColumnMasterKeyMetadata(RSA rsa, string masterKeyPath, string providerName, bool allowEnclaveComputations) |
| 73 | + { |
| 74 | + // Lay the column master key metadata out in memory. Then, calculate the hash of this metadata ready for signature or verification. |
| 75 | + // .NET Core supports Spans in more places, allowing us to allocate on the stack for better performance. It also supports the |
| 76 | + // SHA256.HashData method, which saves allocations compared to instantiating a SHA256 object and calling TransformFinalBlock. |
| 77 | + |
| 78 | + // By this point, we know that we have a valid certificate, so the path is valid. The longest valid masterKeyPath is in one of the formats: |
| 79 | + // * [LocalMachine|CurrentUser]/My/[40 character SHA1 thumbprint] |
| 80 | + // * My/[40 character SHA1 thumbprint] |
| 81 | + // * [40 character SHA1 thumbprint] |
| 82 | + // ProviderName is a constant string of length 23 characters, and allowEnclaveComputations' longest value is 5 characters long. This |
| 83 | + // implies a maximum length of 84 characters for the masterKeyMetadata string - and by extension, 168 bytes for the Unicode-encoded |
| 84 | + // byte array. This is small enough to allocate on the stack, but we fall back to allocating a new char/byte array in case those assumptions fail. |
| 85 | + // It also implies that when masterKeyPath is converted to its invariant lowercase value, it will be the same length (because it's |
| 86 | + // an ASCII string.) |
| 87 | + Debug.Assert(masterKeyPath.Length == masterKeyPath.ToLowerInvariant().Length); |
| 88 | + |
| 89 | + ReadOnlySpan<char> enclaveComputationSpan = (allowEnclaveComputations ? bool.TrueString : bool.FalseString).AsSpan(); |
| 90 | + int masterKeyMetadataLength = providerName.Length + masterKeyPath.Length + enclaveComputationSpan.Length; |
| 91 | + int byteCount; |
| 92 | + |
| 93 | +#if NET |
| 94 | + const int CharStackAllocationThreshold = 128; |
| 95 | + const int ByteStackAllocationThreshold = CharStackAllocationThreshold * sizeof(char); |
| 96 | + |
| 97 | + Span<char> masterKeyMetadata = masterKeyMetadataLength <= CharStackAllocationThreshold |
| 98 | + ? stackalloc char[CharStackAllocationThreshold].Slice(0, masterKeyMetadataLength) |
| 99 | + : new char[masterKeyMetadataLength]; |
| 100 | + Span<char> masterKeyMetadataSpan = masterKeyMetadata; |
| 101 | +#else |
| 102 | + char[] masterKeyMetadata = new char[masterKeyMetadataLength]; |
| 103 | + Span<char> masterKeyMetadataSpan = masterKeyMetadata.AsSpan(); |
| 104 | +#endif |
| 105 | + |
| 106 | + providerName.AsSpan().ToLowerInvariant(masterKeyMetadataSpan); |
| 107 | + masterKeyPath.AsSpan().ToLowerInvariant(masterKeyMetadataSpan.Slice(providerName.Length)); |
| 108 | + enclaveComputationSpan.ToLowerInvariant(masterKeyMetadataSpan.Slice(providerName.Length + masterKeyPath.Length)); |
| 109 | + byteCount = Encoding.Unicode.GetByteCount(masterKeyMetadata); |
| 110 | + |
| 111 | +#if NET |
| 112 | + Span<byte> masterKeyMetadataBytes = byteCount <= ByteStackAllocationThreshold |
| 113 | + ? stackalloc byte[ByteStackAllocationThreshold].Slice(0, byteCount) |
| 114 | + : new byte[byteCount]; |
| 115 | + |
| 116 | + Encoding.Unicode.GetBytes(masterKeyMetadata, masterKeyMetadataBytes); |
| 117 | + |
| 118 | + // Compute hash |
| 119 | + SHA256.HashData(masterKeyMetadataBytes, _hash); |
| 120 | +#else |
| 121 | + byte[] masterKeyMetadataBytes = Encoding.Unicode.GetBytes(masterKeyMetadata); |
| 122 | + using (SHA256 sha256 = SHA256.Create()) |
| 123 | + { |
| 124 | + // Compute hash |
| 125 | + sha256.TransformFinalBlock(masterKeyMetadataBytes, 0, masterKeyMetadataBytes.Length); |
| 126 | + _hash = sha256.Hash; |
| 127 | + } |
| 128 | +#endif |
| 129 | + |
| 130 | + _rsa = rsa; |
| 131 | + } |
| 132 | + |
| 133 | + /// <summary> |
| 134 | + /// Signs the current master key metadata using the RSA key associated with this instance. |
| 135 | + /// </summary> |
| 136 | + /// <returns> |
| 137 | + /// A byte array containing the digital signature of the master key metadata. |
| 138 | + /// </returns> |
| 139 | + /// <exception cref="CryptographicException">Thrown when the signing operation fails.</exception> |
| 140 | + public byte[] Sign() => |
| 141 | + _rsa.SignHash(_hash, s_hashAlgorithm, RSASignaturePadding.Pkcs1); |
| 142 | + |
| 143 | + /// <summary> |
| 144 | + /// Verifies the specified master key metadata signature against the computed hash using the RSA key associated with this instance. |
| 145 | + /// </summary> |
| 146 | + /// <param name="signature">The digital signature to verify. This must be a valid signature generated by <see cref="Sign"/>.</param> |
| 147 | + /// <returns> |
| 148 | + /// <see langword="true"/> if the signature is valid and matches the computed hash; otherwise, <see langword="false"/>. |
| 149 | + /// </returns> |
| 150 | + /// <exception cref="ArgumentNullException">Thrown when <paramref name="signature"/> is <see langword="null"/>.</exception>" |
| 151 | + public bool Verify(byte[] signature) => |
| 152 | + _rsa.VerifyHash(_hash, signature, s_hashAlgorithm, RSASignaturePadding.Pkcs1); |
| 153 | + |
| 154 | + /// <summary> |
| 155 | + /// Releases all resources used by this <see cref="ColumnMasterKeyMetadata"/>. |
| 156 | + /// </summary> |
| 157 | + /// <remarks> |
| 158 | + /// This method disposes the <see cref="RSA"/> instance used to construct this <see cref="ColumnMasterKeyMetadata" /> instance. |
| 159 | + /// </remarks> |
| 160 | + public void Dispose() => |
| 161 | + _rsa.Dispose(); |
| 162 | +} |
0 commit comments