aboutsummaryrefslogtreecommitdiff
path: root/src/Statics.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Statics.cs')
-rw-r--r--src/Statics.cs378
1 files changed, 378 insertions, 0 deletions
diff --git a/src/Statics.cs b/src/Statics.cs
new file mode 100644
index 0000000..56e3e25
--- /dev/null
+++ b/src/Statics.cs
@@ -0,0 +1,378 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Buffers;
+using System.Text.Json;
+using System.Buffers.Text;
+using System.Runtime.CompilerServices;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+using Serilog;
+using Serilog.Core;
+using Serilog.Events;
+
+using Yubico.YubiKey.Piv;
+
+using VNLib.Utils;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+using VNLib.Hashing;
+using VNLib.Hashing.IdentityUtility;
+
+namespace PkiAuthenticator
+{
+
+ internal static class Statics
+ {
+ public static ProcessArguments CliArgs { get; } = new(Environment.GetCommandLineArgs());
+
+ public static ILogProvider Log { get; } = GetLog();
+
+ private static ILogProvider GetLog()
+ {
+ LoggerConfiguration config = new();
+
+ //Set min level from cli flags
+ if(CliArgs.Verbose)
+ {
+ config.MinimumLevel.Verbose();
+ }
+ else if (CliArgs.Debug)
+ {
+ config.MinimumLevel.Debug();
+ }
+ else
+ {
+ config.MinimumLevel.Information();
+ }
+
+ //Make sure the silent flag is not set
+ if(!CliArgs.Silent)
+ {
+ //Write to console for now
+ config.WriteTo.Console();
+ }
+
+ //Init new log
+ return new VLogProvider(config);
+ }
+
+ /// <summary>
+ /// Generats a signed VNLib authentication toke, used to authenticate against
+ /// web applications using the YubiKey
+ /// </summary>
+ /// <param name="session"></param>
+ /// <returns>The process exit code returning the status of the operation.</returns>
+ public static int GenerateOtp(this IAuthenticator authenticator)
+ {
+ string? uid = CliArgs.GetArg("-u");
+ uid ??= CliArgs.GetArg("--user");
+
+ HashAlg digest;
+
+ //Init the jwt header
+ Dictionary<string, string> jwtHeader = new()
+ {
+ ["typ"] = "jwt"
+ };
+
+ Log.Verbose("Recovering the device metadata...");
+
+ switch (authenticator.KeyAlgorithm)
+ {
+ case PivAlgorithm.Rsa1024:
+ case PivAlgorithm.Rsa2048:
+ //Use rsa256 for all rsa operations
+ digest = HashAlg.SHA256;
+ jwtHeader["alg"] = "RS256";
+ break;
+ case PivAlgorithm.EccP256:
+ digest = HashAlg.SHA256;
+ jwtHeader["alg"] = "ES256";
+ break;
+ case PivAlgorithm.EccP384:
+ digest = HashAlg.SHA384;
+ jwtHeader["alg"] = "ES384";
+ break;
+ default:
+ Log.Error("The key's authentication slot contains an unsupported algorithm");
+ return -5;
+ }
+
+ //Build the login jwt
+ using JsonWebToken jwt = new();
+
+ jwt.WriteHeader(jwtHeader);
+
+ Log.Verbose("Recovering the x509 certificate from the key");
+
+ //Get the auth certificate
+ using (X509Certificate2 cert = authenticator.GetCertificate())
+ {
+ //Default uid is the subjet name
+ uid ??= cert.SubjectName.Name.AsSpan().SliceAfterParam("=").ToString();
+
+ //Get random nonce for entropy
+ string nonce = RandomHash.GetRandomBase32(16);
+
+ jwt.InitPayloadClaim()
+ .AddClaim("sub", uid)
+ .AddClaim("n", nonce)
+ .AddClaim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds())
+ //Keyid is the hex sha1 of the certificate
+ .AddClaim("keyid", Convert.ToHexString(cert.GetCertHash(HashAlgorithmName.SHA1)))
+ .AddClaim("serial", cert.SerialNumber)
+ .CommitClaims();
+ }
+
+ Log.Verbose("Signing authentication token...");
+
+ try
+ {
+ //Sign the token
+ jwt.Sign(authenticator, digest);
+
+ Log.Information(Program.TOKEN_PRINT_TEMPLATE, jwt.Compile());
+
+ //If silent mode is enabled, write credential directly to stdout
+ if (CliArgs.Silent)
+ {
+ Console.Write(jwt.Compile());
+ }
+
+ return 0;
+ }
+ catch (OperationCanceledException)
+ {
+ Log.Error("The operation has been cancelled");
+ return -1;
+ }
+ }
+
+ /// <summary>
+ /// Base64url encodes the data buffer and returns a utf8 string from
+ /// the encoded results.
+ /// </summary>
+ /// <param name="data">The binary buffer to encode</param>
+ /// <returns>The encoded string</returns>
+ public static string ToBase64Url(ReadOnlySpan<byte> data)
+ {
+ int base64 = Base64.GetMaxEncodedToUtf8Length(data.Length);
+
+ //Alloc buffer
+ using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage<byte>(base64);
+
+ int written = ToBase64Url(data, buffer.Span);
+
+ return Encoding.UTF8.GetString(buffer.Span[..written]);
+ }
+
+ /// <summary>
+ /// Base64url encodes the data buffer and writes the output to the <paramref name="writer"/>
+ /// argument.
+ /// </summary>
+ /// <param name="data">The binary data to base64url encode</param>
+ /// <param name="writer">A referrence to the <see cref="ForwardOnlyWriter{T}"/></param>
+ /// <exception cref="Exception"></exception>
+ public static void ToUrlSafe(ReadOnlySpan<byte> data, ref ForwardOnlyWriter<byte> writer)
+ {
+ int base64Size = Base64.GetMaxEncodedToUtf8Length(data.Length);
+
+ //Alloc buffer
+ using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage<byte>(base64Size);
+
+ //Convert the data to base64url safe
+ int written = ToBase64Url(data, buffer.Span);
+
+ if(written == ERRNO.E_FAIL)
+ {
+ throw new Exception($"Failed to encode the binary data due to a base64 encoding failure");
+ }
+
+ //Write encoded data to writer
+ writer.Append(buffer.Span[..written]);
+ }
+
+ /// <summary>
+ /// Base64url encodes the data buffer and writes the output to the output buffer.
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="buffer">The output buffer to write the base64url encoded utf8 bytes</param>
+ /// <returns>The number of bytes written to the output buffer, or 0/false if the operation failed</returns>
+ /// <exception cref="Exception"></exception>
+ public static ERRNO ToBase64Url(ReadOnlySpan<byte> data, Span<byte> buffer)
+ {
+ //Encode the data to base64
+ OperationStatus status = Base64.EncodeToUtf8(data, buffer, out _, out int written, true);
+
+ if (status != OperationStatus.Done)
+ {
+ return ERRNO.E_FAIL;
+ }
+
+ //Url encode
+ VnEncoding.Base64ToUrlSafeInPlace(buffer[..written]);
+
+ //Remove trailing padding bytes
+ while (buffer[written - 1] == (byte)'=')
+ {
+ written--;
+ }
+
+ return written;
+ }
+
+ /// <summary>
+ /// Writes the public key information for the current session, using the
+ /// configured slot, to a JWK, setting the key-id (kid) as the as the
+ /// hex encoded hash of the certificate.
+ /// </summary>
+ /// <param name="authenticator"></param>
+ /// <returns>The process exit code, 0 if successful, non-zero if a failure occured</returns>
+ public static string? ExportJwk(this IAuthenticator authenticator)
+ {
+ static void WriteEcParams(X509Certificate2 cert, IDictionary<string, string> jwk)
+ {
+ using ECDsa alg = cert.GetECDsaPublicKey()!;
+
+ //recover params
+ ECParameters p = alg.ExportParameters(false);
+
+ //Write public key elements
+ jwk["x"] = ToBase64Url(p.Q.X);
+ jwk["y"] = ToBase64Url(p.Q.Y);
+ }
+
+ static void WriteRsaParams(X509Certificate2 cert, IDictionary<string, string> jwk)
+ {
+ using RSA rSA = cert.GetRSAPublicKey()!;
+
+ RSAParameters p = rSA.ExportParameters(false);
+
+ jwk["e"] = ToBase64Url(p.Exponent);
+ jwk["n"] = ToBase64Url(p.Modulus);
+ }
+
+ Dictionary<string, string> jwkObj = new()
+ {
+ { "use", "sig" }
+ };
+
+ //Get key certificate
+ using X509Certificate2 cert = authenticator.GetCertificate();
+
+ //Write cert hash to the kid
+ jwkObj["kid"] = Convert.ToHexString(cert.GetCertHash(HashAlgorithmName.SHA1));
+
+ //Write cert serial number
+ jwkObj["serial"] = cert.SerialNumber;
+
+ switch (authenticator.KeyAlgorithm)
+ {
+ case PivAlgorithm.EccP256:
+ jwkObj["kty"] = "EC";
+ jwkObj["crv"] = "P-256";
+ jwkObj["alg"] = "ES256";
+
+ //write the ec params to jwk
+ WriteEcParams(cert, jwkObj);
+ break;
+ case PivAlgorithm.EccP384:
+ jwkObj["kty"] = "EC";
+ jwkObj["crv"] = "P-384";
+ jwkObj["alg"] = "ES384";
+
+ //write the ec params to jwk
+ WriteEcParams(cert, jwkObj);
+ break;
+
+ case PivAlgorithm.Rsa1024:
+ case PivAlgorithm.Rsa2048:
+ jwkObj["kty"] = "RSA";
+ jwkObj["alg"] = "RS256";
+
+ //Rsa print
+ WriteRsaParams(cert, jwkObj);
+ break;
+
+ default:
+ return null;
+ }
+
+ //Write jwk to std out
+ return JsonSerializer.Serialize(jwkObj);
+ }
+
+ /// <summary>
+ /// Writes the public key information for the current session, using the
+ /// configured slot, using PEM encoding.
+ /// </summary>
+ /// <param name="authenticator"></param>
+ /// <returns>The process exit code, 0 if successful, non-zero if a failure occured</returns>
+ public static string ExportPem(this IAuthenticator authenticator)
+ {
+ //Get key certificate
+ using X509Certificate2 cert = authenticator.GetCertificate();
+
+ byte[] pubkey = cert.PublicKey.ExportSubjectPublicKeyInfo();
+
+ //Sb for printing cert data
+ StringBuilder builder = new ();
+ builder.AppendLine("-----BEGIN PUBLIC KEY-----");
+ builder.AppendLine(Convert.ToBase64String(pubkey, Base64FormattingOptions.InsertLineBreaks));
+ builder.AppendLine("-----END PUBLIC KEY-----");
+
+ return builder.ToString();
+ }
+
+ private sealed class VLogProvider : VnDisposeable, ILogProvider
+ {
+ private readonly Logger LogCore;
+
+ public VLogProvider(LoggerConfiguration config)
+ {
+ LogCore = config.CreateLogger();
+ }
+ public void Flush() { }
+
+ public object GetLogProvider() => LogCore;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IsEnabled(LogLevel level) => LogCore.IsEnabled((LogEventLevel)level);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(LogLevel level, string value)
+ {
+ LogCore.Write((LogEventLevel)level, value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(LogLevel level, Exception exception, string value = "")
+ {
+ LogCore.Write((LogEventLevel)level, exception, value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(LogLevel level, string value, params object[] args)
+ {
+ LogCore.Write((LogEventLevel)level, value, args);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(LogLevel level, string value, params ValueType[] args)
+ {
+ //Serilog logger supports passing valuetypes to avoid boxing objects
+ if (LogCore.IsEnabled((LogEventLevel)level))
+ {
+ object[] ar = args.Select(a => (object)a).ToArray();
+ LogCore.Write((LogEventLevel)level, value, ar);
+ }
+ }
+
+ protected override void Free() => LogCore.Dispose();
+ }
+ }
+}