mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
PPAP integration
This commit is contained in:
+453
@@ -0,0 +1,453 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user