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);
}
}
}
}