namespace DSInternals.Common.Data { using System; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using DSInternals.Common.Data.Fido; using Newtonsoft.Json; using Newtonsoft.Json.Converters; /// /// This class represents a single AD/AAD key credential. /// /// /// In Active Directory, this structure is stored as the binary portion of the msDS-KeyCredentialLink DN-Binary attribute /// in the KEYCREDENTIALLINK_BLOB format. /// The Azure Active Directory Graph API represents this structure in JSON format. /// /// https://msdn.microsoft.com/en-us/library/mt220505.aspx [JsonObject(MemberSerialization = MemberSerialization.OptIn)] public class KeyCredential { /// /// Minimum length of the structure. /// private const int MinLength = sizeof(uint); // Version /// /// V0 structure alignment in bytes. /// private const ushort PackSize = 4; /// /// Defines the version of the structure. /// public KeyCredentialVersion Version { get; private set; } /// /// A SHA256 hash of the Value field of the RawKeyMaterial entry. /// /// /// Version 1 keys had a guid in this field instead if a hash. /// [JsonProperty("keyIdentifier", Order = 2)] public string Identifier { get; private set; } public bool IsWeak { get { var key = this.RSAPublicKey; return key.HasValue && key.Value.IsWeakKey(); } } [JsonProperty("usage", Order = 1)] [JsonConverter(typeof(StringEnumConverter))] public KeyUsage Usage { get; private set; } public string LegacyUsage { get; private set; } public KeySource Source { get; private set; } /// /// Key material of the credential. /// [JsonProperty("keyMaterial", Order = 3)] public byte[] RawKeyMaterial { get; private set; } public KeyMaterialFido FidoKeyMaterial { get { if (this.Usage == KeyUsage.FIDO) { var fidoCredString = System.Text.Encoding.UTF8.GetString(this.RawKeyMaterial, 0, this.RawKeyMaterial.Length); return JsonConvert.DeserializeObject(fidoCredString); } else return null; } } public ECParameters? ECPublicKey { get { var fidoKey = this.FidoKeyMaterial; if(fidoKey != null && fidoKey.AuthenticatorData.AttestedCredentialData.CredentialPublicKey.Type == COSE.KeyType.EC2) { return fidoKey.AuthenticatorData.AttestedCredentialData.CredentialPublicKey.ECDsa.ExportParameters(false); } else { return null; } } } public RSAParameters? RSAPublicKey { get { if(this.RawKeyMaterial == null) { return null; } // FIDO keys typically use ECC instead of RSA, but we try to extract the RSA key anyway: var fidoKey = this.FidoKeyMaterial; if (fidoKey != null && fidoKey.AuthenticatorData.AttestedCredentialData.CredentialPublicKey.Type == COSE.KeyType.RSA) { return fidoKey.AuthenticatorData.AttestedCredentialData.CredentialPublicKey.RSA.ExportParameters(false); } if(this.Usage == KeyUsage.NGC || this.Usage == KeyUsage.STK) { // The RSA public key can be stored in at least 3 different formats. if (this.RawKeyMaterial.IsBCryptRSAPublicKeyBlob()) { // This public key is in DER format. This is typically true for device/computer keys. return this.RawKeyMaterial.ImportRSAPublicKeyBCrypt(); } else if(this.RawKeyMaterial.IsTPM20PublicKeyBlob()) { // This public key is encoded as PCP_KEY_BLOB_WIN8. This is typically true for device keys protected by TPM. // The PCP_KEY_BLOB_WIN8 structure is not yet supported by DSInternals. return null; } else if(this.RawKeyMaterial.IsDERPublicKeyBlob()) { // This public key is encoded as BCRYPT_RSAKEY_BLOB. This is typically true for user keys. return this.RawKeyMaterial.ImportRSAPublicKeyDER(); } } // Other key usages probably do not contain any public keys. return null; } } public string RSAModulus { get { var publicKey = this.RSAPublicKey; return publicKey.HasValue ? Convert.ToBase64String(publicKey.Value.Modulus) : null; } } [JsonProperty("customKeyInformation", Order = 6)] [JsonConverter(typeof(CustomKeyInformationConverter))] public CustomKeyInformation CustomKeyInfo { get; private set; } [JsonProperty("deviceId", Order = 5)] public Guid? DeviceId { get; private set; } /// /// The approximate time this key was created. /// [JsonProperty("creationTime", Order = 4)] public DateTime CreationTime { get; private set; } /// /// The approximate time this key was last used. /// public DateTime? LastLogonTime { get; private set; } /// /// Distinguished name of the AD object that holds this key credential. /// public string HolderDN { get; private set; } /// /// Gets the FIDO AAGUID. For JSON deserialization only. /// [JsonProperty("fidoAaGuid", Order = 7)] private Guid? FidoAaGuid { get { var fido = this.FidoKeyMaterial; if (fido != null && fido.AuthenticatorData != null && fido.AuthenticatorData.AttestedCredentialData != null) { return fido.AuthenticatorData.AttestedCredentialData.AaGuid; } else { return null; } } } /// /// Gets the FIDO authenticator version. For JSON deserialization only. /// [JsonProperty("fidoAuthenticatorVersion", Order = 8)] private string FidoAuthenticatorVersion { get { return null; } } /// /// Gets a list of thumbprints of FIDO Attestation Certificates. For JSON deserialization only. /// [JsonProperty("fidoAttestationCertificates", Order = 9)] private string[] FidoAttestationCertificates { get { var fido = this.FidoKeyMaterial; if(fido != null && fido.AttestationCertificates != null) { return fido.AttestationCertificates.Cast().Select(cer => cer.Thumbprint.ToLowerInvariant()).ToArray(); } else { return new string[0]; } } } public KeyCredential(X509Certificate2 certificate, Guid? deviceId, string holderDN, DateTime? currentTime = null, bool isComputerKey = false) { Validator.AssertNotNull(certificate, nameof(certificate)); // Computer NGC keys are DER-encoded, while user NGC keys are encoded as BCRYPT_RSAKEY_BLOB. byte[] publicKey = isComputerKey ? certificate.ExportRSAPublicKeyDER() : certificate.ExportRSAPublicKeyBCrypt(); this.Initialize(publicKey, deviceId, holderDN, currentTime, isComputerKey); } public KeyCredential(byte[] publicKey, Guid? deviceId, string holderDN, DateTime? currentTime = null, bool isComputerKey = false) { Validator.AssertNotNull(publicKey, nameof(publicKey)); this.Initialize(publicKey, deviceId, holderDN, currentTime, isComputerKey); } private void Initialize(byte[] publicKey, Guid? deviceId, string holderDN, DateTime? currentTime, bool isComputerKey) { // Prodess holder DN Validator.AssertNotNullOrEmpty(holderDN, nameof(holderDN)); this.HolderDN = holderDN; // Initialize the Key Credential based on requirements stated in MS-KPP Processing Details: this.Version = KeyCredentialVersion.Version2; this.Identifier = ComputeKeyIdentifier(publicKey, this.Version); this.CreationTime = currentTime.HasValue ? currentTime.Value.ToUniversalTime() : DateTime.UtcNow; this.RawKeyMaterial = publicKey; this.Usage = KeyUsage.NGC; this.Source = KeySource.AD; this.DeviceId = deviceId; // Computer NGC keys have to meet some requirements to pass the validated write // The CustomKeyInformation entry is not present. // The KeyApproximateLastLogonTimeStamp entry is not present. if (!isComputerKey) { this.LastLogonTime = this.CreationTime; this.CustomKeyInfo = new CustomKeyInformation(KeyFlags.None); } } public KeyCredential(byte[] blob, string holderDN) { // Input validation Validator.AssertNotNull(blob, nameof(blob)); Validator.AssertMinLength(blob, MinLength, nameof(blob)); Validator.AssertNotNullOrEmpty(holderDN, nameof(holderDN)); // Init this.HolderDN = holderDN; // Parse binary input using (var stream = new MemoryStream(blob, false)) { using (var reader = new BinaryReader(stream)) { this.Version = (KeyCredentialVersion) reader.ReadUInt32(); // Read all entries corresponding to the KEYCREDENTIALLINK_ENTRY structure: do { // A 16-bit unsigned integer that specifies the length of the Value field. ushort length = reader.ReadUInt16(); // An 8-bit unsigned integer that specifies the type of data that is stored in the Value field. KeyCredentialEntryType entryType = (KeyCredentialEntryType) reader.ReadByte(); // A series of bytes whose size and meaning are defined by the Identifier field. byte[] value = reader.ReadBytes(length); if(this.Version == KeyCredentialVersion.Version0) { // Data used to be aligned to 4B in this legacy format. int paddingLength = (PackSize - length % PackSize) % PackSize; reader.ReadBytes(paddingLength); } // Now parse the value of the current entry based on its type: switch (entryType) { case KeyCredentialEntryType.KeyID: this.Identifier = ConvertFromBinaryIdentifier(value, this.Version); break; case KeyCredentialEntryType.KeyHash: // We do not need to validate the integrity of the data by the hash break; case KeyCredentialEntryType.KeyMaterial: this.RawKeyMaterial = value; break; case KeyCredentialEntryType.KeyUsage: if(length == sizeof(byte)) { // This is apparently a V2 structure this.Usage = (KeyUsage)value[0]; } else { // This is a legacy structure that contains a string-encoded key usage instead of enum. this.LegacyUsage = System.Text.Encoding.UTF8.GetString(value); } break; case KeyCredentialEntryType.KeySource: this.Source = (KeySource)value[0]; break; case KeyCredentialEntryType.DeviceId: this.DeviceId = new Guid(value); break; case KeyCredentialEntryType.CustomKeyInformation: this.CustomKeyInfo = new CustomKeyInformation(value); break; case KeyCredentialEntryType.KeyApproximateLastLogonTimeStamp: this.LastLogonTime = ConvertFromBinaryTime(value, this.Source, this.Version); break; case KeyCredentialEntryType.KeyCreationTime: this.CreationTime = ConvertFromBinaryTime(value, this.Source, this.Version); break; default: // Unknown entry type. We will just ignore it. break; } } while (reader.BaseStream.Position != reader.BaseStream.Length); } } } /// /// This constructor is only used for JSON deserialization. /// [JsonConstructor] private KeyCredential() { this.Source = KeySource.AzureAD; this.Version = KeyCredentialVersion.Version2; } public override string ToString() { return String.Format( "Id: {0}, Source: {1}, Version: {2}, Usage: {3}, CreationTime: {4}", this.Identifier, this.Source, this.Version, this.Usage, this.CreationTime); } public byte[] ToByteArray() { // Note that we do not support the legacy V1 format. // Serialize properties 3-9 first, as property 2 must contain their hash: byte[] binaryProperties; using (var propertyStream = new MemoryStream()) { using (var propertyWriter = new BinaryWriter(propertyStream)) { // Key Material propertyWriter.Write((ushort)this.RawKeyMaterial.Length); propertyWriter.Write((byte)KeyCredentialEntryType.KeyMaterial); propertyWriter.Write(this.RawKeyMaterial); // Key Usage propertyWriter.Write((ushort)sizeof(KeyUsage)); propertyWriter.Write((byte)KeyCredentialEntryType.KeyUsage); propertyWriter.Write((byte)this.Usage); // Key Source propertyWriter.Write((ushort)sizeof(KeySource)); propertyWriter.Write((byte)KeyCredentialEntryType.KeySource); propertyWriter.Write((byte)this.Source); // Device ID if(this.DeviceId.HasValue) { byte[] binaryGuid = this.DeviceId.Value.ToByteArray(); propertyWriter.Write((ushort)binaryGuid.Length); propertyWriter.Write((byte)KeyCredentialEntryType.DeviceId); propertyWriter.Write(binaryGuid); } // Custom Key Information if(this.CustomKeyInfo != null) { byte[] binaryKeyInfo = this.CustomKeyInfo.ToByteArray(); propertyWriter.Write((ushort)binaryKeyInfo.Length); propertyWriter.Write((byte)KeyCredentialEntryType.CustomKeyInformation); propertyWriter.Write(binaryKeyInfo); } // Last Logon Time if(this.LastLogonTime.HasValue) { byte[] binaryLastLogonTime = ConvertToBinaryTime(this.LastLogonTime.Value, this.Source, this.Version); propertyWriter.Write((ushort)binaryLastLogonTime.Length); propertyWriter.Write((byte)KeyCredentialEntryType.KeyApproximateLastLogonTimeStamp); propertyWriter.Write(binaryLastLogonTime); } // Creation Time byte[] binaryCreationTime = ConvertToBinaryTime(this.CreationTime, this.Source, this.Version); propertyWriter.Write((ushort)binaryCreationTime.Length); propertyWriter.Write((byte)KeyCredentialEntryType.KeyCreationTime); propertyWriter.Write(binaryCreationTime); } binaryProperties = propertyStream.ToArray(); } using (var blobStream = new MemoryStream()) { using (var blobWriter = new BinaryWriter(blobStream)) { // Version blobWriter.Write((uint)this.Version); // Key Identifier byte[] binaryKeyId = ConvertToBinaryIdentifier(this.Identifier, this.Version); blobWriter.Write((ushort)binaryKeyId.Length); blobWriter.Write((byte)KeyCredentialEntryType.KeyID); blobWriter.Write(binaryKeyId); // Key Hash byte[] keyHash = ComputeHash(binaryProperties); blobWriter.Write((ushort)keyHash.Length); blobWriter.Write((byte)KeyCredentialEntryType.KeyHash); blobWriter.Write(keyHash); // Append the remaining entries blobWriter.Write(binaryProperties); } return blobStream.ToArray(); } } public string ToDNWithBinary() { return new DNWithBinary(this.HolderDN, this.ToByteArray()).ToString(); } public string ToJson() { return JsonConvert.SerializeObject(this); } public static KeyCredential ParseDNBinary(string dnWithBinary) { Validator.AssertNotNullOrEmpty(dnWithBinary, nameof(dnWithBinary)); var parsed = DNWithBinary.Parse(dnWithBinary); return new KeyCredential(parsed.Binary, parsed.DistinguishedName); } public static KeyCredential ParseJson(string jsonData) { if(String.IsNullOrEmpty(jsonData)) { return null; } else { return JsonConvert.DeserializeObject(jsonData); } } private static DateTime ConvertFromBinaryTime(byte[] binaryTime, KeySource source, KeyCredentialVersion version) { long timeStamp = BitConverter.ToInt64(binaryTime, 0); // AD and AAD use a different time encoding. switch (version) { case KeyCredentialVersion.Version0: return new DateTime(timeStamp); case KeyCredentialVersion.Version1: return DateTime.FromBinary(timeStamp); case KeyCredentialVersion.Version2: default: return source == KeySource.AD ? DateTime.FromFileTime(timeStamp) : DateTime.FromBinary(timeStamp); } } private static byte[] ConvertToBinaryTime(DateTime time, KeySource source, KeyCredentialVersion version) { long timeStamp; switch (version) { case KeyCredentialVersion.Version0: timeStamp = time.Ticks; break; case KeyCredentialVersion.Version1: timeStamp = time.ToBinary(); break; case KeyCredentialVersion.Version2: default: timeStamp = source == KeySource.AD ? time.ToFileTime() : time.ToBinary(); break; } return BitConverter.GetBytes(timeStamp); } private static byte[] ComputeHash(byte[] data) { using (var sha256 = new SHA256Managed()) { return sha256.ComputeHash(data); } } private static string ComputeKeyIdentifier(byte[] keyMaterial, KeyCredentialVersion version) { byte[] binaryId = ComputeHash(keyMaterial); return ConvertFromBinaryIdentifier(binaryId, version); } private static string ConvertFromBinaryIdentifier(byte[] binaryId, KeyCredentialVersion version) { switch (version) { case KeyCredentialVersion.Version0: case KeyCredentialVersion.Version1: return binaryId.ToHex(true); case KeyCredentialVersion.Version2: default: return Convert.ToBase64String(binaryId); } } private static byte[] ConvertToBinaryIdentifier(string keyIdentifier, KeyCredentialVersion version) { switch (version) { case KeyCredentialVersion.Version0: case KeyCredentialVersion.Version1: return keyIdentifier.HexToBinary(); case KeyCredentialVersion.Version2: default: return Convert.FromBase64String(keyIdentifier); } } } }