Files
esiur-dotnet/Libraries/Esiur/Security/Authority/Providers/Ppap/PpapAuthenticationHandler.Rotation.cs
T
2026-07-16 19:36:30 +03:00

454 lines
18 KiB
C#

using System;
using System.IO;
namespace Esiur.Security.Authority.Providers.Ppap;
public sealed partial class PpapAuthenticationHandler
{
enum RotationState
{
NotStarted,
SubjectAwaitChallenge,
SubjectAwaitCommit,
VerifierAwaitProof,
ResponderAwaitNext,
InitiatorAwaitResponderOffer,
InitiatorAwaitResponderCommitAck,
ResponderAwaitDone,
InitiatorDoneSent,
Complete,
Failed,
}
RotationState _rotationState;
PpapIdentityRole _rotationRole;
PpapRegistrationRecord _pendingRotation;
byte[] _rotationPrivateKey;
byte[] _rotationChallengeSecret;
byte[] _rotationOffer;
byte[] _rotationChallenge;
/// <summary>
/// True for every live PPAP handler because protocol completion requires an
/// encrypted post-authentication phase. Static identities use a no-op Done exchange.
/// </summary>
public bool RequiresKeyRotation
{
get
{
lock (_sync)
{
// PPAP always requires the encrypted post-authentication phase. It is
// a no-op Done exchange when every authenticated identity is static.
return _state != HandshakeState.Failed
&& _state != HandshakeState.Disposed;
}
}
}
public AuthenticationKeyRotationResult BeginKeyRotation()
{
lock (_sync)
{
try
{
if (_direction != AuthenticationDirection.Initiator
|| _state != HandshakeState.Complete
|| _rotationState != RotationState.NotStarted
|| !RequiresKeyRotation)
throw new InvalidOperationException("PPAP key rotation cannot be started in the current state.");
if (RoleRequiresRotation(PpapIdentityRole.Initiator))
return CreateRotationOffer(PpapIdentityRole.Initiator);
if (!RoleRequiresRotation(PpapIdentityRole.Responder))
{
_rotationState = RotationState.InitiatorDoneSent;
var done = PpapWire.EncodeRotationDone();
ClearCompletedRotationSecrets();
return RotationSucceeded(done);
}
_rotationState = RotationState.InitiatorAwaitResponderOffer;
return RotationInProgress(PpapWire.EncodeRotationStart(
PpapIdentityRole.Responder));
}
catch
{
_rotationState = RotationState.Failed;
ClearCompletedRotationSecrets();
return RotationFailed();
}
}
}
public AuthenticationKeyRotationResult ProcessKeyRotation(object data)
{
lock (_sync)
{
try
{
if (_state != HandshakeState.Complete || !RequiresKeyRotation)
throw new InvalidOperationException("PPAP key rotation is not available.");
if (_direction == AuthenticationDirection.Initiator)
return ProcessInitiatorRotation(data);
return ProcessResponderRotation(data);
}
catch
{
_rotationState = RotationState.Failed;
ClearCompletedRotationSecrets();
return RotationFailed();
}
}
}
AuthenticationKeyRotationResult ProcessInitiatorRotation(object data)
{
if (_rotationState == RotationState.SubjectAwaitChallenge)
return ProcessRotationChallenge(data, PpapIdentityRole.Initiator);
if (_rotationState == RotationState.SubjectAwaitCommit)
{
var committedVersion = PpapWire.DecodeRotationCommit(data,
PpapIdentityRole.Initiator);
CommitSubjectRotation(PpapIdentityRole.Initiator, committedVersion);
if (RoleRequiresRotation(PpapIdentityRole.Responder))
{
_rotationState = RotationState.InitiatorAwaitResponderOffer;
return RotationInProgress(PpapWire.EncodeRotationStart(
PpapIdentityRole.Responder));
}
_rotationState = RotationState.InitiatorDoneSent;
var done = PpapWire.EncodeRotationDone();
ClearCompletedRotationSecrets();
return RotationSucceeded(done);
}
if (_rotationState == RotationState.InitiatorAwaitResponderOffer)
return ProcessRotationOffer(data, PpapIdentityRole.Responder);
if (_rotationState == RotationState.VerifierAwaitProof)
return ProcessRotationProof(data, PpapIdentityRole.Responder);
if (_rotationState == RotationState.InitiatorAwaitResponderCommitAck)
{
var version = PpapWire.DecodeRotationCommitAck(data,
PpapIdentityRole.Responder);
if (_responderRegistration == null
|| version != _responderRegistration.Version)
throw new InvalidDataException("Unexpected responder rotation version.");
_rotationState = RotationState.InitiatorDoneSent;
var done = PpapWire.EncodeRotationDone();
ClearCompletedRotationSecrets();
return RotationSucceeded(done);
}
throw new InvalidDataException("Unexpected PPAP initiator rotation message.");
}
AuthenticationKeyRotationResult ProcessResponderRotation(object data)
{
if (_rotationState == RotationState.NotStarted)
{
var type = PpapWire.PeekMessageType(data);
if (type == PpapMessageType.RotationDone
&& !RoleRequiresRotation(PpapIdentityRole.Initiator)
&& !RoleRequiresRotation(PpapIdentityRole.Responder))
return ProcessRotationDone(data);
if (type == PpapMessageType.RotationOffer
&& RoleRequiresRotation(PpapIdentityRole.Initiator))
return ProcessRotationOffer(data, PpapIdentityRole.Initiator);
if (type == PpapMessageType.RotationStart
&& !RoleRequiresRotation(PpapIdentityRole.Initiator)
&& RoleRequiresRotation(PpapIdentityRole.Responder))
{
var role = PpapWire.DecodeRotationStart(data);
if (role != PpapIdentityRole.Responder)
throw new InvalidDataException("Unexpected rotation role.");
return CreateRotationOffer(PpapIdentityRole.Responder);
}
throw new InvalidDataException("Unexpected initial PPAP rotation message.");
}
if (_rotationState == RotationState.VerifierAwaitProof)
return ProcessRotationProof(data, PpapIdentityRole.Initiator);
if (_rotationState == RotationState.ResponderAwaitNext)
{
var type = PpapWire.PeekMessageType(data);
if (type == PpapMessageType.RotationStart
&& RoleRequiresRotation(PpapIdentityRole.Responder))
{
var role = PpapWire.DecodeRotationStart(data);
if (role != PpapIdentityRole.Responder)
throw new InvalidDataException("Unexpected rotation role.");
return CreateRotationOffer(PpapIdentityRole.Responder);
}
if (type == PpapMessageType.RotationDone
&& !RoleRequiresRotation(PpapIdentityRole.Responder))
return ProcessRotationDone(data);
throw new InvalidDataException("Unexpected PPAP rotation continuation.");
}
if (_rotationState == RotationState.SubjectAwaitChallenge)
return ProcessRotationChallenge(data, PpapIdentityRole.Responder);
if (_rotationState == RotationState.SubjectAwaitCommit)
{
var committedVersion = PpapWire.DecodeRotationCommit(data,
PpapIdentityRole.Responder);
CommitSubjectRotation(PpapIdentityRole.Responder, committedVersion);
_rotationState = RotationState.ResponderAwaitDone;
return RotationInProgress(PpapWire.EncodeRotationCommitAck(
PpapIdentityRole.Responder, committedVersion));
}
if (_rotationState == RotationState.ResponderAwaitDone)
return ProcessRotationDone(data);
throw new InvalidDataException("Unexpected PPAP responder rotation message.");
}
AuthenticationKeyRotationResult CreateRotationOffer(PpapIdentityRole role)
{
if (!LocalIsSubject(role) || !RoleRequiresRotation(role)
|| _localIdentity == null
|| _localIdentity.Kind != PpapIdentityKind.PasswordDerived)
throw new InvalidOperationException("The local endpoint cannot rotate this identity role.");
var current = GetRoleRegistration(role);
var nonce = PpapCryptography.RandomBytes(PpapProtocol.RegistrationNonceLength);
byte[] publicKey = null;
try
{
_rotationPrivateKey = _provider.DerivePrivateKey(_localIdentity,
_domain, nonce, _localIdentity.KdfProfile,
postAuthentication: true);
publicKey = PpapCryptography.GetPublicKey(_rotationPrivateKey);
_pendingRotation = new PpapRegistrationRecord(current.Version + 1,
current.Identity, PpapIdentityKind.PasswordDerived, nonce,
publicKey, _localIdentity.KdfProfile);
var offer = PpapWire.EncodeRotationOffer(role, current.Identity,
current.Version, nonce, publicKey, _localIdentity.KdfProfile);
ReplaceRotationBytes(ref _rotationOffer, offer);
_rotationRole = role;
_rotationState = RotationState.SubjectAwaitChallenge;
return RotationInProgress(offer);
}
finally
{
PpapCryptography.Clear(publicKey);
PpapCryptography.Clear(nonce);
}
}
AuthenticationKeyRotationResult ProcessRotationOffer(object data,
PpapIdentityRole expectedRole)
{
if (LocalIsSubject(expectedRole))
throw new InvalidOperationException("A subject cannot verify its own rotation.");
var raw = RequireBytes(data);
var offer = PpapWire.DecodeRotationOffer(raw);
if (offer.Role != expectedRole)
throw new InvalidDataException("Unexpected rotation role.");
var current = GetRoleRegistration(expectedRole);
if (current == null
|| current.Kind != PpapIdentityKind.PasswordDerived
|| !string.Equals(current.Identity, offer.Identity, StringComparison.Ordinal)
|| current.Version != offer.ExpectedVersion
|| !current.KdfProfile.Equals(offer.KdfProfile))
throw new InvalidDataException("The rotation offer does not match the authenticated registration.");
_pendingRotation = new PpapRegistrationRecord(current.Version + 1,
current.Identity, PpapIdentityKind.PasswordDerived, offer.Nonce,
offer.EncapsulationKey, offer.KdfProfile);
PpapCryptography.Encapsulate(offer.EncapsulationKey,
out var ciphertext, out _rotationChallengeSecret);
try
{
var challenge = PpapWire.EncodeRotationChallenge(expectedRole,
ciphertext);
ReplaceRotationBytes(ref _rotationOffer, raw);
ReplaceRotationBytes(ref _rotationChallenge, challenge);
_rotationRole = expectedRole;
_rotationState = RotationState.VerifierAwaitProof;
return RotationInProgress(challenge);
}
finally
{
PpapCryptography.Clear(ciphertext);
PpapCryptography.Clear(offer.Nonce);
PpapCryptography.Clear(offer.EncapsulationKey);
}
}
AuthenticationKeyRotationResult ProcessRotationChallenge(object data,
PpapIdentityRole expectedRole)
{
if (!LocalIsSubject(expectedRole) || _rotationRole != expectedRole
|| _rotationPrivateKey == null || _pendingRotation == null)
throw new InvalidOperationException("Unexpected rotation challenge.");
var raw = RequireBytes(data);
var challenge = PpapWire.DecodeRotationChallenge(raw);
if (challenge.Role != expectedRole)
throw new InvalidDataException("Unexpected rotation challenge role.");
var secret = PpapCryptography.Decapsulate(_rotationPrivateKey,
challenge.Ciphertext);
try
{
ReplaceRotationBytes(ref _rotationChallenge, raw);
var proof = PpapCryptography.ComputeRotationProof(secret,
_sessionKey, _rotationOffer, _rotationChallenge);
try
{
_rotationState = RotationState.SubjectAwaitCommit;
return RotationInProgress(PpapWire.EncodeRotationProof(
expectedRole, proof));
}
finally
{
PpapCryptography.Clear(proof);
}
}
finally
{
PpapCryptography.Clear(secret);
PpapCryptography.Clear(challenge.Ciphertext);
PpapCryptography.Clear(_rotationPrivateKey);
_rotationPrivateKey = null;
}
}
AuthenticationKeyRotationResult ProcessRotationProof(object data,
PpapIdentityRole expectedRole)
{
if (LocalIsSubject(expectedRole) || _rotationRole != expectedRole
|| _rotationChallengeSecret == null || _pendingRotation == null)
throw new InvalidOperationException("Unexpected rotation proof.");
var proof = PpapWire.DecodeRotationProof(data);
if (proof.Role != expectedRole)
throw new InvalidDataException("Unexpected rotation proof role.");
var expected = PpapCryptography.ComputeRotationProof(
_rotationChallengeSecret, _sessionKey, _rotationOffer,
_rotationChallenge);
var verified = PpapCryptography.FixedTimeEquals(expected, proof.Proof);
PpapCryptography.Clear(expected);
PpapCryptography.Clear(proof.Proof);
if (!verified)
throw new InvalidDataException("Rotation proof verification failed.");
var current = GetRoleRegistration(expectedRole);
if (!_provider.Registrations.TryRotate(_domain, current.Identity,
current.Version, _pendingRotation))
throw new InvalidOperationException("The registration changed concurrently.");
SetRoleRegistration(expectedRole, _pendingRotation);
var committedVersion = _pendingRotation.Version;
_pendingRotation = null;
ClearRotationExchangeSecrets();
if (expectedRole == PpapIdentityRole.Initiator)
_rotationState = RotationState.ResponderAwaitNext;
else
_rotationState = RotationState.InitiatorAwaitResponderCommitAck;
return RotationInProgress(PpapWire.EncodeRotationCommit(expectedRole,
committedVersion));
}
void CommitSubjectRotation(PpapIdentityRole role, long committedVersion)
{
if (!LocalIsSubject(role) || _rotationRole != role
|| _pendingRotation == null
|| _pendingRotation.Version != committedVersion)
throw new InvalidDataException("Unexpected rotation commit.");
SetRoleRegistration(role, _pendingRotation);
_pendingRotation = null;
ClearRotationExchangeSecrets();
}
AuthenticationKeyRotationResult ProcessRotationDone(object data)
{
PpapWire.DecodeRotationDone(data);
_rotationState = RotationState.Complete;
ClearCompletedRotationSecrets();
return RotationSucceeded(null);
}
bool RoleRequiresRotation(PpapIdentityRole role)
{
var registration = GetRoleRegistration(role);
return registration != null
&& registration.Kind == PpapIdentityKind.PasswordDerived;
}
bool LocalIsSubject(PpapIdentityRole role)
=> (_direction == AuthenticationDirection.Initiator
&& role == PpapIdentityRole.Initiator)
|| (_direction == AuthenticationDirection.Responder
&& role == PpapIdentityRole.Responder);
PpapRegistrationRecord GetRoleRegistration(PpapIdentityRole role)
=> role == PpapIdentityRole.Initiator
? _initiatorRegistration : _responderRegistration;
void SetRoleRegistration(PpapIdentityRole role,
PpapRegistrationRecord registration)
{
if (role == PpapIdentityRole.Initiator)
_initiatorRegistration = registration;
else
_responderRegistration = registration;
}
static AuthenticationKeyRotationResult RotationInProgress(byte[] data)
=> new AuthenticationKeyRotationResult(
AuthenticationKeyRotationRuling.InProgress, data);
static AuthenticationKeyRotationResult RotationSucceeded(byte[] data)
=> new AuthenticationKeyRotationResult(
AuthenticationKeyRotationRuling.Succeeded, data);
static AuthenticationKeyRotationResult RotationFailed()
=> new AuthenticationKeyRotationResult(
AuthenticationKeyRotationRuling.Failed, null,
"PPAP authentication key rotation failed.");
static void ReplaceRotationBytes(ref byte[] target, byte[] value)
{
PpapCryptography.Clear(target);
target = value == null ? null : (byte[])value.Clone();
}
void ClearRotationExchangeSecrets()
{
PpapCryptography.Clear(_rotationPrivateKey);
PpapCryptography.Clear(_rotationChallengeSecret);
PpapCryptography.Clear(_rotationOffer);
PpapCryptography.Clear(_rotationChallenge);
_rotationPrivateKey = null;
_rotationChallengeSecret = null;
_rotationOffer = null;
_rotationChallenge = null;
}
void ClearRotationSecrets()
{
ClearRotationExchangeSecrets();
_pendingRotation = null;
}
void ClearCompletedRotationSecrets()
{
ClearRotationSecrets();
PpapCryptography.Clear(_sessionKey);
_sessionKey = null;
}
}