mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
548 lines
22 KiB
C#
548 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
namespace Esiur.Security.Authority.Providers.Ppap;
|
|
|
|
/// <summary>
|
|
/// Per-connection PPAP ML-KEM state. A handler instance must not be reused.
|
|
/// </summary>
|
|
public sealed partial class PpapAuthenticationHandler : IAuthenticationHandler,
|
|
IAuthenticationKeyRotationHandler, IDisposable
|
|
{
|
|
enum HandshakeState
|
|
{
|
|
InitiatorStart,
|
|
InitiatorAwaitServerHello,
|
|
InitiatorAwaitResponderProof,
|
|
ResponderAwaitClientHello,
|
|
ResponderAwaitInitiatorProof,
|
|
ResponderAwaitInitiatorFinished,
|
|
Complete,
|
|
Failed,
|
|
Disposed,
|
|
}
|
|
|
|
readonly object _sync = new();
|
|
readonly PpapAuthenticationProvider _provider;
|
|
readonly PpapLocalIdentity _localIdentity;
|
|
readonly AuthenticationDirection _direction;
|
|
readonly AuthenticationMode _mode;
|
|
readonly string _domain;
|
|
readonly string _expectedInitiatorIdentity;
|
|
readonly string _expectedResponderIdentity;
|
|
readonly bool _authenticateInitiator;
|
|
readonly bool _authenticateResponder;
|
|
readonly List<byte[]> _transcript = new();
|
|
|
|
HandshakeState _state;
|
|
string _initiatorIdentity;
|
|
string _responderIdentity;
|
|
PpapRegistrationRecord _initiatorRegistration;
|
|
PpapRegistrationRecord _responderRegistration;
|
|
byte[] _initiatorMask;
|
|
byte[] _responderMask;
|
|
byte[] _ephemeralPrivateKey;
|
|
byte[] _ephemeralSecret;
|
|
byte[] _initiatorIdentitySecret;
|
|
byte[] _responderIdentitySecret;
|
|
byte[] _sessionKey;
|
|
byte[] _initiatorFinishedKey;
|
|
byte[] _responderFinishedKey;
|
|
byte[] _transcriptHash;
|
|
byte[] _authenticationContext;
|
|
|
|
public IAuthenticationProvider Provider => _provider;
|
|
public string Protocol => PpapProtocol.Name;
|
|
|
|
internal PpapAuthenticationHandler(PpapAuthenticationProvider provider,
|
|
AuthenticationContext context)
|
|
{
|
|
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
|
|
_localIdentity = provider.ResolveLocalIdentity(context);
|
|
_direction = context.Direction;
|
|
_mode = context.Mode;
|
|
_domain = PpapCryptography.NormalizeDomain(context.Domain);
|
|
_expectedInitiatorIdentity = NormalizeOptionalIdentity(context.InitiatorIdentity);
|
|
_expectedResponderIdentity = NormalizeOptionalIdentity(context.ResponderIdentity);
|
|
|
|
if (_direction != AuthenticationDirection.Initiator
|
|
&& _direction != AuthenticationDirection.Responder)
|
|
throw new ArgumentOutOfRangeException(nameof(context.Direction));
|
|
if (_mode != AuthenticationMode.InitializerIdentity
|
|
&& _mode != AuthenticationMode.ResponderIdentity
|
|
&& _mode != AuthenticationMode.DualIdentity)
|
|
throw new ArgumentOutOfRangeException(nameof(context.Mode));
|
|
|
|
_authenticateInitiator = _mode == AuthenticationMode.InitializerIdentity
|
|
|| _mode == AuthenticationMode.DualIdentity;
|
|
_authenticateResponder = _mode == AuthenticationMode.ResponderIdentity
|
|
|| _mode == AuthenticationMode.DualIdentity;
|
|
|
|
var localIsAuthenticated = _direction == AuthenticationDirection.Initiator
|
|
? _authenticateInitiator
|
|
: _authenticateResponder;
|
|
if (localIsAuthenticated && _localIdentity == null)
|
|
throw new InvalidOperationException(
|
|
"This PPAP mode requires a provider-configured local identity.");
|
|
|
|
if (_localIdentity != null)
|
|
{
|
|
var expectedLocal = _direction == AuthenticationDirection.Initiator
|
|
? _expectedInitiatorIdentity
|
|
: _expectedResponderIdentity;
|
|
if (expectedLocal != null && !string.Equals(expectedLocal,
|
|
_localIdentity.Identity, StringComparison.Ordinal))
|
|
throw new InvalidOperationException(
|
|
"The connection identity does not match the PPAP provider identity.");
|
|
}
|
|
|
|
_state = _direction == AuthenticationDirection.Initiator
|
|
? HandshakeState.InitiatorStart
|
|
: HandshakeState.ResponderAwaitClientHello;
|
|
}
|
|
|
|
public AuthenticationResult Process(object authData)
|
|
{
|
|
lock (_sync)
|
|
{
|
|
if (_state == HandshakeState.Disposed)
|
|
return Failed();
|
|
|
|
try
|
|
{
|
|
if (_direction == AuthenticationDirection.Initiator)
|
|
return ProcessInitiator(authData);
|
|
return ProcessResponder(authData);
|
|
}
|
|
catch
|
|
{
|
|
_state = HandshakeState.Failed;
|
|
ClearHandshakeSecrets(clearSessionKey: true);
|
|
ClearRotationSecrets();
|
|
return Failed();
|
|
}
|
|
}
|
|
}
|
|
|
|
AuthenticationResult ProcessInitiator(object authData)
|
|
{
|
|
if (_state == HandshakeState.InitiatorStart)
|
|
{
|
|
if (authData != null)
|
|
throw new InvalidDataException("Unexpected initial PPAP data.");
|
|
|
|
if (_authenticateInitiator)
|
|
_initiatorIdentity = _localIdentity.Identity;
|
|
|
|
PpapCryptography.GenerateKeyPair(out _ephemeralPrivateKey,
|
|
out var ephemeralPublicKey);
|
|
_initiatorMask = PpapCryptography.RandomBytes(PpapProtocol.IdentityMaskLength);
|
|
byte[] message = null;
|
|
try
|
|
{
|
|
message = PpapWire.EncodeClientHello(_mode, _domain,
|
|
ephemeralPublicKey, _initiatorMask);
|
|
AddTranscript(message);
|
|
_state = HandshakeState.InitiatorAwaitServerHello;
|
|
return InProgress(message);
|
|
}
|
|
finally
|
|
{
|
|
PpapCryptography.Clear(ephemeralPublicKey);
|
|
}
|
|
}
|
|
|
|
if (_state == HandshakeState.InitiatorAwaitServerHello)
|
|
{
|
|
var raw = RequireBytes(authData);
|
|
var hello = PpapWire.DecodeServerHello(raw, _authenticateResponder);
|
|
AddTranscript(raw);
|
|
|
|
_ephemeralSecret = PpapCryptography.Decapsulate(
|
|
_ephemeralPrivateKey, hello.EphemeralCiphertext);
|
|
PpapCryptography.Clear(_ephemeralPrivateKey);
|
|
_ephemeralPrivateKey = null;
|
|
_responderMask = hello.ResponderMask;
|
|
|
|
byte[] maskedInitiator = null;
|
|
byte[] responderCiphertext = null;
|
|
PpapRegistrationDescriptor responderDescriptor = null;
|
|
|
|
if (_authenticateInitiator)
|
|
maskedInitiator = PpapCryptography.MaskIdentity(_domain,
|
|
_initiatorIdentity, _responderMask, _ephemeralSecret);
|
|
|
|
if (_authenticateResponder)
|
|
{
|
|
_responderRegistration = _provider.Registrations.ResolveMasked(
|
|
_domain, _initiatorMask, _ephemeralSecret,
|
|
hello.MaskedResponderIdentity);
|
|
ValidateRemoteRegistration(_responderRegistration,
|
|
_expectedResponderIdentity);
|
|
_responderIdentity = _responderRegistration.Identity;
|
|
PpapCryptography.Encapsulate(
|
|
_responderRegistration.EncapsulationKeyBytes,
|
|
out responderCiphertext, out _responderIdentitySecret);
|
|
responderDescriptor = PpapRegistrationDescriptor.FromRecord(
|
|
_responderRegistration);
|
|
}
|
|
|
|
try
|
|
{
|
|
var message = PpapWire.EncodeInitiatorProof(maskedInitiator,
|
|
responderCiphertext, responderDescriptor, _domain,
|
|
_ephemeralSecret);
|
|
AddTranscript(message);
|
|
_state = HandshakeState.InitiatorAwaitResponderProof;
|
|
return InProgress(message);
|
|
}
|
|
finally
|
|
{
|
|
PpapCryptography.Clear(maskedInitiator);
|
|
PpapCryptography.Clear(responderCiphertext);
|
|
}
|
|
}
|
|
|
|
if (_state == HandshakeState.InitiatorAwaitResponderProof)
|
|
{
|
|
var proof = PpapWire.DecodeResponderProof(authData,
|
|
_authenticateInitiator, _domain, _ephemeralSecret);
|
|
AddTranscript(proof.TranscriptCore);
|
|
|
|
if (_authenticateInitiator)
|
|
{
|
|
ValidateLocalDescriptor(proof.InitiatorRegistration,
|
|
_localIdentity);
|
|
byte[] privateKey = null;
|
|
byte[] publicKey = null;
|
|
try
|
|
{
|
|
privateKey = _provider.DerivePrivateKey(_localIdentity, _domain,
|
|
proof.InitiatorRegistration.Nonce,
|
|
proof.InitiatorRegistration.KdfProfile);
|
|
publicKey = PpapCryptography.GetPublicKey(privateKey);
|
|
_initiatorIdentitySecret = PpapCryptography.Decapsulate(
|
|
privateKey, proof.InitiatorCiphertext);
|
|
_initiatorRegistration = new PpapRegistrationRecord(
|
|
proof.InitiatorRegistration.Version, _initiatorIdentity,
|
|
proof.InitiatorRegistration.Kind,
|
|
proof.InitiatorRegistration.Nonce, publicKey,
|
|
proof.InitiatorRegistration.KdfProfile);
|
|
}
|
|
finally
|
|
{
|
|
PpapCryptography.Clear(privateKey);
|
|
PpapCryptography.Clear(publicKey);
|
|
}
|
|
}
|
|
|
|
DeriveHandshakeKeys();
|
|
var expected = PpapCryptography.ComputeFinished(false,
|
|
_responderFinishedKey, _transcriptHash);
|
|
var verified = PpapCryptography.FixedTimeEquals(expected, proof.Finished);
|
|
PpapCryptography.Clear(expected);
|
|
PpapCryptography.Clear(proof.Finished);
|
|
PpapCryptography.Clear(proof.TranscriptCore);
|
|
if (!verified)
|
|
throw new InvalidDataException("Responder Finished verification failed.");
|
|
|
|
var initiatorFinished = PpapCryptography.ComputeFinished(true,
|
|
_initiatorFinishedKey, _transcriptHash);
|
|
try
|
|
{
|
|
var message = PpapWire.EncodeInitiatorFinished(initiatorFinished);
|
|
_state = HandshakeState.Complete;
|
|
ClearPostConfirmationSecrets();
|
|
return Succeeded(message);
|
|
}
|
|
finally
|
|
{
|
|
PpapCryptography.Clear(initiatorFinished);
|
|
}
|
|
}
|
|
|
|
throw new InvalidDataException("Invalid PPAP initiator state.");
|
|
}
|
|
|
|
AuthenticationResult ProcessResponder(object authData)
|
|
{
|
|
if (_state == HandshakeState.ResponderAwaitClientHello)
|
|
{
|
|
var raw = RequireBytes(authData);
|
|
var hello = PpapWire.DecodeClientHello(raw);
|
|
if (hello.Mode != _mode
|
|
|| !string.Equals(hello.Domain, _domain, StringComparison.Ordinal))
|
|
throw new InvalidDataException("PPAP context mismatch.");
|
|
AddTranscript(raw);
|
|
_initiatorMask = hello.InitiatorMask;
|
|
|
|
if (_authenticateResponder)
|
|
_responderIdentity = _localIdentity.Identity;
|
|
|
|
PpapCryptography.Encapsulate(hello.EphemeralKey,
|
|
out var ephemeralCiphertext, out _ephemeralSecret);
|
|
_responderMask = PpapCryptography.RandomBytes(
|
|
PpapProtocol.IdentityMaskLength);
|
|
byte[] maskedResponder = null;
|
|
if (_authenticateResponder)
|
|
maskedResponder = PpapCryptography.MaskIdentity(_domain,
|
|
_responderIdentity, _initiatorMask, _ephemeralSecret);
|
|
|
|
try
|
|
{
|
|
var message = PpapWire.EncodeServerHello(ephemeralCiphertext,
|
|
_responderMask, maskedResponder);
|
|
AddTranscript(message);
|
|
_state = HandshakeState.ResponderAwaitInitiatorProof;
|
|
return InProgress(message);
|
|
}
|
|
finally
|
|
{
|
|
PpapCryptography.Clear(ephemeralCiphertext);
|
|
PpapCryptography.Clear(maskedResponder);
|
|
PpapCryptography.Clear(hello.EphemeralKey);
|
|
}
|
|
}
|
|
|
|
if (_state == HandshakeState.ResponderAwaitInitiatorProof)
|
|
{
|
|
var raw = RequireBytes(authData);
|
|
var proof = PpapWire.DecodeInitiatorProof(raw,
|
|
_authenticateInitiator, _authenticateResponder, _domain,
|
|
_ephemeralSecret);
|
|
AddTranscript(raw);
|
|
|
|
if (_authenticateResponder)
|
|
{
|
|
ValidateLocalDescriptor(proof.ResponderRegistration,
|
|
_localIdentity);
|
|
byte[] privateKey = null;
|
|
byte[] publicKey = null;
|
|
try
|
|
{
|
|
privateKey = _provider.DerivePrivateKey(_localIdentity, _domain,
|
|
proof.ResponderRegistration.Nonce,
|
|
proof.ResponderRegistration.KdfProfile);
|
|
publicKey = PpapCryptography.GetPublicKey(privateKey);
|
|
_responderIdentitySecret = PpapCryptography.Decapsulate(
|
|
privateKey, proof.ResponderCiphertext);
|
|
_responderRegistration = new PpapRegistrationRecord(
|
|
proof.ResponderRegistration.Version, _responderIdentity,
|
|
proof.ResponderRegistration.Kind,
|
|
proof.ResponderRegistration.Nonce, publicKey,
|
|
proof.ResponderRegistration.KdfProfile);
|
|
}
|
|
finally
|
|
{
|
|
PpapCryptography.Clear(privateKey);
|
|
PpapCryptography.Clear(publicKey);
|
|
}
|
|
}
|
|
|
|
byte[] initiatorCiphertext = null;
|
|
PpapRegistrationDescriptor initiatorDescriptor = null;
|
|
if (_authenticateInitiator)
|
|
{
|
|
_initiatorRegistration = _provider.Registrations.ResolveMasked(
|
|
_domain, _responderMask, _ephemeralSecret,
|
|
proof.MaskedInitiatorIdentity);
|
|
ValidateRemoteRegistration(_initiatorRegistration,
|
|
_expectedInitiatorIdentity);
|
|
_initiatorIdentity = _initiatorRegistration.Identity;
|
|
PpapCryptography.Encapsulate(
|
|
_initiatorRegistration.EncapsulationKeyBytes,
|
|
out initiatorCiphertext, out _initiatorIdentitySecret);
|
|
initiatorDescriptor = PpapRegistrationDescriptor.FromRecord(
|
|
_initiatorRegistration);
|
|
}
|
|
|
|
byte[] core = null;
|
|
byte[] protectedInitiatorDescriptor = null;
|
|
byte[] responderFinished = null;
|
|
try
|
|
{
|
|
core = PpapWire.EncodeResponderProofCore(initiatorCiphertext,
|
|
initiatorDescriptor, _domain, _ephemeralSecret,
|
|
out protectedInitiatorDescriptor);
|
|
AddTranscript(core);
|
|
DeriveHandshakeKeys();
|
|
responderFinished = PpapCryptography.ComputeFinished(false,
|
|
_responderFinishedKey, _transcriptHash);
|
|
var message = PpapWire.EncodeResponderProof(initiatorCiphertext,
|
|
protectedInitiatorDescriptor, responderFinished);
|
|
_state = HandshakeState.ResponderAwaitInitiatorFinished;
|
|
return InProgress(message);
|
|
}
|
|
finally
|
|
{
|
|
PpapCryptography.Clear(core);
|
|
PpapCryptography.Clear(protectedInitiatorDescriptor);
|
|
PpapCryptography.Clear(initiatorCiphertext);
|
|
PpapCryptography.Clear(responderFinished);
|
|
}
|
|
}
|
|
|
|
if (_state == HandshakeState.ResponderAwaitInitiatorFinished)
|
|
{
|
|
var remoteFinished = PpapWire.DecodeInitiatorFinished(authData);
|
|
var expected = PpapCryptography.ComputeFinished(true,
|
|
_initiatorFinishedKey, _transcriptHash);
|
|
var verified = PpapCryptography.FixedTimeEquals(expected, remoteFinished);
|
|
PpapCryptography.Clear(expected);
|
|
PpapCryptography.Clear(remoteFinished);
|
|
if (!verified)
|
|
throw new InvalidDataException("Initiator Finished verification failed.");
|
|
|
|
_state = HandshakeState.Complete;
|
|
ClearPostConfirmationSecrets();
|
|
return Succeeded(null);
|
|
}
|
|
|
|
throw new InvalidDataException("Invalid PPAP responder state.");
|
|
}
|
|
|
|
void DeriveHandshakeKeys()
|
|
{
|
|
_authenticationContext = PpapWire.EncodeAuthenticationContext(_mode,
|
|
_domain, _initiatorIdentity,
|
|
_authenticateInitiator ? (PpapIdentityKind?)GetInitiatorKind() : null,
|
|
_responderIdentity,
|
|
_authenticateResponder ? (PpapIdentityKind?)GetResponderKind() : null);
|
|
_transcriptHash = PpapCryptography.ComputeTranscriptHash(
|
|
_transcript, _authenticationContext);
|
|
PpapCryptography.DeriveHandshakeKeys(_ephemeralSecret,
|
|
_initiatorIdentitySecret, _responderIdentitySecret,
|
|
_transcriptHash, _authenticationContext,
|
|
out _sessionKey, out _initiatorFinishedKey,
|
|
out _responderFinishedKey);
|
|
|
|
PpapCryptography.Clear(_ephemeralSecret);
|
|
PpapCryptography.Clear(_initiatorIdentitySecret);
|
|
PpapCryptography.Clear(_responderIdentitySecret);
|
|
_ephemeralSecret = null;
|
|
_initiatorIdentitySecret = null;
|
|
_responderIdentitySecret = null;
|
|
}
|
|
|
|
PpapIdentityKind GetInitiatorKind()
|
|
=> _initiatorRegistration?.Kind ?? _localIdentity.Kind;
|
|
|
|
PpapIdentityKind GetResponderKind()
|
|
=> _responderRegistration?.Kind ?? _localIdentity.Kind;
|
|
|
|
void ValidateRemoteRegistration(PpapRegistrationRecord record,
|
|
string expectedIdentity)
|
|
{
|
|
if (record == null)
|
|
throw new InvalidDataException("The masked identity could not be resolved.");
|
|
if (expectedIdentity != null && !string.Equals(expectedIdentity,
|
|
record.Identity, StringComparison.Ordinal))
|
|
throw new InvalidDataException("The resolved identity was not expected.");
|
|
}
|
|
|
|
static void ValidateLocalDescriptor(PpapRegistrationDescriptor descriptor,
|
|
PpapLocalIdentity identity)
|
|
{
|
|
if (descriptor == null || identity == null || descriptor.Kind != identity.Kind)
|
|
throw new InvalidDataException("The local registration descriptor is invalid.");
|
|
if (identity.Kind == PpapIdentityKind.PasswordDerived
|
|
&& !identity.KdfProfile.Equals(descriptor.KdfProfile))
|
|
throw new InvalidDataException("The local registration KDF profile is not accepted.");
|
|
}
|
|
|
|
void AddTranscript(byte[] message)
|
|
{
|
|
if (message == null || message.Length == 0
|
|
|| message.Length > PpapProtocol.MaximumWireMessageBytes)
|
|
throw new InvalidDataException("Invalid transcript message.");
|
|
_transcript.Add((byte[])message.Clone());
|
|
}
|
|
|
|
static byte[] RequireBytes(object data)
|
|
{
|
|
if (!(data is byte[] value))
|
|
throw new InvalidDataException("PPAP data must be a byte array.");
|
|
return value;
|
|
}
|
|
|
|
AuthenticationResult InProgress(byte[] data)
|
|
=> new AuthenticationResult(AuthenticationRuling.InProgress, data);
|
|
|
|
AuthenticationResult Succeeded(byte[] data)
|
|
{
|
|
var local = _direction == AuthenticationDirection.Initiator
|
|
? _initiatorIdentity : _responderIdentity;
|
|
var remote = _direction == AuthenticationDirection.Initiator
|
|
? _responderIdentity : _initiatorIdentity;
|
|
return new AuthenticationResult(AuthenticationRuling.Succeeded, data,
|
|
local, remote, _sessionKey);
|
|
}
|
|
|
|
static AuthenticationResult Failed()
|
|
=> new AuthenticationResult(AuthenticationRuling.Failed, null);
|
|
|
|
static string NormalizeOptionalIdentity(string identity)
|
|
=> identity == null ? null : PpapCryptography.NormalizeIdentity(identity);
|
|
|
|
void ClearPostConfirmationSecrets()
|
|
{
|
|
PpapCryptography.Clear(_initiatorFinishedKey);
|
|
PpapCryptography.Clear(_responderFinishedKey);
|
|
PpapCryptography.Clear(_transcriptHash);
|
|
PpapCryptography.Clear(_authenticationContext);
|
|
PpapCryptography.Clear(_initiatorMask);
|
|
PpapCryptography.Clear(_responderMask);
|
|
_initiatorFinishedKey = null;
|
|
_responderFinishedKey = null;
|
|
_transcriptHash = null;
|
|
_authenticationContext = null;
|
|
_initiatorMask = null;
|
|
_responderMask = null;
|
|
foreach (var message in _transcript)
|
|
PpapCryptography.Clear(message);
|
|
_transcript.Clear();
|
|
}
|
|
|
|
void ClearHandshakeSecrets(bool clearSessionKey)
|
|
{
|
|
PpapCryptography.Clear(_ephemeralPrivateKey);
|
|
PpapCryptography.Clear(_ephemeralSecret);
|
|
PpapCryptography.Clear(_initiatorIdentitySecret);
|
|
PpapCryptography.Clear(_responderIdentitySecret);
|
|
PpapCryptography.Clear(_initiatorFinishedKey);
|
|
PpapCryptography.Clear(_responderFinishedKey);
|
|
PpapCryptography.Clear(_transcriptHash);
|
|
PpapCryptography.Clear(_authenticationContext);
|
|
if (clearSessionKey)
|
|
PpapCryptography.Clear(_sessionKey);
|
|
_ephemeralPrivateKey = null;
|
|
_ephemeralSecret = null;
|
|
_initiatorIdentitySecret = null;
|
|
_responderIdentitySecret = null;
|
|
_initiatorFinishedKey = null;
|
|
_responderFinishedKey = null;
|
|
if (clearSessionKey)
|
|
_sessionKey = null;
|
|
foreach (var message in _transcript)
|
|
PpapCryptography.Clear(message);
|
|
_transcript.Clear();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (_sync)
|
|
{
|
|
if (_state == HandshakeState.Disposed)
|
|
return;
|
|
ClearHandshakeSecrets(clearSessionKey: true);
|
|
PpapCryptography.Clear(_initiatorMask);
|
|
PpapCryptography.Clear(_responderMask);
|
|
PpapCryptography.Clear(_transcriptHash);
|
|
PpapCryptography.Clear(_authenticationContext);
|
|
ClearRotationSecrets();
|
|
_state = HandshakeState.Disposed;
|
|
}
|
|
}
|
|
}
|