diff --git a/Docs/Examples/Enroll YubiKey FIDO2 against demo.yubico.com.md b/Docs/Examples/Enroll YubiKey FIDO2 against demo.yubico.com.md new file mode 100644 index 0000000..41ea1b0 --- /dev/null +++ b/Docs/Examples/Enroll YubiKey FIDO2 against demo.yubico.com.md @@ -0,0 +1,47 @@ +## UNDER CONSTRUCTION ## +# Lets start by creating the information prior to creation. +$username = "powershellYK$($(new-guid).tostring().Replace('-',''))" +$password = (get-date -Format 'yyyy-MM-dd') +$site = "demo.yubico.com" + +# Now that we have a user and password, lets create the user in the Yubico playground +$createUser = @{ +'displayName'='powershellYK Demo'; +'namespace'='playground'; +'username'=$username; +'password'=$password +} | ConvertTo-JSON + +$userCreation = Invoke-RestMethod -Method Post -SessionVariable session -Uri "https://$site/api/v1/user" -Body $createUser -ContentType 'application/json' + +#Lets begin registering the YubiKey +$registerBeginBody = @{'authenticatorAttachment' = 'cross-platform'; 'residentKey' = $true} | ConvertTo-JSON +$registerBeginReturn = Invoke-RestMethod -Method Post -WebSession $session -Uri "https://$site/api/v1/user/$($userCreation.data.uuid)/webauthn/register-begin" -Body $registerBeginBody -ContentType 'application/json' + +$userEntity = [Yubico.YubiKey.Fido2.UserEntity]::new([system.convert]::FromBase64String($registerBeginReturn.data.publicKey.user.id.'$base64')) +$userEntity.Name = $registerBeginReturn.data.publicKey.user.name +$userentity.DisplayName = $registerBeginReturn.data.publicKey.user.displayname + + +$out = New-YubiKeyFIDO2Credential -RelyingPartyID $registerBeginReturn.data.publicKey.rp.id -RelyingPartyName $registerBeginReturn.data.publicKey.rp.name -Discoverable $true -Challange $registerBeginReturn.data.publicKey.challenge.'$base64' -UserEntity $userEntity + +# This Data is lost by the SDK so we need to build it backup. Wonder if this is where it breaks. +$a = [powershellYK.FIDO2.CredentialData]::new($out) +#[system.convert]::ToBase64String($a.w3cEncoded()) + +$clientDataJSON = @{ + 'type' = 'webauthn.create'; + 'challenge' = $registerBeginReturn.data.publicKey.challenge.'$base64' -replace '\+', '-' -replace '/', '_' -replace '=',''; + 'origin' = "https://$site"; + 'crossOrigin' = $false +} | ConvertTo-JSON -Compress + +# Lets send stuff back to demo.yubico.com to enable the security key +$registerFinishBody = @{ + 'requestId' = $registerBeginReturn.data.requestId; + 'attestation' = @{ + 'attestationObject' = @{'$base64'=[system.convert]::ToBase64String($a.w3cEncoded())}; + 'clientDataJSON' = @{'$base64'=[system.convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($clientDataJSON))} + } +} | ConvertTo-JSON -Compress +$registerFinishReturn = Invoke-RestMethod -Method Post -WebSession $session -Uri "https://$site/api/v1/user/$($userCreation.data.uuid)/webauthn/register-finish" -Body $registerFinishBody -ContentType 'application/json' diff --git a/Module/Cmdlets/FIDO2/GetYubikeyFIDO2Credential.cs b/Module/Cmdlets/FIDO2/GetYubikeyFIDO2Credential.cs index 31337f8..67ce98b 100644 --- a/Module/Cmdlets/FIDO2/GetYubikeyFIDO2Credential.cs +++ b/Module/Cmdlets/FIDO2/GetYubikeyFIDO2Credential.cs @@ -67,7 +67,7 @@ protected override void ProcessRecord() foreach (CredentialUserInfo user in relayCredentials) { - Credential credential = new Credential(RPId: relyingParty.Id, UserName: user.User.Name, DisplayName: user.User.DisplayName, CredentialID: user.CredentialId); + Credential credential = new Credential(relyingParty: relyingParty, credentialUserInfo: user); WriteObject(credential); } } diff --git a/Module/Cmdlets/FIDO2/NewFIDO2Credential.cs b/Module/Cmdlets/FIDO2/NewFIDO2Credential.cs new file mode 100644 index 0000000..b5563ce --- /dev/null +++ b/Module/Cmdlets/FIDO2/NewFIDO2Credential.cs @@ -0,0 +1,106 @@ +using System.Management.Automation; +using Yubico.YubiKey; +using Yubico.YubiKey.Fido2; +using System.Linq; +using powershellYK.support; +using System.Formats.Cbor; +using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext; +using Yubico.YubiKey.Cryptography; +using powershellYK.FIDO2; + +namespace powershellYK.Cmdlets.Fido +{ + [Cmdlet(VerbsCommon.New, "YubiKeyFIDO2Credential", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + public class NewYubikeyFIDO2CredentialCmdlet : PSCmdlet + { + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Specify which relayingParty (site) this credential is regards to.", ParameterSetName = "UserEntity-HostData")] + public required string RelyingPartyID { private get; set; } + [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Friendlyname for the relayingParty.", ParameterSetName = "UserEntity-HostData")] + public required string RelyingPartyName { private get; set; } + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Username to create credental for.", ParameterSetName = "UserData-HostData")] + public required string Username { private get; set; } + [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "UserDisplayName to create credental for.", ParameterSetName = "UserData-HostData")] + public string? UserDisplayName { private get; set; } + [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "UserID.", ParameterSetName = "UserData-HostData")] + public byte[]? UserID { private get; set; } + + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challange.")] + public required Challenge Challange { private get; set; } + [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Should this credential be discoverable.")] + public bool Discoverable { private get; set; } = true; + + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Supply the user entity in complete form.", ParameterSetName = "UserEntity-HostData")] + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Supply the user entity in complete form.", ParameterSetName = "UserEntity-RelyingParty")] + public UserEntity? UserEntity { get; set; } = new UserEntity(new byte[] { 0, 0 }); + protected override void BeginProcessing() + { + // If no FIDO2 PIN exists, we need to connect to the FIDO2 application + if (YubiKeyModule._fido2PIN is null) + { + WriteDebug("No FIDO2 session has been authenticated, calling Connect-YubikeyFIDO2..."); + var myPowersShellInstance = PowerShell.Create(RunspaceMode.CurrentRunspace).AddCommand("Connect-YubikeyFIDO2"); + if (this.MyInvocation.BoundParameters.ContainsKey("InformationAction")) + { + myPowersShellInstance = myPowersShellInstance.AddParameter("InformationAction", this.MyInvocation.BoundParameters["InformationAction"]); + } + myPowersShellInstance.Invoke(); + if (YubiKeyModule._fido2PIN is null) + { + throw new Exception("Connect-YubikeyFIDO2 failed to connect to the FIDO2 applet!"); + } + } + + + if (Windows.IsRunningAsAdministrator() == false) + { + throw new Exception("FIDO access on Windows requires running as Administrator."); + } + } + + protected override void ProcessRecord() + { + WriteWarning("This cmdlet is still in development and may not work as expected."); + if (UserDisplayName is null) + { + UserDisplayName = Username; + } + using (var fido2Session = new Fido2Session((YubiKeyDevice)YubiKeyModule._yubikey!)) + { + fido2Session.KeyCollector = YubiKeyModule._KeyCollector.YKKeyCollectorDelegate; + + //var randomObject = CryptographyProviders.RngCreator(); + //byte[] randomBytes = new byte[32]; + //randomObject.GetBytes(UserID); + var userId = new ReadOnlyMemory(UserID); + var relayingParty = new RelyingParty(RelyingPartyID) { Name = RelyingPartyName }; + + if (UserEntity is null) + { + UserEntity = new UserEntity(userId) + { + Name = Username, + DisplayName = UserDisplayName ?? Username, + }; + } + + ReadOnlyMemory clientDataHash = Challange.ToByte().AsMemory(); + + + var make = new MakeCredentialParameters(relayingParty, UserEntity); + if (Discoverable) + { + make.AddOption("rk", true); + } + + if (fido2Session.AuthenticatorInfo.IsExtensionSupported("hmac-secret")) + { + // make.AddHmacSecretExtension(fido2Session.AuthenticatorInfo); + } + + make.ClientDataHash = clientDataHash; + MakeCredentialData returnvalue = fido2Session.MakeCredential(make); + WriteObject(returnvalue); + } + } + } +} diff --git a/Module/Cmdlets/PIV/BuildYubiKeyPIVCertificateSigningRequest.cs b/Module/Cmdlets/PIV/BuildYubiKeyPIVCertificateSigningRequest.cs index 4c396d3..4df059d 100644 --- a/Module/Cmdlets/PIV/BuildYubiKeyPIVCertificateSigningRequest.cs +++ b/Module/Cmdlets/PIV/BuildYubiKeyPIVCertificateSigningRequest.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography; using Yubico.YubiKey.Sample.PivSampleCode; using powershellYK.PIV; +using powershellYK.support.transform; namespace powershellYK.Cmdlets.PIV @@ -21,6 +22,7 @@ public class BuildYubiKeyPIVCertificateSigningRequestCmdlet : Cmdlet [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Subject name of certificate")] public string Subjectname { get; set; } = "CN=SubjectName to be supplied by Server,O=Fake"; + [TransformPath()] [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Save CSR as file")] public string? OutFile { get; set; } = null; diff --git a/Module/Cmdlets/PIV/BuildYubikeyPIVSignCertificate.cs b/Module/Cmdlets/PIV/BuildYubikeyPIVSignCertificate.cs index 82e28f2..ae4fff4 100644 --- a/Module/Cmdlets/PIV/BuildYubikeyPIVSignCertificate.cs +++ b/Module/Cmdlets/PIV/BuildYubikeyPIVSignCertificate.cs @@ -22,6 +22,7 @@ public class BuildYubikeySignedCertificateCommand : Cmdlet [ValidateSet("SHA1", "SHA256", "SHA384", "SHA512", IgnoreCase = true)] [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "HashAlgoritm")] public HashAlgorithmName HashAlgorithm { get; set; } = HashAlgorithmName.SHA256; + [TransformPath()] [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Output file")] public string? OutFile { get; set; } = null; diff --git a/Module/Cmdlets/PIV/RemoveYubikeyPIVCertificate.cs b/Module/Cmdlets/PIV/RemoveYubikeyPIVCertificate.cs new file mode 100644 index 0000000..e532df4 --- /dev/null +++ b/Module/Cmdlets/PIV/RemoveYubikeyPIVCertificate.cs @@ -0,0 +1,53 @@ +using System.Management.Automation; +using Yubico.YubiKey; +using Yubico.YubiKey.Piv; +using powershellYK.support.validators; +using powershellYK.PIV; +using System.Security.Cryptography.X509Certificates; + +namespace powershellYK.Cmdlets.PIV +{ + [Cmdlet(VerbsCommon.Remove, "YubiKeyPIVCertificate", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + public class RemoveYubiKeyPIVCertificateCmdlet : Cmdlet + { + [ArgumentCompletions("\"PIV Authentication\"", "\"Digital Signature\"", "\"Key Management\"", "\"Card Authentication\"", "0x9a", "0x9c", "0x9d", "0x9e")] + [ValidateYubikeyPIVSlot(DontAllowAttestion = true)] + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "What slot to remove a key from")] + public PIVSlot Slot { get; set; } + protected override void BeginProcessing() + { + YubiKeyModule.ConnectYubikey(); + } + protected override void ProcessRecord() + { + using (var pivSession = new PivSession((YubiKeyDevice)YubiKeyModule._yubikey!)) + { + pivSession.KeyCollector = YubiKeyModule._KeyCollector.YKKeyCollectorDelegate; + + X509Certificate2? currentCertificate; + + try + { + currentCertificate = pivSession.GetCertificate(Slot); + } + catch + { + throw new Exception($"No certificate found in PIV slot {Slot}."); + } + + if (ShouldProcess($"Certificate in slot {Slot}, subjectname: '{currentCertificate.SubjectName}'", "Remove")) + { + throw new NotImplementedException("Remove-YubiKeyPIVCertificate not implemented."); + try + { + // This will throw an exception if no key is found in the slot + } + catch + { + } + } + } + } + + } +} \ No newline at end of file diff --git a/Module/powershellYK.psd1 b/Module/powershellYK.psd1 index ca7f837..1950a68 100644 --- a/Module/powershellYK.psd1 +++ b/Module/powershellYK.psd1 @@ -84,6 +84,7 @@ CmdletsToExport = @( 'Enable-YubiKeyFIDO2EnterpriseAttestation', 'Get-YubiKeyFIDO2', 'Get-YubiKeyFIDO2Credential', + 'New-YubiKeyFIDO2Credential', 'Remove-YubiKeyFIDO2Credential' 'Set-YubiKeyFIDO2', 'Set-YubiKeyFIDO2PIN', diff --git a/Module/types/FIDO2-Credentials.cs b/Module/types/FIDO2-Credentials.cs index e5c2c80..dfd8c9c 100644 --- a/Module/types/FIDO2-Credentials.cs +++ b/Module/types/FIDO2-Credentials.cs @@ -7,19 +7,20 @@ namespace powershellYK.FIDO2 { public class Credential { - public string? DisplayName { get; private set; } - public string? UserName { get; private set; } - public string? RPId { get; private set; } + public string? DisplayName { get { return this.CredentialUserInfo.User.DisplayName; } } + public string? UserName { get { return this.CredentialUserInfo.User.Name; } } + public string? RPId { get { return this.RelyingParty.Id; } } public powershellYK.FIDO2.CredentialID CredentialID { get; private set; } [Hidden] - public CoseKey? coseKey { get; set; } + public RelyingParty RelyingParty { get; private set; } + [Hidden] + public CredentialUserInfo CredentialUserInfo { get; private set; } - public Credential(string RPId, string? UserName, string? DisplayName, CredentialId CredentialID) + public Credential(RelyingParty relyingParty, CredentialUserInfo credentialUserInfo) { - this.RPId = RPId; - this.UserName = UserName; - this.DisplayName = DisplayName; - this.CredentialID = new powershellYK.FIDO2.CredentialID(CredentialID); + this.CredentialID = new powershellYK.FIDO2.CredentialID(credentialUserInfo.CredentialId); + this.RelyingParty = relyingParty; + this.CredentialUserInfo = credentialUserInfo; } #region Operators diff --git a/Module/types/FIDO2/Challenge.cs b/Module/types/FIDO2/Challenge.cs new file mode 100644 index 0000000..8e09ce5 --- /dev/null +++ b/Module/types/FIDO2/Challenge.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json.Linq; +using powershellYK.support; +using System.Management.Automation; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Fido2; +using Yubico.YubiKey.Fido2.Cose; + +namespace powershellYK.FIDO2 +{ + public class Challenge + { + private readonly byte[] _challange; + + public Challenge(string value) + { + // If the length is 32, we assume it's a hex string + if (value.Length == 32 && value.All(c => (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) + { + this._challange = HexConverter.StringToByteArray(value); + } + // else we assume it's a base64 string + else + { + this._challange = System.Convert.FromBase64String(value); + } + } + public Challenge(byte[] value) + { + this._challange = value; + } + + public static Challenge FakeChallange(string relyingPartyID) + { + return new Challenge(BuildFakeClientDataHash(relyingPartyID)); + } + public override string ToString() + { + return this.ToString(null); + } + public string ToString(string? format = "x") + { + return HexConverter.ByteArrayToString(_challange).ToLower(); + } + public byte[] ToByte() + { + return _challange; + } + #region Operators + + public static implicit operator byte[](Challenge source) + { + return source.ToByte(); + } + + public static implicit operator string(Challenge source) + { + return source.ToString(null); + } + + #endregion // Operators + + #region support + private static byte[] BuildFakeClientDataHash(string relyingPartyId) + { + byte[] idBytes = System.Text.Encoding.Unicode.GetBytes(relyingPartyId); + + // Generate a random value to represent the challenge. + var randomObject = CryptographyProviders.RngCreator(); + byte[] randomBytes = new byte[16]; + randomObject.GetBytes(randomBytes); + + var digester = CryptographyProviders.Sha256Creator(); + _ = digester.TransformBlock(randomBytes, 0, randomBytes.Length, null, 0); + _ = digester.TransformFinalBlock(idBytes, 0, idBytes.Length); + + return digester.Hash!; + } + #endregion // support + } +} diff --git a/Module/types/FIDO2/CredentialData.cs b/Module/types/FIDO2/CredentialData.cs new file mode 100644 index 0000000..c7ddc9f --- /dev/null +++ b/Module/types/FIDO2/CredentialData.cs @@ -0,0 +1,41 @@ +using powershellYK.support; +using System.Formats.Cbor; +using System.Runtime.CompilerServices; +using System.Text; +using Yubico.YubiKey.Fido2; + +namespace powershellYK.FIDO2 +{ + public class CredentialData + { + public MakeCredentialData MakeCredentialData { get { return this._makeCredentialData; } } + private readonly MakeCredentialData _makeCredentialData; + + public CredentialData(MakeCredentialData MakeCredentialData) + { + this._makeCredentialData = MakeCredentialData; + } + public override string ToString() + { + return this.ToString(null); + } + public string ToString(string? format = "x") + { + return ""; + } + + public byte[] w3cEncoded() + { + var writer = new CborWriter(); + writer.WriteStartMap(3); + writer.WriteTextString("fmt"); + writer.WriteTextString(this._makeCredentialData.Format); + writer.WriteTextString("attStmt"); + writer.WriteEncodedValue(_makeCredentialData.EncodedAttestationStatement.Span); + writer.WriteTextString("authData"); + writer.WriteByteString(_makeCredentialData.AuthenticatorData.EncodedAuthenticatorData.Span); + writer.WriteEndMap(); + return writer.Encode(); + } + } +}