mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-31 01:40:42 +00:00
PPAP integration
This commit is contained in:
@@ -0,0 +1,664 @@
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Kems;
|
||||
using Org.BouncyCastle.Crypto.Macs;
|
||||
using Org.BouncyCastle.Crypto.Modes;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Security.Authority.Providers.Ppap;
|
||||
|
||||
public static class PpapProtocol
|
||||
{
|
||||
public const string Name = "ppap-mlkem768-v1";
|
||||
public const int WireVersion = 1;
|
||||
public const int PublicKeyLength = 1184;
|
||||
public const int PrivateKeyLength = 2400;
|
||||
public const int CiphertextLength = 1088;
|
||||
public const int KemSecretLength = 32;
|
||||
public const int SeedLength = 64;
|
||||
public const int HashLength = 32;
|
||||
public const int FinishedTagLength = 32;
|
||||
public const int IdentityMaskLength = 32;
|
||||
public const int RegistrationNonceLength = 32;
|
||||
public const int SessionKeyLength = 32;
|
||||
public const int DescriptorPlaintextLength = 128;
|
||||
public const int DescriptorKeyLength = 32;
|
||||
public const int DescriptorNonceLength = 12;
|
||||
public const int DescriptorTagLength = 16;
|
||||
public const int ProtectedDescriptorLength =
|
||||
DescriptorPlaintextLength + DescriptorTagLength;
|
||||
public const int MaximumIdentityBytes = 512;
|
||||
public const int MaximumDomainBytes = 512;
|
||||
public const int MaximumPasswordBytes = 4096;
|
||||
public const int MaximumWireMessageBytes = 8192;
|
||||
}
|
||||
|
||||
internal static class PpapCryptography
|
||||
{
|
||||
static readonly MLKemParameters KemParameters = MLKemParameters.ml_kem_768;
|
||||
static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true);
|
||||
static readonly byte[] PasswordSeedLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/password-seed");
|
||||
static readonly byte[] IdentityMaskLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/masked-identity");
|
||||
static readonly byte[] DescriptorSaltLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/descriptor-salt");
|
||||
static readonly byte[] DescriptorKeyLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/descriptor-key");
|
||||
static readonly byte[] DescriptorNonceLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/descriptor-nonce");
|
||||
static readonly byte[] DescriptorAadLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/descriptor-aad");
|
||||
static readonly byte[] InitiatorDescriptorLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/initiator-registration");
|
||||
static readonly byte[] ResponderDescriptorLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/responder-registration");
|
||||
static readonly byte[] TranscriptLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/transcript");
|
||||
static readonly byte[] KeyScheduleLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/key-schedule");
|
||||
static readonly byte[] SessionKeyLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/session-key");
|
||||
static readonly byte[] InitiatorFinishedKeyLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/initiator-finished-key");
|
||||
static readonly byte[] ResponderFinishedKeyLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/responder-finished-key");
|
||||
static readonly byte[] InitiatorFinishedLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/initiator-finished");
|
||||
static readonly byte[] ResponderFinishedLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/responder-finished");
|
||||
static readonly byte[] RotationProofLabel = Encoding.ASCII.GetBytes(
|
||||
"esiur/ppap-mlkem768-v1/rotation-proof");
|
||||
|
||||
internal static string NormalizeIdentity(string identity)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(identity))
|
||||
throw new ArgumentException("An identity is required.", nameof(identity));
|
||||
var normalized = identity.Normalize(NormalizationForm.FormC);
|
||||
var encoded = EncodeUtf8(normalized, PpapProtocol.MaximumIdentityBytes, nameof(identity));
|
||||
Clear(encoded);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
internal static string NormalizeDomain(string domain)
|
||||
{
|
||||
var normalized = (domain ?? string.Empty).Normalize(NormalizationForm.FormC);
|
||||
var encoded = EncodeUtf8(normalized, PpapProtocol.MaximumDomainBytes, nameof(domain));
|
||||
Clear(encoded);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
internal static byte[] EncodeUtf8(string value, int maximumBytes, string parameterName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encoded = StrictUtf8.GetBytes(value ?? string.Empty);
|
||||
if (encoded.Length > maximumBytes)
|
||||
{
|
||||
Clear(encoded);
|
||||
throw new ArgumentException("The UTF-8 value is too long.", parameterName);
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
catch (EncoderFallbackException ex)
|
||||
{
|
||||
throw new ArgumentException("The value contains invalid Unicode.", parameterName, ex);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string DecodeUtf8(byte[] value, int maximumBytes, string parameterName)
|
||||
{
|
||||
if (value == null || value.Length > maximumBytes)
|
||||
throw new ArgumentException("The UTF-8 value has an invalid length.", parameterName);
|
||||
try
|
||||
{
|
||||
return StrictUtf8.GetString(value).Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
catch (DecoderFallbackException ex)
|
||||
{
|
||||
throw new ArgumentException("The value is not valid UTF-8.", parameterName, ex);
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[] RandomBytes(int length)
|
||||
{
|
||||
if (length <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(length));
|
||||
var value = new byte[length];
|
||||
using (var random = RandomNumberGenerator.Create())
|
||||
random.GetBytes(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
internal static byte[] Hash(params byte[][] values)
|
||||
{
|
||||
var digest = new Sha3Digest(256);
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (value != null && value.Length != 0)
|
||||
digest.BlockUpdate(value, 0, value.Length);
|
||||
}
|
||||
var output = new byte[PpapProtocol.HashLength];
|
||||
digest.DoFinal(output, 0);
|
||||
return output;
|
||||
}
|
||||
|
||||
internal static byte[] HashFramed(params byte[][] values)
|
||||
{
|
||||
var encoded = EncodeFrames(values);
|
||||
try
|
||||
{
|
||||
return Hash(encoded);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(encoded);
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[] MaskIdentity(string domain, string identity, byte[] mask,
|
||||
byte[] maskKey)
|
||||
{
|
||||
if (mask == null || mask.Length != PpapProtocol.IdentityMaskLength)
|
||||
throw new ArgumentException("The identity mask has an invalid length.", nameof(mask));
|
||||
ValidateKemSecret(maskKey, nameof(maskKey));
|
||||
|
||||
var domainBytes = EncodeUtf8(NormalizeDomain(domain),
|
||||
PpapProtocol.MaximumDomainBytes, nameof(domain));
|
||||
var identityBytes = EncodeUtf8(NormalizeIdentity(identity),
|
||||
PpapProtocol.MaximumIdentityBytes, nameof(identity));
|
||||
try
|
||||
{
|
||||
var input = EncodeFrames(IdentityMaskLabel, domainBytes, identityBytes, mask);
|
||||
try
|
||||
{
|
||||
return ComputeMac(maskKey, input);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(input);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(domainBytes);
|
||||
Clear(identityBytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Protected descriptor v1 is:
|
||||
// uint32be descriptorLength || canonicalDescriptor || zeroPadding
|
||||
// padded to DescriptorPlaintextLength, followed by a 128-bit GCM tag.
|
||||
// HKDF-SHA3 derives a role/domain/message-specific AES-256 key and 96-bit
|
||||
// nonce from the fresh ephemeral ML-KEM secret; the nonce is not sent.
|
||||
// Protocol implementations MUST encrypt at most one descriptor for each
|
||||
// role under a given ephemeral secret. ResponderProof therefore reuses its
|
||||
// single protected field when constructing the transcript core and packet.
|
||||
internal static byte[] ProtectRegistrationDescriptor(string domain,
|
||||
PpapIdentityRole role, byte[] ephemeralSecret, byte[] descriptor)
|
||||
{
|
||||
ValidateKemSecret(ephemeralSecret, nameof(ephemeralSecret));
|
||||
if (descriptor == null || descriptor.Length == 0
|
||||
|| descriptor.Length > PpapProtocol.DescriptorPlaintextLength - 4)
|
||||
throw new ArgumentException("The registration descriptor has an invalid length.",
|
||||
nameof(descriptor));
|
||||
|
||||
var framed = EncodeFrames(descriptor);
|
||||
var plaintext = new byte[PpapProtocol.DescriptorPlaintextLength];
|
||||
var output = new byte[PpapProtocol.ProtectedDescriptorLength];
|
||||
byte[] key = null;
|
||||
byte[] nonce = null;
|
||||
byte[] aad = null;
|
||||
try
|
||||
{
|
||||
Buffer.BlockCopy(framed, 0, plaintext, 0, framed.Length);
|
||||
DeriveDescriptorProtection(domain, role, ephemeralSecret,
|
||||
out key, out nonce, out aad);
|
||||
|
||||
var cipher = new GcmBlockCipher(AesUtilities.CreateEngine());
|
||||
cipher.Init(true, new AeadParameters(new KeyParameter(key),
|
||||
PpapProtocol.DescriptorTagLength * 8, nonce, aad));
|
||||
var written = cipher.ProcessBytes(plaintext, 0, plaintext.Length,
|
||||
output, 0);
|
||||
written += cipher.DoFinal(output, written);
|
||||
if (written != output.Length)
|
||||
throw new CryptographicException(
|
||||
"AES-GCM produced an invalid protected descriptor length.");
|
||||
return output;
|
||||
}
|
||||
catch (CryptoException ex)
|
||||
{
|
||||
Clear(output);
|
||||
throw new CryptographicException(
|
||||
"Registration descriptor protection failed.", ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Clear(output);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(framed);
|
||||
Clear(plaintext);
|
||||
Clear(key);
|
||||
Clear(nonce);
|
||||
Clear(aad);
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[] UnprotectRegistrationDescriptor(string domain,
|
||||
PpapIdentityRole role, byte[] ephemeralSecret, byte[] protectedDescriptor)
|
||||
{
|
||||
ValidateKemSecret(ephemeralSecret, nameof(ephemeralSecret));
|
||||
if (protectedDescriptor == null
|
||||
|| protectedDescriptor.Length != PpapProtocol.ProtectedDescriptorLength)
|
||||
throw new InvalidDataException(
|
||||
"The protected registration descriptor has an invalid length.");
|
||||
|
||||
var plaintext = new byte[PpapProtocol.DescriptorPlaintextLength];
|
||||
byte[] key = null;
|
||||
byte[] nonce = null;
|
||||
byte[] aad = null;
|
||||
try
|
||||
{
|
||||
DeriveDescriptorProtection(domain, role, ephemeralSecret,
|
||||
out key, out nonce, out aad);
|
||||
var cipher = new GcmBlockCipher(AesUtilities.CreateEngine());
|
||||
cipher.Init(false, new AeadParameters(new KeyParameter(key),
|
||||
PpapProtocol.DescriptorTagLength * 8, nonce, aad));
|
||||
var written = cipher.ProcessBytes(protectedDescriptor, 0,
|
||||
protectedDescriptor.Length, plaintext, 0);
|
||||
written += cipher.DoFinal(plaintext, written);
|
||||
if (written != plaintext.Length)
|
||||
throw new InvalidDataException(
|
||||
"AES-GCM produced an invalid registration descriptor length.");
|
||||
|
||||
var offset = 0;
|
||||
var descriptorLength = ReadInt32(plaintext, ref offset);
|
||||
if (descriptorLength == 0
|
||||
|| descriptorLength > PpapProtocol.DescriptorPlaintextLength - offset)
|
||||
throw new InvalidDataException(
|
||||
"The protected registration descriptor is malformed.");
|
||||
var descriptor = new byte[descriptorLength];
|
||||
Buffer.BlockCopy(plaintext, offset, descriptor, 0, descriptorLength);
|
||||
offset += descriptorLength;
|
||||
for (var i = offset; i < plaintext.Length; i++)
|
||||
{
|
||||
if (plaintext[i] != 0)
|
||||
{
|
||||
Clear(descriptor);
|
||||
throw new InvalidDataException(
|
||||
"The protected registration descriptor is not canonical.");
|
||||
}
|
||||
}
|
||||
return descriptor;
|
||||
}
|
||||
catch (CryptoException ex)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"Protected registration descriptor authentication failed.", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(plaintext);
|
||||
Clear(key);
|
||||
Clear(nonce);
|
||||
Clear(aad);
|
||||
}
|
||||
}
|
||||
|
||||
static void DeriveDescriptorProtection(string domain, PpapIdentityRole role,
|
||||
byte[] ephemeralSecret, out byte[] key, out byte[] nonce, out byte[] aad)
|
||||
{
|
||||
var roleLabel = GetDescriptorRoleLabel(role, out var messageType);
|
||||
var domainBytes = EncodeUtf8(NormalizeDomain(domain),
|
||||
PpapProtocol.MaximumDomainBytes, nameof(domain));
|
||||
var binding = new[]
|
||||
{
|
||||
(byte)PpapProtocol.WireVersion,
|
||||
(byte)role,
|
||||
(byte)messageType,
|
||||
};
|
||||
var salt = HashFramed(DescriptorSaltLabel, roleLabel, domainBytes, binding);
|
||||
key = null;
|
||||
nonce = null;
|
||||
aad = null;
|
||||
try
|
||||
{
|
||||
key = Expand(ephemeralSecret, salt,
|
||||
EncodeFrames(DescriptorKeyLabel, roleLabel, domainBytes, binding),
|
||||
PpapProtocol.DescriptorKeyLength);
|
||||
nonce = Expand(ephemeralSecret, salt,
|
||||
EncodeFrames(DescriptorNonceLabel, roleLabel, domainBytes, binding),
|
||||
PpapProtocol.DescriptorNonceLength);
|
||||
aad = EncodeFrames(DescriptorAadLabel, roleLabel, domainBytes, binding);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Clear(key);
|
||||
Clear(nonce);
|
||||
Clear(aad);
|
||||
key = null;
|
||||
nonce = null;
|
||||
aad = null;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(domainBytes);
|
||||
Clear(binding);
|
||||
Clear(salt);
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] GetDescriptorRoleLabel(PpapIdentityRole role,
|
||||
out PpapMessageType messageType)
|
||||
{
|
||||
if (role == PpapIdentityRole.Initiator)
|
||||
{
|
||||
messageType = PpapMessageType.ResponderProof;
|
||||
return InitiatorDescriptorLabel;
|
||||
}
|
||||
if (role == PpapIdentityRole.Responder)
|
||||
{
|
||||
messageType = PpapMessageType.InitiatorProof;
|
||||
return ResponderDescriptorLabel;
|
||||
}
|
||||
throw new ArgumentOutOfRangeException(nameof(role));
|
||||
}
|
||||
|
||||
internal static byte[] DerivePasswordPrivateKey(string domain, string identity,
|
||||
byte[] password, byte[] nonce, PpapKdfProfile profile)
|
||||
{
|
||||
if (password == null || password.Length == 0
|
||||
|| password.Length > PpapProtocol.MaximumPasswordBytes)
|
||||
throw new ArgumentException("The password has an invalid length.", nameof(password));
|
||||
if (nonce == null || nonce.Length != PpapProtocol.RegistrationNonceLength)
|
||||
throw new ArgumentException("The registration nonce has an invalid length.", nameof(nonce));
|
||||
if (profile == null)
|
||||
throw new ArgumentNullException(nameof(profile));
|
||||
|
||||
var identityBytes = EncodeUtf8(NormalizeIdentity(identity),
|
||||
PpapProtocol.MaximumIdentityBytes, nameof(identity));
|
||||
var domainBytes = EncodeUtf8(NormalizeDomain(domain),
|
||||
PpapProtocol.MaximumDomainBytes, nameof(domain));
|
||||
var input = EncodeFrames(PasswordSeedLabel, domainBytes, identityBytes,
|
||||
password, nonce);
|
||||
var seed = new byte[PpapProtocol.SeedLength];
|
||||
try
|
||||
{
|
||||
var parameters = new Argon2Parameters.Builder(Argon2Parameters.Argon2id)
|
||||
.WithVersion(profile.Version)
|
||||
.WithSalt((byte[])nonce.Clone())
|
||||
.WithMemoryAsKB(profile.MemoryKiB)
|
||||
.WithIterations(profile.Iterations)
|
||||
.WithParallelism(profile.Parallelism)
|
||||
.Build();
|
||||
var argon2 = new Argon2BytesGenerator();
|
||||
argon2.Init(parameters);
|
||||
argon2.GenerateBytes(input, seed);
|
||||
|
||||
var privateKey = MLKemPrivateKeyParameters.FromSeed(KemParameters, seed);
|
||||
return privateKey.GetEncoded();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(identityBytes);
|
||||
Clear(domainBytes);
|
||||
Clear(input);
|
||||
Clear(seed);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void GenerateKeyPair(out byte[] privateKey, out byte[] publicKey)
|
||||
{
|
||||
var seed = RandomBytes(PpapProtocol.SeedLength);
|
||||
try
|
||||
{
|
||||
var parameters = MLKemPrivateKeyParameters.FromSeed(KemParameters, seed);
|
||||
privateKey = parameters.GetEncoded();
|
||||
publicKey = parameters.GetPublicKeyEncoded();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(seed);
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[] GetPublicKey(byte[] privateKey)
|
||||
{
|
||||
ValidatePrivateKey(privateKey);
|
||||
return MLKemPrivateKeyParameters.FromEncoding(KemParameters, privateKey)
|
||||
.GetPublicKeyEncoded();
|
||||
}
|
||||
|
||||
internal static void ValidatePrivateKey(byte[] privateKey)
|
||||
{
|
||||
if (privateKey == null || privateKey.Length != PpapProtocol.PrivateKeyLength)
|
||||
throw new ArgumentException("The ML-KEM-768 private key has an invalid length.", nameof(privateKey));
|
||||
MLKemPrivateKeyParameters.FromEncoding(KemParameters, privateKey);
|
||||
}
|
||||
|
||||
internal static void ValidatePublicKey(byte[] publicKey)
|
||||
{
|
||||
if (publicKey == null || publicKey.Length != PpapProtocol.PublicKeyLength)
|
||||
throw new ArgumentException("The ML-KEM-768 public key has an invalid length.", nameof(publicKey));
|
||||
MLKemPublicKeyParameters.FromEncoding(KemParameters, publicKey);
|
||||
}
|
||||
|
||||
internal static void Encapsulate(byte[] publicKey, out byte[] ciphertext,
|
||||
out byte[] secret)
|
||||
{
|
||||
ValidatePublicKey(publicKey);
|
||||
var encapsulator = new MLKemEncapsulator(KemParameters);
|
||||
encapsulator.Init(MLKemPublicKeyParameters.FromEncoding(KemParameters, publicKey));
|
||||
ciphertext = new byte[encapsulator.EncapsulationLength];
|
||||
secret = new byte[encapsulator.SecretLength];
|
||||
try
|
||||
{
|
||||
encapsulator.Encapsulate(ciphertext, 0, ciphertext.Length,
|
||||
secret, 0, secret.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Clear(ciphertext);
|
||||
Clear(secret);
|
||||
ciphertext = null;
|
||||
secret = null;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[] Decapsulate(byte[] privateKey, byte[] ciphertext)
|
||||
{
|
||||
ValidatePrivateKey(privateKey);
|
||||
if (ciphertext == null || ciphertext.Length != PpapProtocol.CiphertextLength)
|
||||
throw new ArgumentException("The ML-KEM-768 ciphertext has an invalid length.", nameof(ciphertext));
|
||||
var decapsulator = new MLKemDecapsulator(KemParameters);
|
||||
decapsulator.Init(MLKemPrivateKeyParameters.FromEncoding(KemParameters, privateKey));
|
||||
var secret = new byte[decapsulator.SecretLength];
|
||||
decapsulator.Decapsulate(ciphertext, 0, ciphertext.Length,
|
||||
secret, 0, secret.Length);
|
||||
return secret;
|
||||
}
|
||||
|
||||
internal static byte[] ComputeTranscriptHash(IList<byte[]> messages,
|
||||
byte[] authenticationContext)
|
||||
{
|
||||
if (messages == null)
|
||||
throw new ArgumentNullException(nameof(messages));
|
||||
var frames = new byte[messages.Count + 2][];
|
||||
frames[0] = TranscriptLabel;
|
||||
frames[1] = authenticationContext ?? Array.Empty<byte>();
|
||||
for (var i = 0; i < messages.Count; i++)
|
||||
frames[i + 2] = messages[i];
|
||||
return HashFramed(frames);
|
||||
}
|
||||
|
||||
internal static void DeriveHandshakeKeys(byte[] ephemeralSecret,
|
||||
byte[] initiatorIdentitySecret, byte[] responderIdentitySecret,
|
||||
byte[] transcriptHash, byte[] authenticationContext,
|
||||
out byte[] sessionKey, out byte[] initiatorFinishedKey,
|
||||
out byte[] responderFinishedKey)
|
||||
{
|
||||
ValidateKemSecret(ephemeralSecret, nameof(ephemeralSecret));
|
||||
ValidateOptionalKemSecret(initiatorIdentitySecret, nameof(initiatorIdentitySecret));
|
||||
ValidateOptionalKemSecret(responderIdentitySecret, nameof(responderIdentitySecret));
|
||||
if (transcriptHash == null || transcriptHash.Length != PpapProtocol.HashLength)
|
||||
throw new ArgumentException("The transcript hash has an invalid length.", nameof(transcriptHash));
|
||||
|
||||
var input = EncodeFrames(KeyScheduleLabel, ephemeralSecret,
|
||||
initiatorIdentitySecret ?? Array.Empty<byte>(),
|
||||
responderIdentitySecret ?? Array.Empty<byte>());
|
||||
try
|
||||
{
|
||||
sessionKey = Expand(input, transcriptHash,
|
||||
EncodeFrames(SessionKeyLabel, authenticationContext),
|
||||
PpapProtocol.SessionKeyLength);
|
||||
initiatorFinishedKey = Expand(input, transcriptHash,
|
||||
EncodeFrames(InitiatorFinishedKeyLabel, authenticationContext),
|
||||
PpapProtocol.HashLength);
|
||||
responderFinishedKey = Expand(input, transcriptHash,
|
||||
EncodeFrames(ResponderFinishedKeyLabel, authenticationContext),
|
||||
PpapProtocol.HashLength);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(input);
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] Expand(byte[] input, byte[] salt, byte[] info, int length)
|
||||
{
|
||||
try
|
||||
{
|
||||
var hkdf = new HkdfBytesGenerator(new Sha3Digest(256));
|
||||
hkdf.Init(new HkdfParameters(input, salt, info));
|
||||
var output = new byte[length];
|
||||
hkdf.GenerateBytes(output, 0, output.Length);
|
||||
return output;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(info);
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[] ComputeFinished(bool initiator, byte[] finishedKey,
|
||||
byte[] transcriptHash)
|
||||
{
|
||||
if (finishedKey == null || finishedKey.Length != PpapProtocol.HashLength)
|
||||
throw new ArgumentException("The Finished key has an invalid length.", nameof(finishedKey));
|
||||
if (transcriptHash == null || transcriptHash.Length != PpapProtocol.HashLength)
|
||||
throw new ArgumentException("The transcript hash has an invalid length.", nameof(transcriptHash));
|
||||
var input = EncodeFrames(initiator ? InitiatorFinishedLabel : ResponderFinishedLabel,
|
||||
transcriptHash);
|
||||
try
|
||||
{
|
||||
return ComputeMac(finishedKey, input);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(input);
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte[] ComputeMac(byte[] key, byte[] input)
|
||||
{
|
||||
if (key == null || key.Length == 0)
|
||||
throw new ArgumentException("A MAC key is required.", nameof(key));
|
||||
var hmac = new HMac(new Sha3Digest(256));
|
||||
hmac.Init(new KeyParameter(key));
|
||||
hmac.BlockUpdate(input, 0, input.Length);
|
||||
var output = new byte[hmac.GetMacSize()];
|
||||
hmac.DoFinal(output, 0);
|
||||
return output;
|
||||
}
|
||||
|
||||
internal static byte[] ComputeRotationProof(byte[] challengeSecret,
|
||||
byte[] sessionKey, byte[] offer, byte[] challenge)
|
||||
{
|
||||
ValidateKemSecret(challengeSecret, nameof(challengeSecret));
|
||||
if (sessionKey == null || sessionKey.Length != PpapProtocol.SessionKeyLength)
|
||||
throw new ArgumentException("The session key has an invalid length.", nameof(sessionKey));
|
||||
var context = HashFramed(RotationProofLabel, sessionKey, offer, challenge);
|
||||
try
|
||||
{
|
||||
return ComputeMac(challengeSecret, context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(context);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool FixedTimeEquals(byte[] left, byte[] right)
|
||||
{
|
||||
return left != null && right != null && left.Length == right.Length
|
||||
&& Arrays.FixedTimeEquals(left, right);
|
||||
}
|
||||
|
||||
internal static byte[] EncodeFrames(params byte[][] values)
|
||||
{
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var field = value ?? Array.Empty<byte>();
|
||||
WriteInt32(output, field.Length);
|
||||
output.Write(field, 0, field.Length);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WriteInt32(Stream output, int value)
|
||||
{
|
||||
if (value < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
output.WriteByte((byte)(value >> 24));
|
||||
output.WriteByte((byte)(value >> 16));
|
||||
output.WriteByte((byte)(value >> 8));
|
||||
output.WriteByte((byte)value);
|
||||
}
|
||||
|
||||
internal static int ReadInt32(byte[] input, ref int offset)
|
||||
{
|
||||
if (input == null || offset < 0 || input.Length - offset < 4)
|
||||
throw new InvalidDataException("Truncated PPAP integer.");
|
||||
var value = (input[offset] << 24)
|
||||
| (input[offset + 1] << 16)
|
||||
| (input[offset + 2] << 8)
|
||||
| input[offset + 3];
|
||||
offset += 4;
|
||||
if (value < 0)
|
||||
throw new InvalidDataException("Negative PPAP length.");
|
||||
return value;
|
||||
}
|
||||
|
||||
static void ValidateKemSecret(byte[] value, string parameterName)
|
||||
{
|
||||
if (value == null || value.Length != PpapProtocol.KemSecretLength)
|
||||
throw new ArgumentException("The KEM secret has an invalid length.", parameterName);
|
||||
}
|
||||
|
||||
static void ValidateOptionalKemSecret(byte[] value, string parameterName)
|
||||
{
|
||||
if (value != null && value.Length != PpapProtocol.KemSecretLength)
|
||||
throw new ArgumentException("The KEM secret has an invalid length.", parameterName);
|
||||
}
|
||||
|
||||
internal static void Clear(byte[] value)
|
||||
{
|
||||
if (value != null)
|
||||
Array.Clear(value, 0, value.Length);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user