mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
784 lines
31 KiB
C#
784 lines
31 KiB
C#
using System.Buffers.Binary;
|
|
using Esiur.Security.Authority;
|
|
using Esiur.Security.Authority.Providers.Ppap;
|
|
|
|
namespace Esiur.Tests.Unit;
|
|
|
|
public class PpapAuthenticationTests
|
|
{
|
|
const string Domain = "example.test";
|
|
const string InitiatorIdentity = "alice";
|
|
const string ResponderIdentity = "server";
|
|
const string InitiatorPassword = "correct horse battery staple";
|
|
const string ResponderPassword = "server paper password";
|
|
|
|
static readonly PpapKdfProfile TestKdf = new(
|
|
PpapKdfProfile.Argon2Version13,
|
|
memoryKiB: 8 * 1024,
|
|
iterations: 1,
|
|
parallelism: 1);
|
|
|
|
[Theory]
|
|
[InlineData(AuthenticationMode.InitializerIdentity)]
|
|
[InlineData(AuthenticationMode.ResponderIdentity)]
|
|
[InlineData(AuthenticationMode.DualIdentity)]
|
|
public void PasswordHandshake_AllIdentityModesDeriveMatchingKeys(
|
|
AuthenticationMode mode)
|
|
{
|
|
using var pair = CreatePair(mode);
|
|
|
|
var result = CompleteHandshake(pair.Initiator, pair.Responder);
|
|
|
|
var authenticatesInitiator = mode is AuthenticationMode.InitializerIdentity
|
|
or AuthenticationMode.DualIdentity;
|
|
var authenticatesResponder = mode is AuthenticationMode.ResponderIdentity
|
|
or AuthenticationMode.DualIdentity;
|
|
|
|
Assert.Equal(PpapProtocol.SessionKeyLength, result.Initiator.SessionKey.Length);
|
|
Assert.Equal(result.Initiator.SessionKey, result.Responder.SessionKey);
|
|
Assert.Equal(authenticatesInitiator ? InitiatorIdentity : null,
|
|
result.Initiator.LocalIdentity);
|
|
Assert.Equal(authenticatesResponder ? ResponderIdentity : null,
|
|
result.Initiator.RemoteIdentity);
|
|
Assert.Equal(authenticatesResponder ? ResponderIdentity : null,
|
|
result.Responder.LocalIdentity);
|
|
Assert.Equal(authenticatesInitiator ? InitiatorIdentity : null,
|
|
result.Responder.RemoteIdentity);
|
|
}
|
|
|
|
[Fact]
|
|
public void WrongPassword_FailsWithoutChangingRegistration()
|
|
{
|
|
using var pair = CreatePair(
|
|
AuthenticationMode.InitializerIdentity,
|
|
initiatorPassword: "not the registered password");
|
|
var before = Snapshot(pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
|
|
|
var first = pair.Initiator.Process(null!);
|
|
var second = pair.Responder.Process(Wire(first, PpapMessageType.ClientHello));
|
|
var third = pair.Initiator.Process(Wire(second, PpapMessageType.ServerHello));
|
|
var fourth = pair.Responder.Process(Wire(third, PpapMessageType.InitiatorProof));
|
|
var failed = pair.Initiator.Process(Wire(fourth, PpapMessageType.ResponderProof));
|
|
|
|
Assert.Equal(AuthenticationRuling.Failed, failed.Ruling);
|
|
Assert.Null(failed.SessionKey);
|
|
AssertSnapshot(before, pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
|
}
|
|
|
|
[Fact]
|
|
public void TamperedResponderFinished_FailsClosed()
|
|
{
|
|
using var pair = CreatePair(AuthenticationMode.InitializerIdentity);
|
|
|
|
var first = pair.Initiator.Process(null!);
|
|
var second = pair.Responder.Process(Wire(first, PpapMessageType.ClientHello));
|
|
var third = pair.Initiator.Process(Wire(second, PpapMessageType.ServerHello));
|
|
var fourth = pair.Responder.Process(Wire(third, PpapMessageType.InitiatorProof));
|
|
var tampered = TamperLastByte(Wire(fourth, PpapMessageType.ResponderProof));
|
|
|
|
var failed = pair.Initiator.Process(tampered);
|
|
|
|
Assert.Equal(AuthenticationRuling.Failed, failed.Ruling);
|
|
Assert.Null(failed.SessionKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void TamperedInitiatorFinished_FailsClosed()
|
|
{
|
|
using var pair = CreatePair(AuthenticationMode.InitializerIdentity);
|
|
|
|
var first = pair.Initiator.Process(null!);
|
|
var second = pair.Responder.Process(Wire(first, PpapMessageType.ClientHello));
|
|
var third = pair.Initiator.Process(Wire(second, PpapMessageType.ServerHello));
|
|
var fourth = pair.Responder.Process(Wire(third, PpapMessageType.InitiatorProof));
|
|
var fifth = pair.Initiator.Process(Wire(fourth, PpapMessageType.ResponderProof));
|
|
var tampered = TamperLastByte(Wire(fifth, PpapMessageType.InitiatorFinished));
|
|
|
|
var failed = pair.Responder.Process(tampered);
|
|
|
|
Assert.Equal(AuthenticationRuling.Failed, failed.Ruling);
|
|
Assert.Null(failed.SessionKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void DualIdentityProofs_DoNotExposeRegistrationDescriptors()
|
|
{
|
|
using var pair = CreatePair(AuthenticationMode.DualIdentity);
|
|
var initiatorRegistration = pair.ResponderStore.Get(
|
|
Domain,
|
|
InitiatorIdentity);
|
|
var responderRegistration = pair.InitiatorStore.Get(
|
|
Domain,
|
|
ResponderIdentity);
|
|
|
|
var first = pair.Initiator.Process(null!);
|
|
var second = pair.Responder.Process(
|
|
Wire(first, PpapMessageType.ClientHello));
|
|
var third = pair.Initiator.Process(
|
|
Wire(second, PpapMessageType.ServerHello));
|
|
var initiatorProof = Wire(third, PpapMessageType.InitiatorProof);
|
|
var fourth = pair.Responder.Process(initiatorProof);
|
|
var responderProof = Wire(fourth, PpapMessageType.ResponderProof);
|
|
|
|
AssertDescriptorNotVisible(initiatorProof, responderRegistration);
|
|
AssertDescriptorNotVisible(responderProof, initiatorRegistration);
|
|
}
|
|
|
|
[Fact]
|
|
public void TamperedEncryptedResponderDescriptor_FailsClosed()
|
|
{
|
|
using var pair = CreatePair(AuthenticationMode.DualIdentity);
|
|
var first = pair.Initiator.Process(null!);
|
|
var second = pair.Responder.Process(
|
|
Wire(first, PpapMessageType.ClientHello));
|
|
var third = pair.Initiator.Process(
|
|
Wire(second, PpapMessageType.ServerHello));
|
|
var tampered = TamperField(
|
|
Wire(third, PpapMessageType.InitiatorProof),
|
|
fieldIndex: 2);
|
|
|
|
var failed = pair.Responder.Process(tampered);
|
|
|
|
Assert.Equal(AuthenticationRuling.Failed, failed.Ruling);
|
|
Assert.Null(failed.SessionKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void TamperedEncryptedInitiatorDescriptor_FailsClosed()
|
|
{
|
|
using var pair = CreatePair(AuthenticationMode.DualIdentity);
|
|
var first = pair.Initiator.Process(null!);
|
|
var second = pair.Responder.Process(
|
|
Wire(first, PpapMessageType.ClientHello));
|
|
var third = pair.Initiator.Process(
|
|
Wire(second, PpapMessageType.ServerHello));
|
|
var fourth = pair.Responder.Process(
|
|
Wire(third, PpapMessageType.InitiatorProof));
|
|
var tampered = TamperField(
|
|
Wire(fourth, PpapMessageType.ResponderProof),
|
|
fieldIndex: 1);
|
|
|
|
var failed = pair.Initiator.Process(tampered);
|
|
|
|
Assert.Equal(AuthenticationRuling.Failed, failed.Ruling);
|
|
Assert.Null(failed.SessionKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProtectedDescriptor_DiffersAcrossFreshSessionsForSameRegistration()
|
|
{
|
|
using var pair = CreatePair(AuthenticationMode.DualIdentity);
|
|
var firstProof = CreateInitiatorProof(pair.Initiator, pair.Responder);
|
|
var initiatorProvider = Assert.IsType<PpapAuthenticationProvider>(
|
|
pair.Initiator.Provider);
|
|
var responderProvider = Assert.IsType<PpapAuthenticationProvider>(
|
|
pair.Responder.Provider);
|
|
using var secondInitiator = CreateHandler(
|
|
initiatorProvider,
|
|
AuthenticationDirection.Initiator,
|
|
AuthenticationMode.DualIdentity);
|
|
using var secondResponder = CreateHandler(
|
|
responderProvider,
|
|
AuthenticationDirection.Responder,
|
|
AuthenticationMode.DualIdentity);
|
|
var secondProof = CreateInitiatorProof(secondInitiator, secondResponder);
|
|
|
|
var firstDescriptor = ExtractField(firstProof, fieldIndex: 2);
|
|
var secondDescriptor = ExtractField(secondProof, fieldIndex: 2);
|
|
|
|
Assert.Equal(144, firstDescriptor.Length);
|
|
Assert.Equal(PpapProtocol.ProtectedDescriptorLength, firstDescriptor.Length);
|
|
Assert.Equal(144, secondDescriptor.Length);
|
|
Assert.False(firstDescriptor.SequenceEqual(secondDescriptor));
|
|
}
|
|
|
|
[Fact]
|
|
public void InitializerPassword_RotationChangesVersionNonceAndKey()
|
|
{
|
|
using var pair = CreatePair(AuthenticationMode.InitializerIdentity);
|
|
CompleteHandshake(pair.Initiator, pair.Responder);
|
|
var before = Snapshot(pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
|
|
|
CompleteInitializerRotation(pair.Initiator, pair.Responder);
|
|
|
|
var after = pair.ResponderStore.Get(Domain, InitiatorIdentity);
|
|
Assert.Equal(before.Version + 1, after.Version);
|
|
Assert.False(before.Nonce.SequenceEqual(after.Nonce));
|
|
Assert.False(before.EncapsulationKey.SequenceEqual(after.EncapsulationKey));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResponderPassword_RotationChangesVerifierRecord()
|
|
{
|
|
using var pair = CreatePair(AuthenticationMode.ResponderIdentity);
|
|
CompleteHandshake(pair.Initiator, pair.Responder);
|
|
var before = Snapshot(pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
|
|
|
CompleteResponderRotation(pair.Initiator, pair.Responder);
|
|
|
|
AssertRotated(
|
|
before,
|
|
pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
|
}
|
|
|
|
[Fact]
|
|
public void DualPassword_RotationChangesBothVerifierRecords()
|
|
{
|
|
using var pair = CreatePair(AuthenticationMode.DualIdentity);
|
|
CompleteHandshake(pair.Initiator, pair.Responder);
|
|
var initiatorBefore = Snapshot(
|
|
pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
|
var responderBefore = Snapshot(
|
|
pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
|
|
|
CompleteDualRotation(pair.Initiator, pair.Responder);
|
|
|
|
AssertRotated(
|
|
initiatorBefore,
|
|
pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
|
AssertRotated(
|
|
responderBefore,
|
|
pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
|
}
|
|
|
|
[Fact]
|
|
public void TamperedRotationProof_FailsWithoutPersistingOffer()
|
|
{
|
|
using var pair = CreatePair(AuthenticationMode.InitializerIdentity);
|
|
CompleteHandshake(pair.Initiator, pair.Responder);
|
|
var before = Snapshot(pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
|
|
|
var offer = pair.Initiator.BeginKeyRotation();
|
|
var challenge = pair.Responder.ProcessKeyRotation(
|
|
RotationWire(offer, PpapMessageType.RotationOffer));
|
|
var proof = pair.Initiator.ProcessKeyRotation(
|
|
RotationWire(challenge, PpapMessageType.RotationChallenge));
|
|
var tampered = TamperLastByte(
|
|
RotationWire(proof, PpapMessageType.RotationProof));
|
|
|
|
var failed = pair.Responder.ProcessKeyRotation(tampered);
|
|
|
|
Assert.Equal(AuthenticationKeyRotationRuling.Failed, failed.Ruling);
|
|
AssertSnapshot(before, pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
|
}
|
|
|
|
[Fact]
|
|
public void StaticDualIdentity_UsesEncryptedNoOpCompletionWithoutChangingRecords()
|
|
{
|
|
using var pair = CreateStaticPair();
|
|
var initiatorBefore = Snapshot(
|
|
pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
|
var responderBefore = Snapshot(
|
|
pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
|
CompleteHandshake(pair.Initiator, pair.Responder);
|
|
|
|
var done = pair.Initiator.BeginKeyRotation();
|
|
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, done.Ruling);
|
|
var completed = pair.Responder.ProcessKeyRotation(
|
|
RotationWire(done, PpapMessageType.RotationDone));
|
|
|
|
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, completed.Ruling);
|
|
Assert.Null(completed.Data);
|
|
AssertSnapshot(
|
|
initiatorBefore,
|
|
pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
|
AssertSnapshot(
|
|
responderBefore,
|
|
pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
|
}
|
|
|
|
static HandshakeResult CompleteHandshake(
|
|
PpapAuthenticationHandler initiator,
|
|
PpapAuthenticationHandler responder)
|
|
{
|
|
var first = initiator.Process(null!);
|
|
Assert.Equal(AuthenticationRuling.InProgress, first.Ruling);
|
|
|
|
var second = responder.Process(Wire(first, PpapMessageType.ClientHello));
|
|
Assert.Equal(AuthenticationRuling.InProgress, second.Ruling);
|
|
|
|
var third = initiator.Process(Wire(second, PpapMessageType.ServerHello));
|
|
Assert.Equal(AuthenticationRuling.InProgress, third.Ruling);
|
|
|
|
var fourth = responder.Process(Wire(third, PpapMessageType.InitiatorProof));
|
|
Assert.Equal(AuthenticationRuling.InProgress, fourth.Ruling);
|
|
|
|
var fifth = initiator.Process(Wire(fourth, PpapMessageType.ResponderProof));
|
|
Assert.Equal(AuthenticationRuling.Succeeded, fifth.Ruling);
|
|
|
|
var sixth = responder.Process(Wire(fifth, PpapMessageType.InitiatorFinished));
|
|
Assert.Equal(AuthenticationRuling.Succeeded, sixth.Ruling);
|
|
|
|
return new HandshakeResult(fifth, sixth);
|
|
}
|
|
|
|
static byte[] CreateInitiatorProof(
|
|
PpapAuthenticationHandler initiator,
|
|
PpapAuthenticationHandler responder)
|
|
{
|
|
var first = initiator.Process(null!);
|
|
var second = responder.Process(Wire(first, PpapMessageType.ClientHello));
|
|
var third = initiator.Process(Wire(second, PpapMessageType.ServerHello));
|
|
return Wire(third, PpapMessageType.InitiatorProof);
|
|
}
|
|
|
|
static PpapAuthenticationHandler CreateHandler(
|
|
PpapAuthenticationProvider provider,
|
|
AuthenticationDirection direction,
|
|
AuthenticationMode mode)
|
|
=> Assert.IsType<PpapAuthenticationHandler>(
|
|
provider.CreateAuthenticationHandler(new AuthenticationContext
|
|
{
|
|
Direction = direction,
|
|
Mode = mode,
|
|
Domain = Domain,
|
|
InitiatorIdentity = InitiatorIdentity,
|
|
ResponderIdentity = ResponderIdentity,
|
|
HostName = ResponderIdentity,
|
|
}));
|
|
|
|
static void CompleteInitializerRotation(
|
|
PpapAuthenticationHandler initiator,
|
|
PpapAuthenticationHandler responder)
|
|
{
|
|
Assert.True(initiator.RequiresKeyRotation);
|
|
Assert.True(responder.RequiresKeyRotation);
|
|
|
|
var offer = initiator.BeginKeyRotation();
|
|
Assert.Equal(AuthenticationKeyRotationRuling.InProgress, offer.Ruling);
|
|
|
|
var challenge = responder.ProcessKeyRotation(
|
|
RotationWire(offer, PpapMessageType.RotationOffer));
|
|
Assert.Equal(AuthenticationKeyRotationRuling.InProgress, challenge.Ruling);
|
|
|
|
var proof = initiator.ProcessKeyRotation(
|
|
RotationWire(challenge, PpapMessageType.RotationChallenge));
|
|
Assert.Equal(AuthenticationKeyRotationRuling.InProgress, proof.Ruling);
|
|
|
|
var commit = responder.ProcessKeyRotation(
|
|
RotationWire(proof, PpapMessageType.RotationProof));
|
|
Assert.Equal(AuthenticationKeyRotationRuling.InProgress, commit.Ruling);
|
|
|
|
var done = initiator.ProcessKeyRotation(
|
|
RotationWire(commit, PpapMessageType.RotationCommit));
|
|
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, done.Ruling);
|
|
|
|
var completed = responder.ProcessKeyRotation(
|
|
RotationWire(done, PpapMessageType.RotationDone));
|
|
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, completed.Ruling);
|
|
Assert.Null(completed.Data);
|
|
}
|
|
|
|
static void CompleteResponderRotation(
|
|
PpapAuthenticationHandler initiator,
|
|
PpapAuthenticationHandler responder)
|
|
{
|
|
var start = initiator.BeginKeyRotation();
|
|
Assert.Equal(AuthenticationKeyRotationRuling.InProgress, start.Ruling);
|
|
|
|
var offer = responder.ProcessKeyRotation(
|
|
RotationWire(start, PpapMessageType.RotationStart));
|
|
var challenge = initiator.ProcessKeyRotation(
|
|
RotationWire(offer, PpapMessageType.RotationOffer));
|
|
var proof = responder.ProcessKeyRotation(
|
|
RotationWire(challenge, PpapMessageType.RotationChallenge));
|
|
var commit = initiator.ProcessKeyRotation(
|
|
RotationWire(proof, PpapMessageType.RotationProof));
|
|
var acknowledgement = responder.ProcessKeyRotation(
|
|
RotationWire(commit, PpapMessageType.RotationCommit));
|
|
var done = initiator.ProcessKeyRotation(
|
|
RotationWire(acknowledgement, PpapMessageType.RotationCommitAck));
|
|
var completed = responder.ProcessKeyRotation(
|
|
RotationWire(done, PpapMessageType.RotationDone));
|
|
|
|
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, done.Ruling);
|
|
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, completed.Ruling);
|
|
}
|
|
|
|
static void CompleteDualRotation(
|
|
PpapAuthenticationHandler initiator,
|
|
PpapAuthenticationHandler responder)
|
|
{
|
|
var initiatorOffer = initiator.BeginKeyRotation();
|
|
var initiatorChallenge = responder.ProcessKeyRotation(
|
|
RotationWire(initiatorOffer, PpapMessageType.RotationOffer));
|
|
var initiatorProof = initiator.ProcessKeyRotation(
|
|
RotationWire(initiatorChallenge, PpapMessageType.RotationChallenge));
|
|
var initiatorCommit = responder.ProcessKeyRotation(
|
|
RotationWire(initiatorProof, PpapMessageType.RotationProof));
|
|
var responderStart = initiator.ProcessKeyRotation(
|
|
RotationWire(initiatorCommit, PpapMessageType.RotationCommit));
|
|
|
|
var responderOffer = responder.ProcessKeyRotation(
|
|
RotationWire(responderStart, PpapMessageType.RotationStart));
|
|
var responderChallenge = initiator.ProcessKeyRotation(
|
|
RotationWire(responderOffer, PpapMessageType.RotationOffer));
|
|
var responderProof = responder.ProcessKeyRotation(
|
|
RotationWire(responderChallenge, PpapMessageType.RotationChallenge));
|
|
var responderCommit = initiator.ProcessKeyRotation(
|
|
RotationWire(responderProof, PpapMessageType.RotationProof));
|
|
var acknowledgement = responder.ProcessKeyRotation(
|
|
RotationWire(responderCommit, PpapMessageType.RotationCommit));
|
|
var done = initiator.ProcessKeyRotation(
|
|
RotationWire(acknowledgement, PpapMessageType.RotationCommitAck));
|
|
var completed = responder.ProcessKeyRotation(
|
|
RotationWire(done, PpapMessageType.RotationDone));
|
|
|
|
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, done.Ruling);
|
|
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, completed.Ruling);
|
|
}
|
|
|
|
static byte[] Wire(AuthenticationResult result, PpapMessageType expectedType)
|
|
{
|
|
var wire = Assert.IsType<byte[]>(result.AuthenticationData);
|
|
Assert.True(wire.Length >= 10);
|
|
Assert.Equal(expectedType, (PpapMessageType)wire[5]);
|
|
return wire;
|
|
}
|
|
|
|
static byte[] RotationWire(
|
|
AuthenticationKeyRotationResult result,
|
|
PpapMessageType expectedType)
|
|
{
|
|
var wire = Assert.IsType<byte[]>(result.Data);
|
|
Assert.True(wire.Length >= 10);
|
|
Assert.Equal(expectedType, (PpapMessageType)wire[5]);
|
|
return wire;
|
|
}
|
|
|
|
static byte[] TamperLastByte(byte[] wire)
|
|
{
|
|
var tampered = (byte[])wire.Clone();
|
|
tampered[^1] ^= 0x80;
|
|
return tampered;
|
|
}
|
|
|
|
static byte[] TamperField(byte[] wire, int fieldIndex)
|
|
{
|
|
var tampered = (byte[])wire.Clone();
|
|
var offset = 10;
|
|
for (var index = 0; index <= fieldIndex; index++)
|
|
{
|
|
Assert.True(tampered.Length - offset >= sizeof(int));
|
|
var length = BinaryPrimitives.ReadInt32BigEndian(
|
|
tampered.AsSpan(offset, sizeof(int)));
|
|
offset += sizeof(int);
|
|
Assert.InRange(length, 1, tampered.Length - offset);
|
|
|
|
if (index == fieldIndex)
|
|
{
|
|
tampered[offset + (length / 2)] ^= 0x80;
|
|
return tampered;
|
|
}
|
|
|
|
offset += length;
|
|
}
|
|
|
|
throw new InvalidOperationException("The requested PPAP field was not found.");
|
|
}
|
|
|
|
static byte[] ExtractField(byte[] wire, int fieldIndex)
|
|
{
|
|
var offset = 10;
|
|
for (var index = 0; index <= fieldIndex; index++)
|
|
{
|
|
Assert.True(wire.Length - offset >= sizeof(int));
|
|
var length = BinaryPrimitives.ReadInt32BigEndian(
|
|
wire.AsSpan(offset, sizeof(int)));
|
|
offset += sizeof(int);
|
|
Assert.InRange(length, 1, wire.Length - offset);
|
|
|
|
if (index == fieldIndex)
|
|
return wire.AsSpan(offset, length).ToArray();
|
|
|
|
offset += length;
|
|
}
|
|
|
|
throw new InvalidOperationException("The requested PPAP field was not found.");
|
|
}
|
|
|
|
static void AssertDescriptorNotVisible(
|
|
byte[] wire,
|
|
PpapRegistrationRecord registration)
|
|
{
|
|
Assert.False(ContainsSubsequence(wire, registration.Nonce),
|
|
"The PPAP proof contains the plaintext registration nonce.");
|
|
Assert.False(ContainsSubsequence(
|
|
wire,
|
|
EncodeDescriptorVersionPrefix(registration)),
|
|
"The PPAP proof contains the plaintext registration version descriptor.");
|
|
Assert.False(ContainsSubsequence(
|
|
wire,
|
|
EncodeDescriptorProfile(registration.KdfProfile)),
|
|
"The PPAP proof contains the plaintext registration KDF descriptor.");
|
|
}
|
|
|
|
static byte[] EncodeDescriptorVersionPrefix(PpapRegistrationRecord registration)
|
|
{
|
|
var encoded = new byte[1 + sizeof(long) + sizeof(int)];
|
|
encoded[0] = (byte)registration.Kind;
|
|
BinaryPrimitives.WriteInt64BigEndian(
|
|
encoded.AsSpan(1, sizeof(long)),
|
|
registration.Version);
|
|
BinaryPrimitives.WriteInt32BigEndian(
|
|
encoded.AsSpan(1 + sizeof(long), sizeof(int)),
|
|
registration.Nonce.Length);
|
|
return encoded;
|
|
}
|
|
|
|
static byte[] EncodeDescriptorProfile(PpapKdfProfile profile)
|
|
{
|
|
Assert.NotNull(profile);
|
|
var encoded = new byte[1 + (4 * sizeof(int))];
|
|
encoded[0] = 1;
|
|
BinaryPrimitives.WriteInt32BigEndian(
|
|
encoded.AsSpan(1, sizeof(int)),
|
|
profile.Version);
|
|
BinaryPrimitives.WriteInt32BigEndian(
|
|
encoded.AsSpan(1 + sizeof(int), sizeof(int)),
|
|
profile.MemoryKiB);
|
|
BinaryPrimitives.WriteInt32BigEndian(
|
|
encoded.AsSpan(1 + (2 * sizeof(int)), sizeof(int)),
|
|
profile.Iterations);
|
|
BinaryPrimitives.WriteInt32BigEndian(
|
|
encoded.AsSpan(1 + (3 * sizeof(int)), sizeof(int)),
|
|
profile.Parallelism);
|
|
return encoded;
|
|
}
|
|
|
|
static bool ContainsSubsequence(byte[] value, byte[] candidate)
|
|
{
|
|
if (candidate.Length == 0 || candidate.Length > value.Length)
|
|
return false;
|
|
|
|
for (var offset = 0; offset <= value.Length - candidate.Length; offset++)
|
|
{
|
|
if (value.AsSpan(offset, candidate.Length).SequenceEqual(candidate))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static RegistrationSnapshot Snapshot(PpapRegistrationRecord record)
|
|
=> new(record.Version, record.Nonce, record.EncapsulationKey);
|
|
|
|
static void AssertSnapshot(RegistrationSnapshot expected,
|
|
PpapRegistrationRecord actual)
|
|
{
|
|
Assert.Equal(expected.Version, actual.Version);
|
|
Assert.Equal(expected.Nonce, actual.Nonce);
|
|
Assert.Equal(expected.EncapsulationKey, actual.EncapsulationKey);
|
|
}
|
|
|
|
static void AssertRotated(RegistrationSnapshot before,
|
|
PpapRegistrationRecord after)
|
|
{
|
|
Assert.Equal(before.Version + 1, after.Version);
|
|
Assert.False(before.Nonce.SequenceEqual(after.Nonce));
|
|
Assert.False(before.EncapsulationKey.SequenceEqual(after.EncapsulationKey));
|
|
}
|
|
|
|
static PpapPair CreatePair(
|
|
AuthenticationMode mode,
|
|
string initiatorPassword = InitiatorPassword,
|
|
string responderPassword = ResponderPassword)
|
|
{
|
|
var authenticatesInitiator = mode is AuthenticationMode.InitializerIdentity
|
|
or AuthenticationMode.DualIdentity;
|
|
var authenticatesResponder = mode is AuthenticationMode.ResponderIdentity
|
|
or AuthenticationMode.DualIdentity;
|
|
|
|
var initiatorStore = new InMemoryPpapRegistrationStore();
|
|
var responderStore = new InMemoryPpapRegistrationStore();
|
|
PpapLocalIdentity? initiatorLocal = null;
|
|
PpapLocalIdentity? responderLocal = null;
|
|
|
|
try
|
|
{
|
|
if (authenticatesInitiator)
|
|
{
|
|
initiatorLocal = PpapLocalIdentity.FromPassword(
|
|
InitiatorIdentity, initiatorPassword, TestKdf);
|
|
using var registered = PpapLocalIdentity.FromPassword(
|
|
InitiatorIdentity, InitiatorPassword, TestKdf);
|
|
Assert.True(responderStore.TryAdd(
|
|
Domain,
|
|
PpapRegistrationRecord.FromLocalIdentity(Domain, registered)));
|
|
}
|
|
|
|
if (authenticatesResponder)
|
|
{
|
|
responderLocal = PpapLocalIdentity.FromPassword(
|
|
ResponderIdentity, responderPassword, TestKdf);
|
|
using var registered = PpapLocalIdentity.FromPassword(
|
|
ResponderIdentity, ResponderPassword, TestKdf);
|
|
Assert.True(initiatorStore.TryAdd(
|
|
Domain,
|
|
PpapRegistrationRecord.FromLocalIdentity(Domain, registered)));
|
|
}
|
|
|
|
var initiatorProvider = new PpapAuthenticationProvider(
|
|
initiatorLocal!, initiatorStore);
|
|
var responderProvider = new PpapAuthenticationProvider(
|
|
responderLocal!, responderStore);
|
|
var initiator = Assert.IsType<PpapAuthenticationHandler>(
|
|
initiatorProvider.CreateAuthenticationHandler(new AuthenticationContext
|
|
{
|
|
Direction = AuthenticationDirection.Initiator,
|
|
Mode = mode,
|
|
Domain = Domain,
|
|
InitiatorIdentity = authenticatesInitiator ? InitiatorIdentity : null,
|
|
ResponderIdentity = authenticatesResponder ? ResponderIdentity : null,
|
|
HostName = ResponderIdentity,
|
|
}));
|
|
var responder = Assert.IsType<PpapAuthenticationHandler>(
|
|
responderProvider.CreateAuthenticationHandler(new AuthenticationContext
|
|
{
|
|
Direction = AuthenticationDirection.Responder,
|
|
Mode = mode,
|
|
Domain = Domain,
|
|
InitiatorIdentity = authenticatesInitiator ? InitiatorIdentity : null,
|
|
ResponderIdentity = authenticatesResponder ? ResponderIdentity : null,
|
|
HostName = ResponderIdentity,
|
|
}));
|
|
|
|
return new PpapPair(
|
|
initiatorLocal,
|
|
responderLocal,
|
|
initiatorStore,
|
|
responderStore,
|
|
initiator,
|
|
responder);
|
|
}
|
|
catch
|
|
{
|
|
initiatorLocal?.Dispose();
|
|
responderLocal?.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
static PpapPair CreateStaticPair()
|
|
{
|
|
PpapCryptography.GenerateKeyPair(
|
|
out var initiatorPrivateKey,
|
|
out var initiatorPublicKey);
|
|
PpapCryptography.GenerateKeyPair(
|
|
out var responderPrivateKey,
|
|
out var responderPublicKey);
|
|
PpapLocalIdentity? initiatorLocal = null;
|
|
PpapLocalIdentity? responderLocal = null;
|
|
|
|
try
|
|
{
|
|
initiatorLocal = PpapLocalIdentity.FromStaticKey(
|
|
InitiatorIdentity,
|
|
initiatorPrivateKey);
|
|
responderLocal = PpapLocalIdentity.FromStaticKey(
|
|
ResponderIdentity,
|
|
responderPrivateKey);
|
|
var initiatorStore = new InMemoryPpapRegistrationStore();
|
|
var responderStore = new InMemoryPpapRegistrationStore();
|
|
Assert.True(responderStore.TryAdd(
|
|
Domain,
|
|
PpapRegistrationRecord.FromLocalIdentity(Domain, initiatorLocal)));
|
|
Assert.True(initiatorStore.TryAdd(
|
|
Domain,
|
|
PpapRegistrationRecord.FromLocalIdentity(Domain, responderLocal)));
|
|
|
|
var initiatorProvider = new PpapAuthenticationProvider(
|
|
initiatorLocal!,
|
|
initiatorStore);
|
|
var responderProvider = new PpapAuthenticationProvider(
|
|
responderLocal!,
|
|
responderStore);
|
|
var initiator = Assert.IsType<PpapAuthenticationHandler>(
|
|
initiatorProvider.CreateAuthenticationHandler(new AuthenticationContext
|
|
{
|
|
Direction = AuthenticationDirection.Initiator,
|
|
Mode = AuthenticationMode.DualIdentity,
|
|
Domain = Domain,
|
|
InitiatorIdentity = InitiatorIdentity,
|
|
ResponderIdentity = ResponderIdentity,
|
|
HostName = ResponderIdentity,
|
|
}));
|
|
var responder = Assert.IsType<PpapAuthenticationHandler>(
|
|
responderProvider.CreateAuthenticationHandler(new AuthenticationContext
|
|
{
|
|
Direction = AuthenticationDirection.Responder,
|
|
Mode = AuthenticationMode.DualIdentity,
|
|
Domain = Domain,
|
|
InitiatorIdentity = InitiatorIdentity,
|
|
ResponderIdentity = ResponderIdentity,
|
|
HostName = ResponderIdentity,
|
|
}));
|
|
|
|
return new PpapPair(
|
|
initiatorLocal,
|
|
responderLocal,
|
|
initiatorStore,
|
|
responderStore,
|
|
initiator,
|
|
responder);
|
|
}
|
|
catch
|
|
{
|
|
initiatorLocal?.Dispose();
|
|
responderLocal?.Dispose();
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
PpapCryptography.Clear(initiatorPrivateKey);
|
|
PpapCryptography.Clear(initiatorPublicKey);
|
|
PpapCryptography.Clear(responderPrivateKey);
|
|
PpapCryptography.Clear(responderPublicKey);
|
|
}
|
|
}
|
|
|
|
readonly record struct RegistrationSnapshot(
|
|
long Version,
|
|
byte[] Nonce,
|
|
byte[] EncapsulationKey);
|
|
|
|
readonly record struct HandshakeResult(
|
|
AuthenticationResult Initiator,
|
|
AuthenticationResult Responder);
|
|
|
|
sealed class PpapPair : IDisposable
|
|
{
|
|
readonly PpapLocalIdentity? _initiatorLocal;
|
|
readonly PpapLocalIdentity? _responderLocal;
|
|
|
|
public InMemoryPpapRegistrationStore InitiatorStore { get; }
|
|
public InMemoryPpapRegistrationStore ResponderStore { get; }
|
|
public PpapAuthenticationHandler Initiator { get; }
|
|
public PpapAuthenticationHandler Responder { get; }
|
|
|
|
public PpapPair(
|
|
PpapLocalIdentity? initiatorLocal,
|
|
PpapLocalIdentity? responderLocal,
|
|
InMemoryPpapRegistrationStore initiatorStore,
|
|
InMemoryPpapRegistrationStore responderStore,
|
|
PpapAuthenticationHandler initiator,
|
|
PpapAuthenticationHandler responder)
|
|
{
|
|
_initiatorLocal = initiatorLocal;
|
|
_responderLocal = responderLocal;
|
|
InitiatorStore = initiatorStore;
|
|
ResponderStore = responderStore;
|
|
Initiator = initiator;
|
|
Responder = responder;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Initiator.Dispose();
|
|
Responder.Dispose();
|
|
_initiatorLocal?.Dispose();
|
|
_responderLocal?.Dispose();
|
|
}
|
|
}
|
|
}
|