/*
* Copyright (c) 2023 Vaughn Nugent
*
* Package: PkiAuthenticator
* File: Statics.cs
*
* PkiAuthenticator is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 2 of the License,
* or (at your option) any later version.
*
* PkiAuthenticator is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PkiAuthenticator. If not, see http://www.gnu.org/licenses/.
*/
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);
}
///
/// Generats a signed VNLib authentication toke, used to authenticate against
/// web applications using the YubiKey
///
/// The process exit code returning the status of the operation.
public static int GenerateOtp(this IAuthenticator authenticator)
{
string? uid = CliArgs.GetArg("-u");
uid ??= CliArgs.GetArg("--user");
HashAlg digest;
//Init the jwt header
Dictionary 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;
}
}
///
/// Base64url encodes the data buffer and returns a utf8 string from
/// the encoded results.
///
/// The binary buffer to encode
/// The encoded string
public static string ToBase64Url(ReadOnlySpan data)
{
int base64 = Base64.GetMaxEncodedToUtf8Length(data.Length);
//Alloc buffer
using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAllocNearestPage(base64);
int written = ToBase64Url(data, buffer.Span);
return Encoding.UTF8.GetString(buffer.Span[..written]);
}
///
/// Base64url encodes the data buffer and writes the output to the
/// argument.
///
/// The binary data to base64url encode
/// A referrence to the
///
public static void ToUrlSafe(ReadOnlySpan data, ref ForwardOnlyWriter writer)
{
int base64Size = Base64.GetMaxEncodedToUtf8Length(data.Length);
//Alloc buffer
using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAllocNearestPage(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]);
}
///
/// Base64url encodes the data buffer and writes the output to the output buffer.
///
///
/// The output buffer to write the base64url encoded utf8 bytes
/// The number of bytes written to the output buffer, or 0/false if the operation failed
///
public static ERRNO ToBase64Url(ReadOnlySpan data, Span 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;
}
///
/// 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.
///
///
/// The process exit code, 0 if successful, non-zero if a failure occured
public static string? ExportJwk(this IAuthenticator authenticator)
{
static void WriteEcParams(X509Certificate2 cert, IDictionary 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 jwk)
{
using RSA rSA = cert.GetRSAPublicKey()!;
RSAParameters p = rSA.ExportParameters(false);
jwk["e"] = ToBase64Url(p.Exponent);
jwk["n"] = ToBase64Url(p.Modulus);
}
Dictionary 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);
}
///
/// Writes the public key information for the current session, using the
/// configured slot, using PEM encoding.
///
///
/// The process exit code, 0 if successful, non-zero if a failure occured
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();
}
}
}