diff options
author | vnugent <public@vaughnnugent.com> | 2023-03-19 16:39:03 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-03-19 16:39:03 -0400 |
commit | e8af88efdae6ff3ef4780627430f31dca9cc665b (patch) | |
tree | afef9edb1c4345f7dc03eb6d1a3d88f7e62fae68 /src/HardwareAuthenticator.cs |
Initial commit
Diffstat (limited to 'src/HardwareAuthenticator.cs')
-rw-r--r-- | src/HardwareAuthenticator.cs | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/src/HardwareAuthenticator.cs b/src/HardwareAuthenticator.cs new file mode 100644 index 0000000..1f0f0b2 --- /dev/null +++ b/src/HardwareAuthenticator.cs @@ -0,0 +1,281 @@ +using System; +using System.Linq; +using System.Text; +using System.Buffers; +using System.Formats.Asn1; +using System.Globalization; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +using Yubico.YubiKey; +using Yubico.YubiKey.Piv; + +using VNLib.Utils; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; +using VNLib.Hashing; +using VNLib.Hashing.IdentityUtility; + +using static PkiAuthenticator.Statics; + +namespace PkiAuthenticator +{ + /// <summary> + /// Implements a hardware backed authenticator device using YubiKey's + /// </summary> + public sealed class HardwareAuthenticator : VnDisposeable, IAuthenticator + { + + /* + * Determines the piv slot the user may manually select + * the slot nuber (in hex) or the default + * Authentication slot + */ + + private static byte PivSlot + { + get + { + //Check for slot cli flag + string? slotArg = CliArgs.GetArg("--piv-slot"); + //Try hase from hex, otherwise default to the authentication slot + return byte.TryParse(slotArg, NumberStyles.HexNumber, null, out byte slotNum) ? slotNum : Yubico.YubiKey.Piv.PivSlot.Authentication; + } + } + + private PivSession? _session; + + ///<inheritdoc/> + public PivAlgorithm KeyAlgorithm { get; private set; } + + public int RequiredBufferSize { get; } + + ///<inheritdoc/> + public bool Initialize() + { + IYubiKeyDevice? device; + + //User may select the serial of the specific key to use + if (CliArgs.HasArg("--key") && int.TryParse(CliArgs.GetArg("--key"), out int serial)) + { + Log.Debug("Loading device {d}", serial); + + //Get device by serial number + device = YubiKeyDevice.FindAll() + .Where(d => d.SerialNumber == serial && d.HasFeature(YubiKeyFeature.PivApplication)) + .FirstOrDefault(); + } + else + { + Log.Debug("Connecting to first discovered PIV supported yubikey"); + + //Get first piv device + device = YubiKeyDevice.FindAll() + .Where(static d => d.HasFeature(YubiKeyFeature.PivApplication)) + .FirstOrDefault(); + } + + if (device == null) + { + return false; + } + try + { + //Init PIV session + _session = new(device) + { + KeyCollector = GetUserPinInput + }; + + Log.Debug("Connected to device {id}", device.SerialNumber!); + + //Store the key algorithm + KeyAlgorithm = _session.GetMetadata(PivSlot).Algorithm; + + return true; + } + catch (Exception ex) + { + if (Log.IsEnabled(LogLevel.Debug)) + { + Log.Error(ex); + } + else + { + Log.Error("Failed to initialize your hardware authenticator. Reason {r}", ex.Message); + } + + return false; + } + } + + ///<inheritdoc/> + public int ListDevices() + { + Log.Debug("Discovering hardware devices..."); + + IEnumerable<IYubiKeyDevice> devices = YubiKeyDevice.FindAll(); + + string[] devIds = devices + .Select(d => $"Serial: {d.SerialNumber}, Firmware {d.FirmwareVersion}, Formfactor: {d.FormFactor}, PIV support?: {d.HasFeature(YubiKeyFeature.PivApplication)}") + .ToArray(); + + Log.Information("Found devices\n {dev}", devIds); + + return 0; + } + + ///<inheritdoc/> + public X509Certificate2 GetCertificate() => + _session?.GetCertificate(PivSlot) + ?? throw new InvalidOperationException("The PIV session has not been successfully initialized"); + + protected override void Free() + { + _session?.Dispose(); + } + + static bool GetUserPinInput(KeyEntryData keyData) + { + //Method may be called more than once during pin operation, we only need to prompt for pins + if (keyData.Request != KeyEntryRequest.VerifyPivPin) + { + return false; + } + + string? input; + + //Check if the user issued the pin as cli arg + if (CliArgs.HasArg("--pin")) + { + //No retires allowed during cli, we dont want the device to lock out + if (keyData.IsRetry) + { + return false; + } + + input = CliArgs.GetArg("--pin"); + } + //Check for environment variable + else if (Environment.GetEnvironmentVariable(Program.YUBIKEY_PIN_ENV_VAR_NAME) != null) + { + //No retires allowed during env, we dont want the device to lock out + if (keyData.IsRetry) + { + return false; + } + input = Environment.GetEnvironmentVariable(Program.YUBIKEY_PIN_ENV_VAR_NAME); + } + //If the silent flag is set, a pin cli or env must be set, since we cannot write to STDOUT + else if (CliArgs.Silent) + { + return false; + } + else + { + Log.Information("Please enter your device pin, you have {t} attempts remaining, press enter to cancel", keyData.RetriesRemaining); + + input = Console.ReadLine(); + } + + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + byte[] pinData = Encoding.UTF8.GetBytes(input); + + //Submit pin + keyData.SubmitValue(pinData); + return true; + } + + private static ERRNO ConvertFromBer(Span<byte> berData) + { + static ReadOnlySpan<byte> GetSequence(ReadOnlySpan<byte> bytes) + { + //Parse the initial sequence + AsnDecoder.ReadSequence(bytes, AsnEncodingRules.DER, out int seqStart, out int seqLen, out _, Asn1Tag.Sequence); + + //Return the discovered sequence + return bytes.Slice(seqStart, seqLen); + } + + //Read the initial sequence + ReadOnlySpan<byte> seq = GetSequence(berData); + + //Reat the r integer value first + ReadOnlySpan<byte> r = AsnDecoder.ReadIntegerBytes(seq, AsnEncodingRules.DER, out int read); + + //Get s after r + ReadOnlySpan<byte> s = AsnDecoder.ReadIntegerBytes(seq[read..], AsnEncodingRules.DER, out _); + + int rlen = 0, slen = 0; + + //trim leading whitespace + while (r[0] == 0x00) + { + r = r[1..]; + } + while (s[0] == 0x00) + { + s = s[1..]; + } + + rlen = r.Length; + slen = s.Length; + + //Concat buffer must be 2* the size of the largest value, so we can add padding + Span<byte> concatBuffer = stackalloc byte[Math.Max(rlen, slen) * 2]; + + if (rlen > slen) + { + //Write r first + r.CopyTo(concatBuffer); + + //Write s to the end of the buffer, zero padding exists from stackalloc + s.CopyTo(concatBuffer[rlen..][(rlen - slen)..]); + + Console.WriteLine("r larger"); + } + else if (rlen < slen) + { + //offset the begining of the buffer for leading r padding + r.CopyTo(concatBuffer[(slen - rlen)..]); + + //Write s to the end of the buffer, zero padding exists from stackalloc + s.CopyTo(concatBuffer[slen..]); + + Console.WriteLine("s larger"); + } + else + { + r.CopyTo(concatBuffer); + + s.CopyTo(concatBuffer[rlen..]); + } + + //Write back to output buffer + concatBuffer.CopyTo(berData); + + //Return number written + return concatBuffer.Length; + } + + public ERRNO ComputeSignatureFromHash(ReadOnlySpan<byte> hash, Span<byte> outputBuffer) + { + Log.Debug("Signing authentication data using YubiKey..."); + + //Get the current jwt state as a binary buffer + byte[] signature = _session!.Sign(PivSlot, hash.ToArray()); + + //Covert from BER encoding to IEEE fixed/concat signature data for jwt + ERRNO count = ConvertFromBer(signature); + + //Copy to output buffer + signature[..(int)count].CopyTo(outputBuffer); + + return count; + } + } +} |