Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial New-YubiKeyFIDO2Credential #122

Merged
merged 6 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions Docs/Examples/Enroll YubiKey FIDO2 against demo.yubico.com.md
Original file line number Diff line number Diff line change
@@ -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'
2 changes: 1 addition & 1 deletion Module/Cmdlets/FIDO2/GetYubikeyFIDO2Credential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
106 changes: 106 additions & 0 deletions Module/Cmdlets/FIDO2/NewFIDO2Credential.cs
Original file line number Diff line number Diff line change
@@ -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<byte>(UserID);
var relayingParty = new RelyingParty(RelyingPartyID) { Name = RelyingPartyName };

if (UserEntity is null)
{
UserEntity = new UserEntity(userId)
{
Name = Username,
DisplayName = UserDisplayName ?? Username,
};
}

ReadOnlyMemory<byte> 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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Security.Cryptography;
using Yubico.YubiKey.Sample.PivSampleCode;
using powershellYK.PIV;
using powershellYK.support.transform;


namespace powershellYK.Cmdlets.PIV
Expand All @@ -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;

Expand Down
1 change: 1 addition & 0 deletions Module/Cmdlets/PIV/BuildYubikeyPIVSignCertificate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
53 changes: 53 additions & 0 deletions Module/Cmdlets/PIV/RemoveYubikeyPIVCertificate.cs
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 41 in Module/Cmdlets/PIV/RemoveYubikeyPIVCertificate.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected

Check warning on line 41 in Module/Cmdlets/PIV/RemoveYubikeyPIVCertificate.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected
{
// This will throw an exception if no key is found in the slot
}
catch
{
}
}
}
}

}
}
1 change: 1 addition & 0 deletions Module/powershellYK.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ CmdletsToExport = @(
'Enable-YubiKeyFIDO2EnterpriseAttestation',
'Get-YubiKeyFIDO2',
'Get-YubiKeyFIDO2Credential',
'New-YubiKeyFIDO2Credential',
'Remove-YubiKeyFIDO2Credential'
'Set-YubiKeyFIDO2',
'Set-YubiKeyFIDO2PIN',
Expand Down
19 changes: 10 additions & 9 deletions Module/types/FIDO2-Credentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 80 additions & 0 deletions Module/types/FIDO2/Challenge.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Loading