mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
665 lines
25 KiB
C#
665 lines
25 KiB
C#
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);
|
|
}
|
|
}
|