From 25ae5348623524d542f03060ff5a49a82160abd7 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 8 Feb 2024 18:31:11 +0100 Subject: [PATCH 01/37] Initial implementation of Argon2 (ported from bc-java) --- crypto/src/crypto/ICharToByteConverter.cs | 35 + crypto/src/crypto/PasswordConverter.cs | 44 + .../crypto/generators/Argon2BytesGenerator.cs | 762 ++++++++++++++++++ .../src/crypto/parameters/Argon2Parameters.cs | 208 +++++ crypto/test/src/crypto/test/Argon2Test.cs | 377 +++++++++ 5 files changed, 1426 insertions(+) create mode 100644 crypto/src/crypto/ICharToByteConverter.cs create mode 100644 crypto/src/crypto/PasswordConverter.cs create mode 100644 crypto/src/crypto/generators/Argon2BytesGenerator.cs create mode 100644 crypto/src/crypto/parameters/Argon2Parameters.cs create mode 100644 crypto/test/src/crypto/test/Argon2Test.cs diff --git a/crypto/src/crypto/ICharToByteConverter.cs b/crypto/src/crypto/ICharToByteConverter.cs new file mode 100644 index 000000000..94e0279e9 --- /dev/null +++ b/crypto/src/crypto/ICharToByteConverter.cs @@ -0,0 +1,35 @@ +namespace Org.BouncyCastle.Crypto +{ + public interface ICharToByteConverter + { + /** + * Return the type of the conversion. + * + * @return a type name for the conversion. + */ + string GetName(); + + /** + * Return a byte encoded representation of the passed in password. + * + * @param password the characters to encode. + * @return a byte encoding of password. + */ + byte[] Convert(char[] password); + } + + public static class CharToByteConverterExtensions + { + + /** + * Return a byte encoded representation of the passed in password. + * + * @param password the string to encode. + * @return a byte encoding of password. + */ + public static byte[] Convert(this ICharToByteConverter converter, string password) + { + return converter.Convert(password.ToCharArray()); + } + } +} \ No newline at end of file diff --git a/crypto/src/crypto/PasswordConverter.cs b/crypto/src/crypto/PasswordConverter.cs new file mode 100644 index 000000000..c2786152c --- /dev/null +++ b/crypto/src/crypto/PasswordConverter.cs @@ -0,0 +1,44 @@ +using System; +using System.Text; + +namespace Org.BouncyCastle.Crypto +{ + public class PasswordConverter + : ICharToByteConverter + { + private readonly string name; + private readonly Func converterFunction; + + public PasswordConverter(string name, Func converterFunction) + { + this.name = name; + this.converterFunction = converterFunction; + } + + public byte[] Convert(char[] password) + { + return converterFunction.Invoke(password); + } + + public string GetName() + { + return name; + } + + public readonly static ICharToByteConverter ASCII = new PasswordConverter("ASCII", PbeParametersGenerator.Pkcs5PasswordToBytes); + + public readonly static ICharToByteConverter UTF8 = new PasswordConverter("UTF8", PbeParametersGenerator.Pkcs5PasswordToUtf8Bytes); + + public readonly static ICharToByteConverter PKCS12 = new PasswordConverter("PKCS12", PbeParametersGenerator.Pkcs12PasswordToBytes); + + public readonly static ICharToByteConverter UTF32 = new PasswordConverter("UTF32", Encoding.UTF32.GetBytes); + + public readonly static ICharToByteConverter Unicode = new PasswordConverter("Unicode", Encoding.Unicode.GetBytes); + + public readonly static ICharToByteConverter BigEndianUnicode = new PasswordConverter("BigEndianUnicode", Encoding.BigEndianUnicode.GetBytes); + +#if NET6_0_OR_GREATER + public readonly static ICharToByteConverter Latin1 = new PasswordConverter("Latin1", Encoding.Latin1.GetBytes); +#endif + } +} \ No newline at end of file diff --git a/crypto/src/crypto/generators/Argon2BytesGenerator.cs b/crypto/src/crypto/generators/Argon2BytesGenerator.cs new file mode 100644 index 000000000..3fb0ea7f9 --- /dev/null +++ b/crypto/src/crypto/generators/Argon2BytesGenerator.cs @@ -0,0 +1,762 @@ +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Utilities; +using Org.BouncyCastle.Utilities; +using System; + +namespace Org.BouncyCastle.Crypto.Generators +{ + public sealed class Argon2BytesGenerator + { + private const int ARGON2_BLOCK_SIZE = 1024; + private const int ARGON2_QWORDS_IN_BLOCK = ARGON2_BLOCK_SIZE / 8; + + private const int ARGON2_ADDRESSES_IN_BLOCK = 128; + + private const int ARGON2_PREHASH_DIGEST_LENGTH = 64; + private const int ARGON2_PREHASH_SEED_LENGTH = 72; + + private const int ARGON2_SYNC_POINTS = 4; + + /* Minimum and maximum number of lanes (degree of parallelism) */ + private const int MIN_PARALLELISM = 1; + private const int MAX_PARALLELISM = 16777216; + + /* Minimum and maximum digest size in bytes */ + private const int MIN_OUTLEN = 4; + + /* Minimum and maximum number of passes */ + private const int MIN_ITERATIONS = 1; + + private const long M32L = 0xFFFFFFFFL; + + private readonly byte[] ZERO_BYTES = new byte[4]; + + private Argon2Parameters parameters; + private Block[] memory; + private int segmentLength; + private int laneLength; + + public Argon2BytesGenerator() + { + } + + /** + * Initialise the Argon2BytesGenerator from the parameters. + * + * @param parameters Argon2 configuration. + */ + public void Init(Argon2Parameters parameters) + { + this.parameters = parameters; + + if (parameters.GetLanes() < MIN_PARALLELISM) + { + throw new InvalidOperationException($"lanes must be greater than " + MIN_PARALLELISM); + } + else if (parameters.GetLanes() > MAX_PARALLELISM) + { + throw new InvalidOperationException("lanes must be less than " + MAX_PARALLELISM); + } + else if (parameters.GetMemory() < 2 * parameters.GetLanes()) + { + throw new InvalidOperationException("memory is less than: " + (2 * parameters.GetLanes()) + " expected " + (2 * parameters.GetLanes())); + } + else if (parameters.GetIterations() < MIN_ITERATIONS) + { + throw new InvalidOperationException("iterations is less than: " + MIN_ITERATIONS); + } + + DoInit(parameters); + } + + public int GenerateBytes(string password, byte[] output) + { + return GenerateBytes(password.ToCharArray(), output); + } + + public int GenerateBytes(string password, byte[] output, int outOff, int outLen) + { + return GenerateBytes(password.ToCharArray(), output, outOff, outLen); + } + + public int GenerateBytes(char[] password, byte[] output) + { + return GenerateBytes(parameters.GetCharToByteConverter().Convert(password), output); + } + + public int GenerateBytes(char[] password, byte[] output, int outOff, int outLen) + { + return GenerateBytes(parameters.GetCharToByteConverter().Convert(password), output, outOff, outLen); + } + + public int GenerateBytes(byte[] password, byte[] output) + { + return GenerateBytes(password, output, 0, output.Length); + } + + public int GenerateBytes(byte[] password, byte[] output, int outOff, int outLen) + { + if (outLen < MIN_OUTLEN) + { + throw new InvalidOperationException("output length less than " + MIN_OUTLEN); + } + + byte[] tmpBlockBytes = new byte[ARGON2_BLOCK_SIZE]; + + Initialize(tmpBlockBytes, password, outLen); + FillMemoryBlocks(); + Digest(tmpBlockBytes, output, outOff, outLen); + + Reset(); + + return outLen; + } + + // Clear memory. + private void Reset() + { + // Reset memory. + if (null != memory) + { + for (int i = 0; i < memory.Length; i++) + { + Block b = memory[i]; + b?.Clear(); + } + } + } + + private void DoInit(Argon2Parameters parameters) + { + /* 2. Align memory size */ + /* Minimum memoryBlocks = 8L blocks, where L is the number of lanes */ + int memoryBlocks = parameters.GetMemory(); + + if (memoryBlocks < 2 * ARGON2_SYNC_POINTS * parameters.GetLanes()) + { + memoryBlocks = 2 * ARGON2_SYNC_POINTS * parameters.GetLanes(); + } + + this.segmentLength = memoryBlocks / (parameters.GetLanes() * ARGON2_SYNC_POINTS); + this.laneLength = segmentLength * ARGON2_SYNC_POINTS; + + /* Ensure that all segments have equal length */ + memoryBlocks = segmentLength * (parameters.GetLanes() * ARGON2_SYNC_POINTS); + + InitMemory(memoryBlocks); + } + + private void InitMemory(int memoryBlocks) + { + this.memory = new Block[memoryBlocks]; + + for (int i = 0; i < memory.Length; i++) + { + memory[i] = new Block(); + } + } + + private void FillMemoryBlocks() + { + FillBlock filler = new FillBlock(); + Position position = new Position(); + for (int pass = 0; pass < parameters.GetIterations(); ++pass) + { + position.pass = pass; + + for (int slice = 0; slice < ARGON2_SYNC_POINTS; ++slice) + { + position.slice = slice; + + for (int lane = 0; lane < parameters.GetLanes(); ++lane) + { + position.lane = lane; + + FillSegment(filler, position); + } + } + } + } + + private void FillSegment(FillBlock filler, Position position) + { + Block addressBlock = null, inputBlock = null; + + bool dataIndependentAddressing = IsDataIndependentAddressing(position); + int startingIndex = GetStartingIndex(position); + int currentOffset = position.lane * laneLength + position.slice * segmentLength + startingIndex; + int prevOffset = GetPrevOffset(currentOffset); + + if (dataIndependentAddressing) + { + addressBlock = filler.addressBlock.Clear(); + inputBlock = filler.inputBlock.Clear(); + + InitAddressBlocks(filler, position, inputBlock, addressBlock); + } + + bool withXor = IsWithXor(position); + + for (int index = startingIndex; index < segmentLength; ++index) + { + long pseudoRandom = GetPseudoRandom( + filler, + index, + addressBlock, + inputBlock, + prevOffset, + dataIndependentAddressing); + + int refLane = GetRefLane(position, pseudoRandom); + int refColumn = GetRefColumn(position, index, pseudoRandom, refLane == position.lane); + + /* 2 Creating a new block */ + Block prevBlock = memory[prevOffset]; + Block refBlock = memory[((laneLength) * refLane + refColumn)]; + Block currentBlock = memory[currentOffset]; + + if (withXor) + { + filler.FillBlockWithXor(prevBlock, refBlock, currentBlock); + } + else + { + filler.Fill(prevBlock, refBlock, currentBlock); + } + + prevOffset = currentOffset; + currentOffset++; + } + } + + private bool IsDataIndependentAddressing(Position position) + { + return (parameters.GetArgonType() == Argon2Parameters.ARGON2_i) || + (parameters.GetArgonType() == Argon2Parameters.ARGON2_id + && (position.pass == 0) + && (position.slice < ARGON2_SYNC_POINTS / 2) + ); + } + + private void InitAddressBlocks(FillBlock filler, Position position, Block inputBlock, Block addressBlock) + { + inputBlock.v[0] = (long)position.pass; + inputBlock.v[1] = (long)position.lane; + inputBlock.v[2] = (long)position.slice; + inputBlock.v[3] = (long)memory.Length; + inputBlock.v[4] = (long)parameters.GetIterations(); + inputBlock.v[5] = (long)parameters.GetArgonType(); + + if ((position.pass == 0) && (position.slice == 0)) + { + /* Don't forget to generate the first block of addresses: */ + NextAddresses(filler, inputBlock, addressBlock); + } + } + + private bool IsWithXor(Position position) + { + return !(position.pass == 0 || parameters.GetVersion() == Argon2Parameters.ARGON2_VERSION_10); + } + + private int GetPrevOffset(int currentOffset) + { + if (currentOffset % laneLength == 0) + { + /* Last block in this lane */ + return currentOffset + laneLength - 1; + } + else + { + /* Previous block */ + return currentOffset - 1; + } + } + + private static int GetStartingIndex(Position position) + { + if ((position.pass == 0) && (position.slice == 0)) + { + return 2; /* we have already generated the first two blocks */ + } + else + { + return 0; + } + } + + private static void NextAddresses(FillBlock filler, Block inputBlock, Block addressBlock) + { + inputBlock.v[6]++; + filler.Fill(inputBlock, addressBlock); + filler.Fill(addressBlock, addressBlock); + } + + /* 1.2 Computing the index of the reference block */ + /* 1.2.1 Taking pseudo-random value from the previous block */ + private long GetPseudoRandom( + FillBlock filler, + int index, + Block addressBlock, + Block inputBlock, + int prevOffset, + bool dataIndependentAddressing) + { + if (dataIndependentAddressing) + { + int addressIndex = index % ARGON2_ADDRESSES_IN_BLOCK; + if (addressIndex == 0) + { + NextAddresses(filler, inputBlock, addressBlock); + } + return addressBlock.v[addressIndex]; + } + else + { + return memory[prevOffset].v[0]; + } + } + + private int GetRefLane(Position position, long pseudoRandom) + { + // Double-casting to/from ulong required because unsigned right shift operator + // >>> is supported only in C# 11 (.NET 7 or greater) + int refLane = (int)(((ulong)pseudoRandom >> 32) % (ulong)parameters.GetLanes()); + + if ((position.pass == 0) && (position.slice == 0)) + { + /* Can not reference other lanes yet */ + refLane = position.lane; + } + return refLane; + } + + private int GetRefColumn(Position position, int index, long pseudoRandom, bool sameLane) + { + long referenceAreaSize; + long startPosition; + + if (position.pass == 0) + { + startPosition = 0; + + if (sameLane) + { + /* The same lane => add current segment */ + referenceAreaSize = position.slice * segmentLength + index - 1; + } + else + { + /* pass == 0 && !sameLane => position.slice > 0*/ + referenceAreaSize = position.slice * segmentLength + ((index == 0) ? (-1) : 0); + } + } + else + { + startPosition = ((position.slice + 1) * segmentLength) % laneLength; + + if (sameLane) + { + referenceAreaSize = laneLength - segmentLength + index - 1; + } + else + { + referenceAreaSize = laneLength - segmentLength + ((index == 0) ? (-1) : 0); + } + } + + long relativePosition = pseudoRandom & 0xFFFFFFFFL; + + // Double-casting to/from ulong required because unsigned right shift operator + // >>> is supported only in C# 11 (.NET 7 or greater) + relativePosition = (long)((ulong)(relativePosition * relativePosition) >> 32); + relativePosition = referenceAreaSize - 1 - ((referenceAreaSize * relativePosition) >> 32); + + return (int)(startPosition + relativePosition) % laneLength; + } + + private void Digest(byte[] tmpBlockBytes, byte[] output, int outOff, int outLen) + { + Block finalBlock = memory[laneLength - 1]; + + /* XOR the last blocks */ + for (int i = 1; i < parameters.GetLanes(); i++) + { + int lastBlockInLane = i * laneLength + (laneLength - 1); + finalBlock.XorWith(memory[lastBlockInLane]); + } + + finalBlock.ToBytes(tmpBlockBytes); + + Hash(tmpBlockBytes, output, outOff, outLen); + } + + /** + * H' - hash - variable length hash function + */ + private static void Hash(byte[] input, byte[] output, int outOff, int outLen) + { + byte[] outLenBytes = new byte[4]; + Pack.UInt32_To_LE((uint)outLen, outLenBytes, 0); + + int blake2bLength = 64; + + if (outLen <= blake2bLength) + { + IDigest blake = new Blake2bDigest(outLen * 8); + + blake.BlockUpdate(outLenBytes, 0, outLenBytes.Length); + blake.BlockUpdate(input, 0, input.Length); + blake.DoFinal(output, outOff); + } + else + { + IDigest digest = new Blake2bDigest(blake2bLength * 8); + byte[] outBuffer = new byte[blake2bLength]; + + /* V1 */ + digest.BlockUpdate(outLenBytes, 0, outLenBytes.Length); + digest.BlockUpdate(input, 0, input.Length); + digest.DoFinal(outBuffer, 0); + + int halfLen = blake2bLength / 2, outPos = outOff; + Array.Copy(outBuffer, 0, output, outPos, halfLen); + outPos += halfLen; + + int r = ((outLen + 31) / 32) - 2; + + for (int i = 2; i <= r; i++, outPos += halfLen) + { + /* V2 to Vr */ + digest.BlockUpdate(outBuffer, 0, outBuffer.Length); + digest.DoFinal(outBuffer, 0); + + Array.Copy(outBuffer, 0, output, outPos, halfLen); + } + + int lastLength = outLen - 32 * r; + + /* Vr+1 */ + digest = new Blake2bDigest(lastLength * 8); + + digest.BlockUpdate(outBuffer, 0, outBuffer.Length); + digest.DoFinal(output, outPos); + } + } + + private static void RoundFunction(Block block, + int v0, int v1, int v2, int v3, + int v4, int v5, int v6, int v7, + int v8, int v9, int v10, int v11, + int v12, int v13, int v14, int v15) + { + long[] v = block.v; + + F(v, v0, v4, v8, v12); + F(v, v1, v5, v9, v13); + F(v, v2, v6, v10, v14); + F(v, v3, v7, v11, v15); + + F(v, v0, v5, v10, v15); + F(v, v1, v6, v11, v12); + F(v, v2, v7, v8, v13); + F(v, v3, v4, v9, v14); + } + + private static void F(long[] v, int a, int b, int c, int d) + { + QuarterRound(v, a, b, d, 32); + QuarterRound(v, c, d, b, 24); + QuarterRound(v, a, b, d, 16); + QuarterRound(v, c, d, b, 63); + } + + private static void QuarterRound(long[] v, int x, int y, int z, int s) + { + // fBlaMka(v, x, y); + // rotr64(v, z, x, s); + + long a = v[x], b = v[y], c = v[z]; + + a += b + 2 * (a & M32L) * (b & M32L); + c = Longs.RotateRight(c ^ a, s); + + v[x] = a; + v[z] = c; + } + + /*designed by the Lyra PHC team */ + /* a <- a + b + 2*aL*bL + * + == addition modulo 2^64 + * aL = least 32 bit */ + // private static void fBlaMka(long[] v, int x, int y) + // { + // final long a = v[x], b = v[y]; + // final long ab = (a & M32L) * (b & M32L); + // + // v[x] = a + b + 2 * ab; + // } + // + // private static void rotr64(long[] v, int x, int y, int s) + // { + // v[x] = Longs.rotateRight(v[x] ^ v[y], s); + // } + + private void Initialize(byte[] tmpBlockBytes, byte[] password, int outputLength) + { + /* + * H0 = H64(p, τ, m, t, v, y, |P|, P, |S|, S, |L|, K, |X|, X) + * -> 64 byte (ARGON2_PREHASH_DIGEST_LENGTH) + */ + + Blake2bDigest blake = new Blake2bDigest(ARGON2_PREHASH_DIGEST_LENGTH * 8); + + int[] values = { + parameters.GetLanes(), + outputLength, + parameters.GetMemory(), + parameters.GetIterations(), + parameters.GetVersion(), + parameters.GetArgonType() + }; + + Helpers.IntArrayToLittleEndian(values, tmpBlockBytes, 0); + blake.BlockUpdate(tmpBlockBytes, 0, values.Length * 4); + + AddByteString(tmpBlockBytes, blake, password); + AddByteString(tmpBlockBytes, blake, parameters.GetSalt()); + AddByteString(tmpBlockBytes, blake, parameters.GetSecret()); + AddByteString(tmpBlockBytes, blake, parameters.GetAdditional()); + + byte[] initialHashWithZeros = new byte[ARGON2_PREHASH_SEED_LENGTH]; + blake.DoFinal(initialHashWithZeros, 0); + + FillFirstBlocks(tmpBlockBytes, initialHashWithZeros); + } + + private void AddByteString(byte[] tmpBlockBytes, IDigest digest, byte[] octets) + { + if (null == octets) + { + digest.BlockUpdate(ZERO_BYTES, 0, 4); + return; + } + + Pack.UInt32_To_LE((uint)octets.Length, tmpBlockBytes, 0); + digest.BlockUpdate(tmpBlockBytes, 0, 4); + digest.BlockUpdate(octets, 0, octets.Length); + } + + /** + * (H0 || 0 || i) 72 byte -> 1024 byte + * (H0 || 1 || i) 72 byte -> 1024 byte + */ + private void FillFirstBlocks(byte[] tmpBlockBytes, byte[] initialHashWithZeros) + { + byte[] initialHashWithOnes = new byte[ARGON2_PREHASH_SEED_LENGTH]; + Array.Copy(initialHashWithZeros, 0, initialHashWithOnes, 0, ARGON2_PREHASH_DIGEST_LENGTH); + // Pack.intToLittleEndian(1, initialHashWithOnes, ARGON2_PREHASH_DIGEST_LENGTH); + initialHashWithOnes[ARGON2_PREHASH_DIGEST_LENGTH] = 1; + + for (int i = 0; i < parameters.GetLanes(); i++) + { + Pack.UInt32_To_LE((uint)i, initialHashWithZeros, ARGON2_PREHASH_DIGEST_LENGTH + 4); + Pack.UInt32_To_LE((uint)i, initialHashWithOnes, ARGON2_PREHASH_DIGEST_LENGTH + 4); + + Hash(initialHashWithZeros, tmpBlockBytes, 0, ARGON2_BLOCK_SIZE); + memory[i * laneLength + 0].FromBytes(tmpBlockBytes); + + Hash(initialHashWithOnes, tmpBlockBytes, 0, ARGON2_BLOCK_SIZE); + memory[i * laneLength + 1].FromBytes(tmpBlockBytes); + } + } + + private sealed class FillBlock + { + private readonly Block R = new Block(); + private readonly Block Z = new Block(); + + internal readonly Block addressBlock = new Block(); + internal readonly Block inputBlock = new Block(); + + internal void ApplyBlake() + { + /* Apply Blake2 on columns of 64-bit words: (0,1,...,15) , then + (16,17,..31)... finally (112,113,...127) */ + for (int i = 0; i < 8; i++) + { + + int i16 = 16 * i; + RoundFunction(Z, + i16, i16 + 1, i16 + 2, + i16 + 3, i16 + 4, i16 + 5, + i16 + 6, i16 + 7, i16 + 8, + i16 + 9, i16 + 10, i16 + 11, + i16 + 12, i16 + 13, i16 + 14, + i16 + 15 + ); + } + + /* Apply Blake2 on rows of 64-bit words: (0,1,16,17,...112,113), then + (2,3,18,19,...,114,115).. finally (14,15,30,31,...,126,127) */ + for (int i = 0; i < 8; i++) + { + + int i2 = 2 * i; + RoundFunction(Z, + i2, i2 + 1, i2 + 16, + i2 + 17, i2 + 32, i2 + 33, + i2 + 48, i2 + 49, i2 + 64, + i2 + 65, i2 + 80, i2 + 81, + i2 + 96, i2 + 97, i2 + 112, + i2 + 113 + ); + } + } + + internal void Fill(Block Y, Block currentBlock) + { + Z.CopyBlock(Y); + ApplyBlake(); + currentBlock.Xor(Y, Z); + } + + internal void Fill(Block X, Block Y, Block currentBlock) + { + R.Xor(X, Y); + Z.CopyBlock(R); + ApplyBlake(); + currentBlock.Xor(R, Z); + } + + internal void FillBlockWithXor(Block X, Block Y, Block currentBlock) + { + R.Xor(X, Y); + Z.CopyBlock(R); + ApplyBlake(); + currentBlock.XorWith(R, Z); + } + } + + private sealed class Block + { + private const int SIZE = ARGON2_QWORDS_IN_BLOCK; + + /* 128 * 8 Byte QWords */ + internal readonly long[] v; + + internal Block() + { + v = new long[SIZE]; + } + + internal void FromBytes(byte[] input) + { + if (input.Length < ARGON2_BLOCK_SIZE) + { + throw new ArgumentException("input shorter than blocksize"); + } + + Helpers.LittleEndianToLongArray(input, 0, v); + } + + internal void ToBytes(byte[] output) + { + if (output.Length < ARGON2_BLOCK_SIZE) + { + throw new ArgumentException("output shorter than blocksize"); + } + + Helpers.LongArrayToLittleEndian(v, output, 0); + } + + internal void CopyBlock(Block other) + { + Array.Copy(other.v, 0, v, 0, SIZE); + } + + internal void Xor(Block b1, Block b2) + { + long[] v0 = v; + long[] v1 = b1.v; + long[] v2 = b2.v; + + for (int i = 0; i < SIZE; i++) + { + v0[i] = v1[i] ^ v2[i]; + } + } + + internal void XorWith(Block b1) + { + long[] v0 = v; + long[] v1 = b1.v; + + for (int i = 0; i < SIZE; i++) + { + v0[i] ^= v1[i]; + } + } + + internal void XorWith(Block b1, Block b2) + { + long[] v0 = v; + long[] v1 = b1.v; + long[] v2 = b2.v; + for (int i = 0; i < SIZE; i++) + { + v0[i] ^= v1[i] ^ v2[i]; + } + } + + internal Block Clear() + { + Arrays.Fill(v, 0); + return this; + } + } + + private sealed class Position + { + internal int pass; + internal int lane; + internal int slice; + + internal Position() + { + } + } + + private static class Helpers + { + internal static void LittleEndianToLongArray(byte[] bs, int off, long[] ns) + { + for (int i = 0; i < ns.Length; ++i) + { + ns[i] = (long)Pack.LE_To_UInt64(bs, off); + off += 8; + } + } + + internal static void LongArrayToLittleEndian(long[] ns, byte[] bs, int off) + { + for (int i = 0; i < ns.Length; ++i) + { + Pack.UInt64_To_LE((ulong)ns[i], bs, off); + off += 8; + } + } + + internal static void IntArrayToLittleEndian(int[] ns, byte[] bs, int off) + { + for (int i = 0; i < ns.Length; ++i) + { + Pack.UInt32_To_LE((uint)ns[i], bs, off); + off += 4; + } + } + + } + } +} diff --git a/crypto/src/crypto/parameters/Argon2Parameters.cs b/crypto/src/crypto/parameters/Argon2Parameters.cs new file mode 100644 index 000000000..f514d862e --- /dev/null +++ b/crypto/src/crypto/parameters/Argon2Parameters.cs @@ -0,0 +1,208 @@ +using Org.BouncyCastle.Utilities; +using System; + +namespace Org.BouncyCastle.Crypto.Parameters +{ + public sealed class Argon2Parameters + { + public const int ARGON2_d = 0x00; + public const int ARGON2_i = 0x01; + public const int ARGON2_id = 0x02; + + public const int ARGON2_VERSION_10 = 0x10; + public const int ARGON2_VERSION_13 = 0x13; + + private readonly byte[] salt; + private readonly byte[] secret; + private readonly byte[] additional; + + private readonly int iterations; + private readonly int memory; + private readonly int lanes; + + private readonly int version; + private readonly int type; + private readonly ICharToByteConverter converter; + + public class Builder + { + private const int DEFAULT_ITERATIONS = 3; + private const int DEFAULT_MEMORY_COST = 12; + private const int DEFAULT_LANES = 1; + private const int DEFAULT_TYPE = ARGON2_i; + private const int DEFAULT_VERSION = ARGON2_VERSION_13; + + private byte[] salt = Array.Empty(); + private byte[] secret = Array.Empty(); + private byte[] additional = Array.Empty(); + + private int iterations; + private int memory; + private int lanes; + + private int version; + private readonly int type; + + private ICharToByteConverter converter = PasswordConverter.UTF8; + + public Builder() + : this(DEFAULT_TYPE) + { + } + + public Builder(int type) + { + this.type = type; + lanes = DEFAULT_LANES; + memory = 1 << DEFAULT_MEMORY_COST; + iterations = DEFAULT_ITERATIONS; + version = DEFAULT_VERSION; + } + + public Builder WithParallelism(int parallelism) + { + lanes = parallelism; + return this; + } + + public Builder WithSalt(byte[] salt) + { + this.salt = Arrays.Clone(salt); + return this; + } + + public Builder WithSecret(byte[] secret) + { + this.secret = Arrays.Clone(secret); + return this; + } + + public Builder WithAdditional(byte[] additional) + { + this.additional = Arrays.Clone(additional); + return this; + } + + public Builder WithIterations(int iterations) + { + this.iterations = iterations; + return this; + } + + public Builder WithMemoryAsKB(int memory) + { + this.memory = memory; + return this; + } + + public Builder WithMemoryPowOfTwo(int memory) + { + this.memory = 1 << memory; + return this; + } + + public Builder WithVersion(int version) + { + this.version = version; + return this; + } + + public Builder WithCharToByteConverter(ICharToByteConverter converter) + { + this.converter = converter; + return this; + } + + public Builder WithCharToByteConverter(string name, Func converterFunction) + { + return WithCharToByteConverter(new PasswordConverter(name, converterFunction)); + } + + public Builder WithCharToByteConverter(Func converterFunction) + { + return WithCharToByteConverter("Custom", converterFunction); + } + + public Argon2Parameters Build() + { + return new Argon2Parameters(type, salt, secret, additional, iterations, memory, lanes, version, converter); + } + + public void Clear() + { + Arrays.Clear(salt); + Arrays.Clear(secret); + Arrays.Clear(additional); + } + } + + private Argon2Parameters( + int type, + byte[] salt, + byte[] secret, + byte[] additional, + int iterations, + int memory, + int lanes, + int version, + ICharToByteConverter converter) + { + + this.salt = Arrays.Clone(salt); + this.secret = Arrays.Clone(secret); + this.additional = Arrays.Clone(additional); + this.iterations = iterations; + this.memory = memory; + this.lanes = lanes; + this.version = version; + this.type = type; + this.converter = converter; + } + + public byte[] GetSalt() + { + return Arrays.Clone(salt); + } + + public byte[] GetSecret() + { + return Arrays.Clone(secret); + } + + public byte[] GetAdditional() + { + return Arrays.Clone(additional); + } + + public int GetIterations() + { + return iterations; + } + + public int GetMemory() + { + return memory; + } + + public int GetLanes() + { + return lanes; + } + + public int GetVersion() + { + return version; + } + + public int GetArgonType() + { + return type; + } + + public ICharToByteConverter GetCharToByteConverter() + { + return converter; + } + + } +} diff --git a/crypto/test/src/crypto/test/Argon2Test.cs b/crypto/test/src/crypto/test/Argon2Test.cs new file mode 100644 index 000000000..23a356100 --- /dev/null +++ b/crypto/test/src/crypto/test/Argon2Test.cs @@ -0,0 +1,377 @@ +using NUnit.Framework; + +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.Utilities.Test; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Org.BouncyCastle.Crypto.Tests +{ + [TestFixture] + public class Argon2Test + : SimpleTest + { + private const int DEFAULT_OUTPUTLEN = 32; + + public override string Name => "ArgonTest"; + public override void PerformTest() + { + TestExceptions(); + TestPermutations(); + TestVectorsFromSpecs(); + HashTestsVersion10(); + HashTestsVersion13(); + } + + #region "Exception tests" + [Test] + public void TestExceptions() + { + // lanes less than MIN_PARALLELISM + Assert.Throws(() => { + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + Argon2Parameters.Builder builder = new Argon2Parameters.Builder() + .WithParallelism(0); + + gen.Init(builder.Build()); + }); + + // lanes greater than MAX_PARALLELISM + Assert.Throws(() => { + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + Argon2Parameters.Builder builder = new Argon2Parameters.Builder() + .WithParallelism(16777299); + + gen.Init(builder.Build()); + }); + + // iterations less than MIN_ITERATIONS + Assert.Throws(() => { + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + Argon2Parameters.Builder builder = new Argon2Parameters.Builder() + .WithIterations(0); + + gen.Init(builder.Build()); + }); + + // memory less than 2 * lanes + Assert.Throws(() => { + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + Argon2Parameters.Builder builder = new Argon2Parameters.Builder() + .WithMemoryAsKB(10) + .WithParallelism(6); + + gen.Init(builder.Build()); + }); + + // output length less than MIN_OUTLEN + Assert.Throws(() => { + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + Argon2Parameters.Builder builder = new Argon2Parameters.Builder(); + + gen.Init(builder.Build()); + + byte[] result = new byte[3]; + gen.GenerateBytes("password", result); + }); + + } + #endregion + + #region "Permutation based tests" + [Test] + public void TestPermutations() + { + byte[] rootPassword = Encoding.ASCII.GetBytes("aac"); + byte[] buf; + + byte[][] salts = new byte[][] { + new byte[16], + new byte[16], + new byte[16] + }; + + for (int t = 0; t< 16; t++) + { + salts[1][t] = (byte) t; + salts[2][t] = (byte) (16 - t); + } + + // + // Permutation, starting with a shorter array, same length then one longer. + // + for (int j = rootPassword.Length - 1; j permutations = new List(); + Permute(permutations, buf, 0, buf.Length - 1); + + for (int i = 0; i != permutations.Count; i++) + { + byte[] candidate = permutations[i]; + for (int k = 0; k != salts.Length; k++) + { + byte[] salt = salts[k]; + byte[] expected = Generate(Argon2Parameters.ARGON2_VERSION_10, 1, 8, 2, rootPassword, salt, 32); + byte[] testValue = Generate(Argon2Parameters.ARGON2_VERSION_10, 1, 8, 2, candidate, salt, 32); + + // + // If the passwords are the same for the same salt we should have the same string. + // + bool sameAsRoot = Arrays.AreEqual(rootPassword, candidate); + IsTrue("expected same result", sameAsRoot == Arrays.AreEqual(expected, testValue)); + } + } + } + } + } + + private static void Swap(byte[] buf, int i, int j) + { + byte b = buf[i]; + buf[i] = buf[j]; + buf[j] = b; + } + + private static void Permute(List permutation, byte[] a, int l, int r) + { + if (l == r) + { + permutation.Add(Arrays.Clone(a)); + } + else + { + + for (int i = l; i <= r; i++) + { + // Swapping done + Swap(a, l, i); + + // Recursion called + Permute(permutation, a, l + 1, r); + + //backtrack + Swap(a, l, i); + } + } + } + + private static byte[] Generate(int version, int iterations, int memory, int parallelism, + byte[] password, byte[] salt, int outputLength) + { + Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i) + .WithVersion(version) + .WithIterations(iterations) + .WithMemoryPowOfTwo(memory) + .WithParallelism(parallelism) + .WithSalt(salt); + + // + // Set the password. + // + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + + gen.Init(builder.Build()); + + byte[] result = new byte[outputLength]; + + gen.GenerateBytes(password, result, 0, result.Length); + return result; + } + #endregion + + #region "Hash tests" + /* Multiple test cases for various input values */ + [Test] + public void HashTestsVersion10() + { + /* Multiple test cases for various input values */ + + int version = Argon2Parameters.ARGON2_VERSION_10; + + HashTest(version, 2, 16, 1, "password", "somesalt", + "f6c4db4a54e2a370627aff3db6176b94a2a209a62c8e36152711802f7b30c694", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 20, 1, "password", "somesalt", + "9690ec55d28d3ed32562f2e73ea62b02b018757643a2ae6e79528459de8106e9", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 18, 1, "password", "somesalt", + "3e689aaa3d28a77cf2bc72a51ac53166761751182f1ee292e3f677a7da4c2467", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 8, 1, "password", "somesalt", + "fd4dd83d762c49bdeaf57c47bdcd0c2f1babf863fdeb490df63ede9975fccf06", + DEFAULT_OUTPUTLEN); + HashTest(version, 2, 8, 2, "password", "somesalt", + "b6c11560a6a9d61eac706b79a2f97d68b4463aa3ad87e00c07e2b01e90c564fb", + DEFAULT_OUTPUTLEN); + + HashTest(version, 1, 16, 1, "password", "somesalt", + "81630552b8f3b1f48cdb1992c4c678643d490b2b5eb4ff6c4b3438b5621724b2", + DEFAULT_OUTPUTLEN); + + HashTest(version, 4, 16, 1, "password", "somesalt", + "f212f01615e6eb5d74734dc3ef40ade2d51d052468d8c69440a3a1f2c1c2847b", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 16, 1, "differentpassword", "somesalt", + "e9c902074b6754531a3a0be519e5baf404b30ce69b3f01ac3bf21229960109a3", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 16, 1, "password", "diffsalt", + "79a103b90fe8aef8570cb31fc8b22259778916f8336b7bdac3892569d4f1c497", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 16, 1, "password", "diffsalt", + "1a097a5d1c80e579583f6e19c7e4763ccb7c522ca85b7d58143738e12ca39f8e6e42734c950ff2463675b97c37ba" + + "39feba4a9cd9cc5b4c798f2aaf70eb4bd044c8d148decb569870dbd923430b82a083f284beae777812cce18cdac68ee8ccef" + + "c6ec9789f30a6b5a034591f51af830f4", + 112); + + } + + [Test] + public void HashTestsVersion13() + { + int version = Argon2Parameters.ARGON2_VERSION_13; + + HashTest(version, 2, 16, 1, "password", "somesalt", + "c1628832147d9720c5bd1cfd61367078729f6dfb6f8fea9ff98158e0d7816ed0", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 20, 1, "password", "somesalt", + "d1587aca0922c3b5d6a83edab31bee3c4ebaef342ed6127a55d19b2351ad1f41", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 18, 1, "password", "somesalt", + "296dbae80b807cdceaad44ae741b506f14db0959267b183b118f9b24229bc7cb", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 8, 1, "password", "somesalt", + "89e9029f4637b295beb027056a7336c414fadd43f6b208645281cb214a56452f", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 8, 2, "password", "somesalt", + "4ff5ce2769a1d7f4c8a491df09d41a9fbe90e5eb02155a13e4c01e20cd4eab61", + DEFAULT_OUTPUTLEN); + + HashTest(version, 1, 16, 1, "password", "somesalt", + "d168075c4d985e13ebeae560cf8b94c3b5d8a16c51916b6f4ac2da3ac11bbecf", + DEFAULT_OUTPUTLEN); + + HashTest(version, 4, 16, 1, "password", "somesalt", + "aaa953d58af3706ce3df1aefd4a64a84e31d7f54175231f1285259f88174ce5b", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 16, 1, "differentpassword", "somesalt", + "14ae8da01afea8700c2358dcef7c5358d9021282bd88663a4562f59fb74d22ee", + DEFAULT_OUTPUTLEN); + + HashTest(version, 2, 16, 1, "password", "diffsalt", + "b0357cccfbef91f3860b0dba447b2348cbefecadaf990abfe9cc40726c521271", + DEFAULT_OUTPUTLEN); + + } + + private void HashTest(int version, int iterations, int memory, int parallelism, + string password, string salt, String passwordRef, int outputLength) + { + Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i) + .WithVersion(version) + .WithIterations(iterations) + .WithMemoryPowOfTwo(memory) + .WithParallelism(parallelism) + .WithSalt(Encoding.ASCII.GetBytes(salt)); + + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + + gen.Init(builder.Build()); + + byte[] result = new byte[outputLength]; + gen.GenerateBytes(password, result, 0, result.Length); + + IsTrue(passwordRef + " Failed", AreEqual(result, Hex.Decode(passwordRef))); + } + #endregion + + #region "Known Answer Tests (KATs) from specifications" + private void SpecsTest(int version, int type, string passwordRef) + { + byte[] ad = Hex.Decode("040404040404040404040404"); + byte[] secret = Hex.Decode("0303030303030303"); + byte[] salt = Hex.Decode("02020202020202020202020202020202"); + byte[] password = Hex.Decode("0101010101010101010101010101010101010101010101010101010101010101"); + + byte[] expected = Hex.Decode(passwordRef); + byte[] result = new byte[32]; + + Argon2Parameters.Builder builder = new Argon2Parameters.Builder(type) + .WithVersion(version) + .WithIterations(3) + .WithMemoryAsKB(32) + .WithParallelism(4) + .WithAdditional(ad) + .WithSecret(secret) + .WithSalt(salt); + + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + + gen.Init(builder.Build()); + + gen.GenerateBytes(password, result, 0, result.Length); + + IsTrue(passwordRef + " Failed", AreEqual(result, expected)); + } + + [Test] + public void TestVectorsFromSpecs() + { + /* Version 0x13 (19) from RFC 9106 https://datatracker.ietf.org/doc/html/rfc9106#name-test-vectors */ + SpecsTest( + Argon2Parameters.ARGON2_VERSION_13, + Argon2Parameters.ARGON2_d, + "512b391b6f1162975371d30919734294f868e3be3984f3c1a13a4db9fabe4acb"); + + SpecsTest( + Argon2Parameters.ARGON2_VERSION_13, + Argon2Parameters.ARGON2_i, + "c814d9d1dc7f37aa13f0d77f2494bda1c8de6b016dd388d29952a4c4672b6ce8"); + + SpecsTest( + Argon2Parameters.ARGON2_VERSION_13, + Argon2Parameters.ARGON2_id, + "0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659"); + + /* Version 0x10 (16) from reference C implementation https://github.com/P-H-C/phc-winner-argon2/tree/master/kats */ + SpecsTest( + Argon2Parameters.ARGON2_VERSION_10, + Argon2Parameters.ARGON2_d, + "96a9d4e5a1734092c85e29f410a45914a5dd1f5cbf08b2670da68a0285abf32b"); + + SpecsTest( + Argon2Parameters.ARGON2_VERSION_10, + Argon2Parameters.ARGON2_i, + "87aeedd6517ab830cd9765cd8231abb2e647a5dee08f7c05e02fcb763335d0fd"); + + SpecsTest( + Argon2Parameters.ARGON2_VERSION_10, + Argon2Parameters.ARGON2_id, + "b64615f07789b66b645b67ee9ed3b377ae350b6bfcbb0fc95141ea8f322613c0"); + } + #endregion + } +} From fa13755456017bc33e5167524098b2bd691af9ab Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Sat, 10 Feb 2024 15:44:32 +0100 Subject: [PATCH 02/37] New algorithms and packet types --- crypto/src/bcpg/AeadAlgorithmTag.cs | 12 ++++++ crypto/src/bcpg/PacketTags.cs | 8 +++- crypto/src/bcpg/PublicKeyAlgorithmTags.cs | 13 +++++- crypto/src/bcpg/SymmetricKeyAlgorithmTags.cs | 29 +++++++++---- crypto/src/openpgp/PgpUtilities.cs | 45 +++++++++++++------- 5 files changed, 81 insertions(+), 26 deletions(-) diff --git a/crypto/src/bcpg/AeadAlgorithmTag.cs b/crypto/src/bcpg/AeadAlgorithmTag.cs index 632f88838..f02bc90ae 100644 --- a/crypto/src/bcpg/AeadAlgorithmTag.cs +++ b/crypto/src/bcpg/AeadAlgorithmTag.cs @@ -7,5 +7,17 @@ public enum AeadAlgorithmTag : byte Eax = 1, // EAX (IV len: 16 octets, Tag len: 16 octets) Ocb = 2, // OCB (IV len: 15 octets, Tag len: 16 octets) Gcm = 3, // GCM (IV len: 12 octets, Tag len: 16 octets) + + Experimental_1 = 100, + Experimental_2 = 101, + Experimental_3 = 102, + Experimental_4 = 103, + Experimental_5 = 104, + Experimental_6 = 105, + Experimental_7 = 106, + Experimental_8 = 107, + Experimental_9 = 108, + Experimental_10 = 109, + Experimental_11 = 110, } } diff --git a/crypto/src/bcpg/PacketTags.cs b/crypto/src/bcpg/PacketTags.cs index 5a53d4e95..d6c2fc075 100644 --- a/crypto/src/bcpg/PacketTags.cs +++ b/crypto/src/bcpg/PacketTags.cs @@ -19,8 +19,12 @@ public enum PacketTag UserId = 13, // User ID Packet PublicSubkey = 14, // Public Subkey Packet UserAttribute = 17, // User attribute - SymmetricEncryptedIntegrityProtected = 18, // Symmetric encrypted, integrity protected - ModificationDetectionCode = 19, // Modification detection code + SymmetricEncryptedIntegrityProtected = 18, // Symmetric encrypted, integrity protected + + ModificationDetectionCode = 19, // Reserved (formerly Modification Detection Code Packet) + ReservedAeadEncryptedData = 20, // Reserved (defined as AEAD Encrypted Data Packet in retired draft [draft-koch-openpgp-rfc4880bis]) + + Padding = 21, // Padding Packet [https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#padding-packet] Experimental1 = 60, // Private or Experimental Values Experimental2 = 61, diff --git a/crypto/src/bcpg/PublicKeyAlgorithmTags.cs b/crypto/src/bcpg/PublicKeyAlgorithmTags.cs index a309b65ae..7fa4d728a 100644 --- a/crypto/src/bcpg/PublicKeyAlgorithmTags.cs +++ b/crypto/src/bcpg/PublicKeyAlgorithmTags.cs @@ -12,7 +12,8 @@ public enum PublicKeyAlgorithmTag Dsa = 17, // DSA (Digital Signature Standard) ECDH = 18, // Reserved for Elliptic Curve (actual algorithm name) ECDsa = 19, // Reserved for ECDSA - ElGamalGeneral = 20, // Elgamal (Encrypt or Sign) + + ElGamalGeneral = 20, // Reserved (formerly Elgamal Encrypt or Sign) DiffieHellman = 21, // Reserved for Diffie-Hellman (X9.42, as defined for IETF-S/MIME) // TODO Mark obsolete once Ed25519, Ed448 available @@ -20,6 +21,16 @@ public enum PublicKeyAlgorithmTag EdDsa = 22, // EdDSA - (internet draft, but appearing in use) EdDsa_Legacy = 22, // new name for old EdDSA tag. + // defined as Reserver by crypto-refresh draft + AEDH = 23, + AEDSA = 24, + + // https://datatracker.ietf.org/doc/draft-ietf-openpgp-crypto-refresh/ + X25519 = 25, + X448 = 26, + Ed25519 = 27, + Ed448 = 28, + Experimental_1 = 100, Experimental_2 = 101, Experimental_3 = 102, diff --git a/crypto/src/bcpg/SymmetricKeyAlgorithmTags.cs b/crypto/src/bcpg/SymmetricKeyAlgorithmTags.cs index e05a48616..abd00fa21 100644 --- a/crypto/src/bcpg/SymmetricKeyAlgorithmTags.cs +++ b/crypto/src/bcpg/SymmetricKeyAlgorithmTags.cs @@ -10,14 +10,27 @@ public enum SymmetricKeyAlgorithmTag TripleDes = 2, // Triple-DES (DES-EDE, as per spec -168 bit key derived from 192) Cast5 = 3, // Cast5 (128 bit key, as per RFC 2144) Blowfish = 4, // Blowfish (128 bit key, 16 rounds) [Blowfish] - Safer = 5, // Safer-SK128 (13 rounds) [Safer] + Safer = 5, // Reserved - formerly Safer-SK128 (13 rounds) [Safer] Des = 6, // Reserved for DES/SK - Aes128 = 7, // Reserved for AES with 128-bit key - Aes192 = 8, // Reserved for AES with 192-bit key - Aes256 = 9, // Reserved for AES with 256-bit key - Twofish = 10, // Reserved for Twofish - Camellia128 = 11, // Reserved for AES with 128-bit key - Camellia192 = 12, // Reserved for AES with 192-bit key - Camellia256 = 13 // Reserved for AES with 256-bit key + Aes128 = 7, // AES with 128-bit key + Aes192 = 8, // AES with 192-bit key + Aes256 = 9, // AES with 256-bit key + Twofish = 10, // Twofish with 256-bit key [TWOFISH] + Camellia128 = 11, // Camellia with 128-bit key [RFC3713] + Camellia192 = 12, // Camellia with 192-bit key + Camellia256 = 13, // Camellia with 256-bit key + + + Experimental_1 = 100, + Experimental_2 = 101, + Experimental_3 = 102, + Experimental_4 = 103, + Experimental_5 = 104, + Experimental_6 = 105, + Experimental_7 = 106, + Experimental_8 = 107, + Experimental_9 = 108, + Experimental_10 = 109, + Experimental_11 = 110 } } diff --git a/crypto/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index fa04f5f46..c760131e7 100644 --- a/crypto/src/openpgp/PgpUtilities.cs +++ b/crypto/src/openpgp/PgpUtilities.cs @@ -26,18 +26,22 @@ public sealed class PgpUtilities private static IDictionary CreateNameToHashID() { - var d = new Dictionary(StringComparer.OrdinalIgnoreCase); - d.Add("sha1", HashAlgorithmTag.Sha1); - d.Add("sha224", HashAlgorithmTag.Sha224); - d.Add("sha256", HashAlgorithmTag.Sha256); - d.Add("sha384", HashAlgorithmTag.Sha384); - d.Add("sha512", HashAlgorithmTag.Sha512); - d.Add("ripemd160", HashAlgorithmTag.RipeMD160); - d.Add("rmd160", HashAlgorithmTag.RipeMD160); - d.Add("md2", HashAlgorithmTag.MD2); - d.Add("tiger", HashAlgorithmTag.Tiger192); - d.Add("haval", HashAlgorithmTag.Haval5pass160); - d.Add("md5", HashAlgorithmTag.MD5); + var d = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "sha1", HashAlgorithmTag.Sha1 }, + { "sha224", HashAlgorithmTag.Sha224 }, + { "sha256", HashAlgorithmTag.Sha256 }, + { "sha384", HashAlgorithmTag.Sha384 }, + { "sha512", HashAlgorithmTag.Sha512 }, + { "ripemd160", HashAlgorithmTag.RipeMD160 }, + { "rmd160", HashAlgorithmTag.RipeMD160 }, + { "md2", HashAlgorithmTag.MD2 }, + { "tiger", HashAlgorithmTag.Tiger192 }, + { "haval", HashAlgorithmTag.Haval5pass160 }, + { "md5", HashAlgorithmTag.MD5 }, + { "Sha3_256" , HashAlgorithmTag.Sha3_256}, + { "Sha3_512" , HashAlgorithmTag.Sha3_512}, + }; return d; } @@ -102,7 +106,11 @@ public static string GetDigestName( return "SHA384"; case HashAlgorithmTag.Sha512: return "SHA512"; - default: + case HashAlgorithmTag.Sha3_256: + return "SHA3-256"; + case HashAlgorithmTag.Sha3_512: + return "SHA3-512"; + default: throw new PgpException("unknown hash algorithm tag in GetDigestName: " + hashAlgorithm); } } @@ -157,11 +165,18 @@ public static string GetSignatureName( case PublicKeyAlgorithmTag.ElGamalGeneral: encAlg = "ElGamal"; break; - default: + case PublicKeyAlgorithmTag.Ed25519: + encAlg = "Ed25519"; + break; + case PublicKeyAlgorithmTag.Ed448: + encAlg = "Ed448"; + break; + default: throw new PgpException("unknown algorithm tag in signature:" + keyAlgorithm); } - return GetDigestName(hashAlgorithm) + "with" + encAlg; + string digestName = GetDigestName(hashAlgorithm); + return $"{digestName}with{encAlg}"; } public static string GetSymmetricCipherName( From 5dcbac68917755bbc05d382bed46b86257c9ec53 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Sun, 11 Feb 2024 16:56:30 +0100 Subject: [PATCH 03/37] Padding Packet support --- crypto/src/bcpg/BcpgInputStream.cs | 9 +- crypto/src/bcpg/PaddingPacket.cs | 49 ++++++ crypto/src/openpgp/PgpObjectFactory.cs | 2 + crypto/src/openpgp/PgpPadding.cs | 21 +++ .../IgnoreMarkerPacketInCertificatesTest.cs | 57 ++++++- .../src/openpgp/test/PaddingPacketTest.cs | 142 ++++++++++++++++++ 6 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 crypto/src/bcpg/PaddingPacket.cs create mode 100644 crypto/src/openpgp/PgpPadding.cs create mode 100644 crypto/test/src/openpgp/test/PaddingPacketTest.cs diff --git a/crypto/src/bcpg/BcpgInputStream.cs b/crypto/src/bcpg/BcpgInputStream.cs index 03aa717d3..efd258c9d 100644 --- a/crypto/src/bcpg/BcpgInputStream.cs +++ b/crypto/src/bcpg/BcpgInputStream.cs @@ -241,6 +241,8 @@ public Packet ReadPacket() return new SymmetricEncIntegrityPacket(objStream); case PacketTag.ModificationDetectionCode: return new ModDetectionCodePacket(objStream); + case PacketTag.Padding: + return new PaddingPacket(objStream); case PacketTag.Experimental1: case PacketTag.Experimental2: case PacketTag.Experimental3: @@ -252,9 +254,14 @@ public Packet ReadPacket() } public PacketTag SkipMarkerPackets() + { + return SkipMarkerAndPaddingPackets(); + } + + public PacketTag SkipMarkerAndPaddingPackets() { PacketTag tag; - while ((tag = NextPacketTag()) == PacketTag.Marker) + while ((tag = NextPacketTag()) == PacketTag.Marker || tag == PacketTag.Padding) { ReadPacket(); } diff --git a/crypto/src/bcpg/PaddingPacket.cs b/crypto/src/bcpg/PaddingPacket.cs new file mode 100644 index 000000000..4063c8a16 --- /dev/null +++ b/crypto/src/bcpg/PaddingPacket.cs @@ -0,0 +1,49 @@ +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; + +namespace Org.BouncyCastle.Bcpg +{ + public class PaddingPacket + : ContainedPacket + { + private readonly byte[] padding; + + public PaddingPacket(BcpgInputStream bcpgIn) + { + padding = bcpgIn.ReadAll(); + } + + public PaddingPacket(int length, BcpgInputStream bcpgIn) + { + padding = new byte[length]; + bcpgIn.ReadFully(padding); + } + + public PaddingPacket(byte[] padding) + { + this.padding = Arrays.Clone(padding); + } + + public PaddingPacket(int length, SecureRandom random) + : this(RandomBytes(length, random)) + { + } + + private static byte[] RandomBytes(int length, SecureRandom random) + { + byte[] bytes = new byte[length]; + random.NextBytes(bytes); + return bytes; + } + + public byte[] GetPadding() + { + return Arrays.Clone(padding); + } + + public override void Encode(BcpgOutputStream bcpgOut) + { + bcpgOut.WritePacket(PacketTag.Padding, padding); + } + } +} \ No newline at end of file diff --git a/crypto/src/openpgp/PgpObjectFactory.cs b/crypto/src/openpgp/PgpObjectFactory.cs index 068b85154..3dabd5e5a 100644 --- a/crypto/src/openpgp/PgpObjectFactory.cs +++ b/crypto/src/openpgp/PgpObjectFactory.cs @@ -101,6 +101,8 @@ public PgpObject NextPgpObject() } case PacketTag.Marker: return new PgpMarker(bcpgIn); + case PacketTag.Padding: + return new PgpPadding(bcpgIn); case PacketTag.Experimental1: case PacketTag.Experimental2: case PacketTag.Experimental3: diff --git a/crypto/src/openpgp/PgpPadding.cs b/crypto/src/openpgp/PgpPadding.cs new file mode 100644 index 000000000..0fd601929 --- /dev/null +++ b/crypto/src/openpgp/PgpPadding.cs @@ -0,0 +1,21 @@ +using System.IO; + +namespace Org.BouncyCastle.Bcpg.OpenPgp +{ + public class PgpPadding + : PgpObject + { + private readonly PaddingPacket data; + + public PgpPadding(BcpgInputStream bcpgInput) + { + Packet packet = bcpgInput.ReadPacket(); + if (!(packet is PaddingPacket paddingPacket)) + throw new IOException("unexpected packet in stream: " + packet); + + data = paddingPacket; + } + + public byte[] GetPadding() => data.GetPadding(); + } +} \ No newline at end of file diff --git a/crypto/test/src/openpgp/test/IgnoreMarkerPacketInCertificatesTest.cs b/crypto/test/src/openpgp/test/IgnoreMarkerPacketInCertificatesTest.cs index 71196edae..de1ce1652 100644 --- a/crypto/test/src/openpgp/test/IgnoreMarkerPacketInCertificatesTest.cs +++ b/crypto/test/src/openpgp/test/IgnoreMarkerPacketInCertificatesTest.cs @@ -58,15 +58,66 @@ public class IgnoreMarkerPacketInCertificatesTest "=6mfA\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - public override string Name + // [PUBLIC KEY, MARKER, PADDING, USER-ID, SIGNATURE, SUBKEY, SIGNATURE] + private static readonly string CERT_WITH_MARKER_AND_PADDING = "" + + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAcoDUEdQ1QQo9HIzzSFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + + "ZW5wZ3AuZXhhbXBsZT7CwQ4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikbO\n" + + "wM0EXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + + "IRDMXDOPyzEfjwARAQABwsD2BBgBCgAgFiEE0aZuGiOxgsmYD3iM+/zIKgFeczAF\n" + + "Al2lnPICGwwACgkQ+/zIKgFeczDp/wv/boLfh2SMF99PMyPkF3Obwy0Xrs5id4nh\n" + + "NAzDv7jUgvitVxIqEiGT/dR3mSdpG0/Z5/X7kXrqH39E9A4nn628HCEEBxRZK6kq\n" + + "dSt1VplBqdia1LFxVXY8v35ASI03e3OW6FpY7/+sALEn4r9ldCUjPBBVOk2F8bMB\n" + + "oxVX3Ol/e7STXiK1y/pqUpjz6stm87XAgh5FkuZTS1kMPke1YO9RXusgUjVa6gtv\n" + + "4pmBtifc5aMI8dV1Ot1nYKqdlsbdJfDprAf1vNEtX0ReRuEgx7PR14JV16j7AUWS\n" + + "WHz/lUrZvS+T/7CownF+lrWUe8kuhvM4/1++uzCyv3YwDb6T3TVZ4hJHuoTNwjQV\n" + + "2DwDIUATFoQrpXKM/tJcYvC9+KzDfg7G5mveqbHVK5+7i2gfdesHtAk3xfKqpuwb\n" + + "FQIGpaJ/1FIrjGPNFN7nqI96JIkk4hyIw/2LaV0j4qAvJzJ4O8agGPQcIs7eBVoF\n" + + "7i5tWuPkqOFfY9U0Ql3ddlHNpdkTZoAx\n" + + "=kgFK\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + + public override string Name { get { return "IgnoreMarkerPacketInCertificatesTest"; } } public override void PerformTest() + { + PerformTestMarker(CERT_WITH_MARKER); + PerformTestMarker(CERT_WITH_MARKER_AND_PADDING); + } + + private void PerformTestMarker(string cert) { ArmoredInputStream armorIn = new ArmoredInputStream( - new MemoryStream(Encoding.UTF8.GetBytes(CERT_WITH_MARKER), false)); + new MemoryStream(Encoding.UTF8.GetBytes(cert), false)); PgpObjectFactory objectFactory = new PgpObjectFactory(armorIn); PgpPublicKeyRing certificate = (PgpPublicKeyRing)objectFactory.NextPgpObject(); @@ -86,7 +137,7 @@ public override void PerformTest() IsEquals(1, Count(signatures)); } - [Test] + [Test] public void TestFunction() { string resultText = Perform().ToString(); diff --git a/crypto/test/src/openpgp/test/PaddingPacketTest.cs b/crypto/test/src/openpgp/test/PaddingPacketTest.cs new file mode 100644 index 000000000..8c607d7b9 --- /dev/null +++ b/crypto/test/src/openpgp/test/PaddingPacketTest.cs @@ -0,0 +1,142 @@ +using NUnit.Framework; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.Utilities.Test; +using System.IO; + +namespace Org.BouncyCastle.Bcpg.OpenPgp.Tests +{ + [TestFixture] + public class PaddingPacketTest + : SimpleTest + { + public override string Name => "PaddingPacketTest"; + + [Test] + public void PaddingPacketReadFromStreamTest() + { + /* + * Simple padding packet + * + * 0xD5 Packet tag (0xC0 | 0x15) + * 0x04 Length + * 0x01 0x02 0x03 0x04 Padding content + */ + byte[] packet = Hex.Decode("D50401020304"); + byte[] expected = Hex.Decode("01020304"); + + using (Stream input = new MemoryStream(packet)) + { + PgpObjectFactory objectFactory = new PgpObjectFactory(input); + PgpObject obj = objectFactory.NextPgpObject(); + + IsTrue(obj is PgpPadding); + + byte[] padding = (obj as PgpPadding).GetPadding(); + + IsEquals($"unexpected padding length: expected {expected.Length} got {padding.Length}", padding.Length, expected.Length); + FailIf($"unexpected padding", !AreEqual(padding, expected)); + } + } + + + [Test] + public void PaddingPacketReadThreePacketsFromStreamTest() + { + byte[] packet = Hex.Decode("D50401020304D503556677D503AABBCC"); + byte[][] expected = new byte[][] { + Hex.Decode("01020304"), + Hex.Decode("556677"), + Hex.Decode("AABBCC") + }; + + using (Stream input = new MemoryStream(packet)) + { + PgpObjectFactory objectFactory = new PgpObjectFactory(input); + + int i = 0; + PgpObject obj; + while ((obj = objectFactory.NextPgpObject()) != null) + { + IsTrue(obj is PgpPadding); + byte[] padding = (obj as PgpPadding).GetPadding(); + + IsEquals($"unexpected padding length: expected {expected[i].Length} got {padding.Length}", padding.Length, expected[i].Length); + FailIf($"unexpected padding", !AreEqual(padding, expected[i])); + ++i; + } + + IsEquals(i, 3); + } + } + + [Test] + public void PaddingPacketEncodeTest() + { + byte[] encoded = Hex.Decode("D50401020304"); + + byte[] padding = Hex.Decode("01020304"); + PaddingPacket packet = new PaddingPacket(padding); + + using (MemoryStream output = new MemoryStream()) + { + BcpgOutputStream bcOut = new BcpgOutputStream(output); + packet.Encode(bcOut); + bcOut.Close(); + + FailIf("wrong encoding", !AreEqual(output.ToArray(), encoded)); + } + } + + [Test] + public void PaddingPacketEncodeThenDecodeTest() + { + SecureRandom random = new SecureRandom(); + PaddingPacket packet = new PaddingPacket(32, random); + + using (MemoryStream output = new MemoryStream()) + { + BcpgOutputStream bcOut = new BcpgOutputStream(output); + packet.Encode(bcOut); + bcOut.Close(); + + using (Stream input = new MemoryStream(output.ToArray())) + { + PgpObjectFactory factory = new PgpObjectFactory(input); + + PgpPadding padding = (PgpPadding)factory.NextPgpObject(); + IsTrue(Arrays.AreEqual(packet.GetPadding(), padding.GetPadding())); + } + } + } + + [Test] + public void KnownPaddingBytesTest() + { + byte[] known = Strings.ToByteArray("thisIsKnownPadding"); + PaddingPacket packet = new PaddingPacket(known); + IsTrue(Arrays.AreEqual(known, packet.GetPadding())); + } + + [Test] + public void Random50BytesTest() + { + int len = 50; + SecureRandom random = new SecureRandom(); + PaddingPacket packet = new PaddingPacket(len, random); + IsEquals(len, packet.GetPadding().Length); + } + + + public override void PerformTest() + { + PaddingPacketReadFromStreamTest(); + PaddingPacketReadThreePacketsFromStreamTest(); + PaddingPacketEncodeTest(); + PaddingPacketEncodeThenDecodeTest(); + KnownPaddingBytesTest(); + Random50BytesTest(); + } + } +} \ No newline at end of file From 091b92ed8db447dfe11e2807e3d0c3e3d9821e7a Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Tue, 13 Feb 2024 19:23:37 +0100 Subject: [PATCH 04/37] Initial support for Version6 keys and signatures - Ed25519Legacy with v4 keys/signatures - classes for "native" (octet array) key material - parsing v5/v6 packets (public key, unlocked secret key, signature) - basic verification of v6 signatures - test vectors from crypto-refresh draft --- crypto/src/bcpg/Ed25519PublicBCPGKey.cs | 19 ++ crypto/src/bcpg/Ed25519SecretBCPGKey.cs | 19 ++ crypto/src/bcpg/Ed448PublicBCPGKey.cs | 19 ++ crypto/src/bcpg/Ed448SecretBCPGKey.cs | 19 ++ crypto/src/bcpg/OctetArrayBCPGKey.cs | 48 +++ crypto/src/bcpg/PublicKeyAlgorithmTags.cs | 2 +- crypto/src/bcpg/PublicKeyPacket.cs | 50 +++- crypto/src/bcpg/SecretKeyPacket.cs | 38 ++- crypto/src/bcpg/SignaturePacket.cs | 147 ++++++++-- crypto/src/bcpg/X25519PublicBCPGKey.cs | 19 ++ crypto/src/bcpg/X25519SecretBCPGKey.cs | 19 ++ crypto/src/bcpg/X448PublicBCPGKey.cs | 19 ++ crypto/src/bcpg/X448SecretBCPGKey.cs | 19 ++ crypto/src/openpgp/PgpPublicKey.cs | 74 ++++- crypto/src/openpgp/PgpSecretKey.cs | 24 ++ crypto/src/openpgp/PgpSignature.cs | 19 +- crypto/src/openpgp/PgpUtilities.cs | 2 + .../src/openpgp/test/PgpCryptoRefreshTest.cs | 273 ++++++++++++++++++ 18 files changed, 761 insertions(+), 68 deletions(-) create mode 100644 crypto/src/bcpg/Ed25519PublicBCPGKey.cs create mode 100644 crypto/src/bcpg/Ed25519SecretBCPGKey.cs create mode 100644 crypto/src/bcpg/Ed448PublicBCPGKey.cs create mode 100644 crypto/src/bcpg/Ed448SecretBCPGKey.cs create mode 100644 crypto/src/bcpg/OctetArrayBCPGKey.cs create mode 100644 crypto/src/bcpg/X25519PublicBCPGKey.cs create mode 100644 crypto/src/bcpg/X25519SecretBCPGKey.cs create mode 100644 crypto/src/bcpg/X448PublicBCPGKey.cs create mode 100644 crypto/src/bcpg/X448SecretBCPGKey.cs create mode 100644 crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs diff --git a/crypto/src/bcpg/Ed25519PublicBCPGKey.cs b/crypto/src/bcpg/Ed25519PublicBCPGKey.cs new file mode 100644 index 000000000..802657909 --- /dev/null +++ b/crypto/src/bcpg/Ed25519PublicBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class Ed25519PublicBcpgKey + : OctetArrayBcpgKey + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-ed2 + public const int length = 32; + + public Ed25519PublicBcpgKey(BcpgInputStream bcpgIn) + : base(length, bcpgIn) + { + } + + public Ed25519PublicBcpgKey(byte[] key) + :base(length, key) + { + } + } +} \ No newline at end of file diff --git a/crypto/src/bcpg/Ed25519SecretBCPGKey.cs b/crypto/src/bcpg/Ed25519SecretBCPGKey.cs new file mode 100644 index 000000000..e161707e9 --- /dev/null +++ b/crypto/src/bcpg/Ed25519SecretBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class Ed25519SecretBcpgKey + : OctetArrayBcpgKey + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-ed2 + public const int length = 32; + + public Ed25519SecretBcpgKey(BcpgInputStream bcpgIn) + : base(length, bcpgIn) + { + } + + public Ed25519SecretBcpgKey(byte[] key) + : base(length, key) + { + } + } +} \ No newline at end of file diff --git a/crypto/src/bcpg/Ed448PublicBCPGKey.cs b/crypto/src/bcpg/Ed448PublicBCPGKey.cs new file mode 100644 index 000000000..fc0283969 --- /dev/null +++ b/crypto/src/bcpg/Ed448PublicBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class Ed448PublicBcpgKey + : OctetArrayBcpgKey + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-ed4 + public const int length = 57; + + public Ed448PublicBcpgKey(BcpgInputStream bcpgIn) + : base(length, bcpgIn) + { + } + + public Ed448PublicBcpgKey(byte[] key) + : base(length, key) + { + } + } +} diff --git a/crypto/src/bcpg/Ed448SecretBCPGKey.cs b/crypto/src/bcpg/Ed448SecretBCPGKey.cs new file mode 100644 index 000000000..3b355ceb9 --- /dev/null +++ b/crypto/src/bcpg/Ed448SecretBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class Ed448SecretBcpgKey + : OctetArrayBcpgKey + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-ed4 + public const int length = 57; + + public Ed448SecretBcpgKey(BcpgInputStream bcpgIn) + : base(length, bcpgIn) + { + } + + public Ed448SecretBcpgKey(byte[] key) + : base(length, key) + { + } + } +} diff --git a/crypto/src/bcpg/OctetArrayBCPGKey.cs b/crypto/src/bcpg/OctetArrayBCPGKey.cs new file mode 100644 index 000000000..1bf09958e --- /dev/null +++ b/crypto/src/bcpg/OctetArrayBCPGKey.cs @@ -0,0 +1,48 @@ +using Org.BouncyCastle.Utilities; +using System; + +namespace Org.BouncyCastle.Bcpg +{ + /** + * Public/Secret BcpgKey which is encoded as an array of octets rather than an MPI + * + */ + public abstract class OctetArrayBcpgKey + : BcpgObject, IBcpgKey + { + private readonly byte[] key; + + protected OctetArrayBcpgKey(int length, BcpgInputStream bcpgIn) + { + key = new byte[length]; + bcpgIn.ReadFully(key); + } + + protected OctetArrayBcpgKey(int length, byte[] key) + { + if (key.Length != length) + { + throw new ArgumentException("unexpected key encoding length: expected " + length + " bytes, got " + key.Length); + } + + this.key = Arrays.Clone(key); + } + + /// + public string Format + { + get { return "PGP"; } + } + + /// + public override void Encode(BcpgOutputStream bcpgOut) + { + bcpgOut.Write(key); + } + + public byte[] GetKey() + { + return Arrays.Clone(key); + } + } +} \ No newline at end of file diff --git a/crypto/src/bcpg/PublicKeyAlgorithmTags.cs b/crypto/src/bcpg/PublicKeyAlgorithmTags.cs index 7fa4d728a..a88a84c0d 100644 --- a/crypto/src/bcpg/PublicKeyAlgorithmTags.cs +++ b/crypto/src/bcpg/PublicKeyAlgorithmTags.cs @@ -21,7 +21,7 @@ public enum PublicKeyAlgorithmTag EdDsa = 22, // EdDSA - (internet draft, but appearing in use) EdDsa_Legacy = 22, // new name for old EdDSA tag. - // defined as Reserver by crypto-refresh draft + // defined as Reserved by crypto-refresh draft AEDH = 23, AEDSA = 24, diff --git a/crypto/src/bcpg/PublicKeyPacket.cs b/crypto/src/bcpg/PublicKeyPacket.cs index 639d5595c..26cc069d9 100644 --- a/crypto/src/bcpg/PublicKeyPacket.cs +++ b/crypto/src/bcpg/PublicKeyPacket.cs @@ -9,11 +9,19 @@ namespace Org.BouncyCastle.Bcpg public class PublicKeyPacket : ContainedPacket //, PublicKeyAlgorithmTag { - private int version; - private long time; - private int validDays; - private PublicKeyAlgorithmTag algorithm; - private IBcpgKey key; + public const int Version2 = 2; + public const int Version3 = 3; + public const int Version4 = 4; + public const int Version5 = 5; + public const int Version6 = 6; + + private readonly int version; + private readonly long time; + private readonly int validDays; + private readonly PublicKeyAlgorithmTag algorithm; + private readonly IBcpgKey key; + + private readonly long v6KeyLen; internal PublicKeyPacket( BcpgInputStream bcpgIn) @@ -23,13 +31,19 @@ internal PublicKeyPacket( time = ((uint)bcpgIn.ReadByte() << 24) | ((uint)bcpgIn.ReadByte() << 16) | ((uint)bcpgIn.ReadByte() << 8) | (uint)bcpgIn.ReadByte(); - if (version <= 3) + if (version <= Version3) { validDays = (bcpgIn.ReadByte() << 8) | bcpgIn.ReadByte(); } algorithm = (PublicKeyAlgorithmTag)bcpgIn.ReadByte(); + if (version == Version5 || version == Version6) + { + v6KeyLen = ((uint)bcpgIn.ReadByte() << 24) | ((uint)bcpgIn.ReadByte() << 16) + | ((uint)bcpgIn.ReadByte() << 8) | (uint)bcpgIn.ReadByte(); + } + switch (algorithm) { case PublicKeyAlgorithmTag.RsaEncrypt: @@ -53,8 +67,20 @@ internal PublicKeyPacket( case PublicKeyAlgorithmTag.EdDsa_Legacy: key = new EdDsaPublicBcpgKey(bcpgIn); break; + case PublicKeyAlgorithmTag.Ed25519: + key = new Ed25519PublicBcpgKey(bcpgIn); + break; + case PublicKeyAlgorithmTag.Ed448: + key = new Ed448PublicBcpgKey(bcpgIn); + break; + case PublicKeyAlgorithmTag.X25519: + key = new X25519PublicBcpgKey(bcpgIn); + break; + case PublicKeyAlgorithmTag.X448: + key = new X448PublicBcpgKey(bcpgIn); + break; default: - throw new IOException("unknown PGP public key algorithm encountered"); + throw new IOException("unknown PGP public key algorithm encountered"); } } @@ -64,7 +90,7 @@ public PublicKeyPacket( DateTime time, IBcpgKey key) { - this.version = 4; + this.version = Version4; this.time = DateTimeUtilities.DateTimeToUnixMs(time) / 1000L; this.algorithm = algorithm; this.key = key; @@ -103,12 +129,18 @@ public virtual byte[] GetEncodedContents() pOut.WriteByte((byte)version); pOut.WriteInt((int)time); - if (version <= 3) + if (version <= Version3) { pOut.WriteShort((short)validDays); } pOut.WriteByte((byte)algorithm); + + if (version == Version5 || version == Version6) + { + pOut.WriteInt((int)v6KeyLen); + } + pOut.WriteObject((BcpgObject)key); } return bOut.ToArray(); diff --git a/crypto/src/bcpg/SecretKeyPacket.cs b/crypto/src/bcpg/SecretKeyPacket.cs index 15b89b404..c5b7e8126 100644 --- a/crypto/src/bcpg/SecretKeyPacket.cs +++ b/crypto/src/bcpg/SecretKeyPacket.cs @@ -1,5 +1,5 @@ +using System; using System.IO; - using Org.BouncyCastle.Utilities; namespace Org.BouncyCastle.Bcpg @@ -11,15 +11,18 @@ public class SecretKeyPacket public const int UsageNone = 0x00; public const int UsageChecksum = 0xff; public const int UsageSha1 = 0xfe; + public const int UsageAead = 0xfd; + - private PublicKeyPacket pubKeyPacket; + private readonly PublicKeyPacket pubKeyPacket; private readonly byte[] secKeyData; - private int s2kUsage; - private SymmetricKeyAlgorithmTag encAlgorithm; - private S2k s2k; - private byte[] iv; + private readonly int s2kUsage; + private readonly SymmetricKeyAlgorithmTag encAlgorithm; + private readonly S2k s2k; + private readonly byte[] iv; + private readonly AeadAlgorithmTag aeadAlgo; - internal SecretKeyPacket( + internal SecretKeyPacket( BcpgInputStream bcpgIn) { if (this is SecretSubkeyPacket) @@ -33,17 +36,32 @@ internal SecretKeyPacket( s2kUsage = bcpgIn.ReadByte(); - if (s2kUsage == UsageChecksum || s2kUsage == UsageSha1) + if (s2kUsage != UsageNone && pubKeyPacket.Version == PublicKeyPacket.Version6) + { + // TODO: Use length to parse unknown parameters + int conditionalParameterLength = bcpgIn.ReadByte(); + } + + if (s2kUsage == UsageChecksum || s2kUsage == UsageSha1 || s2kUsage == UsageAead) { encAlgorithm = (SymmetricKeyAlgorithmTag) bcpgIn.ReadByte(); + if (s2kUsage == UsageAead) + { + aeadAlgo = (AeadAlgorithmTag)bcpgIn.ReadByte(); + } + if (pubKeyPacket.Version == 6 && (s2kUsage == UsageSha1 || s2kUsage == UsageAead)) + { + // TODO: Use length to parse unknown S2Ks + int s2kLen = bcpgIn.ReadByte(); + } s2k = new S2k(bcpgIn); } else { encAlgorithm = (SymmetricKeyAlgorithmTag) s2kUsage; - } + } - if (!(s2k != null && s2k.Type == S2k.GnuDummyS2K && s2k.ProtectionMode == 0x01)) + if (!(s2k != null && s2k.Type == S2k.GnuDummyS2K && s2k.ProtectionMode == 0x01)) { if (s2kUsage != 0) { diff --git a/crypto/src/bcpg/SignaturePacket.cs b/crypto/src/bcpg/SignaturePacket.cs index ff3e5d3f9..79dc8cebe 100644 --- a/crypto/src/bcpg/SignaturePacket.cs +++ b/crypto/src/bcpg/SignaturePacket.cs @@ -14,25 +14,33 @@ namespace Org.BouncyCastle.Bcpg public class SignaturePacket : ContainedPacket { - private int version; - private int signatureType; - private long creationTime; - private long keyId; - private PublicKeyAlgorithmTag keyAlgorithm; - private HashAlgorithmTag hashAlgorithm; - private MPInteger[] signature; - private byte[] fingerprint; - private SignatureSubpacket[] hashedData; - private SignatureSubpacket[] unhashedData; - private byte[] signatureEncoding; - - internal SignaturePacket(BcpgInputStream bcpgIn) + public const int Version2 = 2; + public const int Version3 = 3; + public const int Version4 = 4; + public const int Version5 = 5; + public const int Version6 = 6; + + private readonly int version; + private readonly int signatureType; + private long creationTime; + private readonly long keyId; + private readonly PublicKeyAlgorithmTag keyAlgorithm; + private readonly HashAlgorithmTag hashAlgorithm; + private readonly MPInteger[] signature; + private readonly byte[] fingerprint; + private readonly SignatureSubpacket[] hashedData; + private readonly SignatureSubpacket[] unhashedData; + private readonly byte[] signatureEncoding; + + // fields for v6 signatures + private readonly byte[] salt; + + internal SignaturePacket(BcpgInputStream bcpgIn) { version = bcpgIn.ReadByte(); - if (version == 3 || version == 2) + if (version == Version2 || version == Version3) { -// int l = bcpgIn.ReadByte(); signatureType = bcpgIn.ReadByte(); @@ -51,13 +59,26 @@ internal SignaturePacket(BcpgInputStream bcpgIn) keyAlgorithm = (PublicKeyAlgorithmTag) bcpgIn.ReadByte(); hashAlgorithm = (HashAlgorithmTag) bcpgIn.ReadByte(); } - else if (version == 4) + else if (version >= Version4 && version <= Version6) { signatureType = bcpgIn.ReadByte(); keyAlgorithm = (PublicKeyAlgorithmTag) bcpgIn.ReadByte(); hashAlgorithm = (HashAlgorithmTag) bcpgIn.ReadByte(); - int hashedLength = (bcpgIn.ReadByte() << 8) | bcpgIn.ReadByte(); + int hashedLength; + + if (version == Version6) + { + hashedLength = (bcpgIn.ReadByte() << 24) + | (bcpgIn.ReadByte() << 16) + | (bcpgIn.ReadByte() << 8) + | bcpgIn.ReadByte(); + } + else + { + hashedLength = (bcpgIn.ReadByte() << 8) + | bcpgIn.ReadByte(); + } byte[] hashed = new byte[hashedLength]; bcpgIn.ReadFully(hashed); @@ -90,7 +111,21 @@ internal SignaturePacket(BcpgInputStream bcpgIn) } } - int unhashedLength = (bcpgIn.ReadByte() << 8) | bcpgIn.ReadByte(); + int unhashedLength; + + if (version == Version6) + { + unhashedLength = (bcpgIn.ReadByte() << 24) + | (bcpgIn.ReadByte() << 16) + | (bcpgIn.ReadByte() << 8) + | bcpgIn.ReadByte(); + } + else + { + unhashedLength = (bcpgIn.ReadByte() << 8) + | bcpgIn.ReadByte(); + } + byte[] unhashed = new byte[unhashedLength]; bcpgIn.ReadFully(unhashed); @@ -124,7 +159,14 @@ internal SignaturePacket(BcpgInputStream bcpgIn) fingerprint = new byte[2]; bcpgIn.ReadFully(fingerprint); - switch (keyAlgorithm) + if (version == Version6) + { + int saltSize = bcpgIn.ReadByte(); + salt = new byte[saltSize]; + bcpgIn.ReadFully(salt); + } + + switch (keyAlgorithm) { case PublicKeyAlgorithmTag.RsaGeneral: case PublicKeyAlgorithmTag.RsaSign: @@ -149,6 +191,20 @@ internal SignaturePacket(BcpgInputStream bcpgIn) MPInteger ecS = new MPInteger(bcpgIn); signature = new MPInteger[2]{ ecR, ecS }; break; + + case PublicKeyAlgorithmTag.Ed25519: + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for-ed2 + signature = null; + signatureEncoding = new byte[64]; + bcpgIn.ReadFully(signatureEncoding); + break; + case PublicKeyAlgorithmTag.Ed448: + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for-ed4 + signature = null; + signatureEncoding = new byte[114]; + bcpgIn.ReadFully(signatureEncoding); + break; + default: if (keyAlgorithm < PublicKeyAlgorithmTag.Experimental_1 || keyAlgorithm > PublicKeyAlgorithmTag.Experimental_11) throw new IOException("unknown signature key algorithm: " + keyAlgorithm); @@ -179,7 +235,7 @@ public SignaturePacket( SignatureSubpacket[] unhashedData, byte[] fingerprint, MPInteger[] signature) - : this(4, signatureType, keyId, keyAlgorithm, hashAlgorithm, hashedData, unhashedData, fingerprint, signature) + : this(Version4, signatureType, keyId, keyAlgorithm, hashAlgorithm, hashedData, unhashedData, fingerprint, signature) { } @@ -260,7 +316,7 @@ public byte[] GetFingerprint() */ public byte[] GetSignatureTrailer() { - if (version == 3) + if (version == Version3) { long time = creationTime / 1000L; @@ -277,8 +333,14 @@ public byte[] GetSignatureTrailer() sOut.WriteByte((byte)KeyAlgorithm); sOut.WriteByte((byte)HashAlgorithm); - // Mark position an reserve two bytes for length + // Mark position an reserve two bytes (version4) or four bytes (version6) + // for length long lengthPosition = sOut.Position; + if (version == Version6) + { + sOut.WriteByte(0x00); + sOut.WriteByte(0x00); + } sOut.WriteByte(0x00); sOut.WriteByte(0x00); @@ -289,6 +351,11 @@ public byte[] GetSignatureTrailer() } ushort dataLength = Convert.ToUInt16(sOut.Position - lengthPosition - 2); + if (version == Version6) + { + dataLength -= 2; + } + uint hDataLength = Convert.ToUInt32(sOut.Position); sOut.WriteByte((byte)Version); @@ -300,6 +367,11 @@ public byte[] GetSignatureTrailer() // Reset position and fill in length sOut.Position = lengthPosition; + if (version == Version6) + { + sOut.WriteByte((byte)(dataLength >> 24)); + sOut.WriteByte((byte)(dataLength >> 16)); + } sOut.WriteByte((byte)(dataLength >> 8)); sOut.WriteByte((byte)(dataLength )); @@ -316,6 +388,11 @@ public byte[] GetSignatureTrailer() */ public MPInteger[] GetSignature() => signature; + public byte[] GetSignatureSalt() + { + return Arrays.Clone(salt); + } + /** * Return the byte encoding of the signature section. * @return uninterpreted signature bytes. @@ -323,7 +400,7 @@ public byte[] GetSignatureTrailer() public byte[] GetSignatureBytes() { if (signatureEncoding != null) - return (byte[])signatureEncoding.Clone(); + return Arrays.Clone(signatureEncoding); MemoryStream bOut = new MemoryStream(); @@ -359,7 +436,7 @@ public override void Encode(BcpgOutputStream bcpgOut) { pOut.WriteByte((byte)version); - if (version == 3 || version == 2) + if (version == Version3 || version == Version2) { byte nextBlockLength = 5; pOut.Write(nextBlockLength, (byte)signatureType); @@ -367,11 +444,11 @@ public override void Encode(BcpgOutputStream bcpgOut) pOut.WriteLong(keyId); pOut.Write((byte)keyAlgorithm, (byte)hashAlgorithm); } - else if (version == 4) + else if (version >= Version4 && version <= Version6) { pOut.Write((byte)signatureType, (byte)keyAlgorithm, (byte)hashAlgorithm); - EncodeLengthAndData(pOut, GetEncodedSubpackets(hashedData)); - EncodeLengthAndData(pOut, GetEncodedSubpackets(unhashedData)); + EncodeLengthAndData(version, pOut, GetEncodedSubpackets(hashedData)); + EncodeLengthAndData(version, pOut, GetEncodedSubpackets(unhashedData)); } else { @@ -380,6 +457,12 @@ public override void Encode(BcpgOutputStream bcpgOut) pOut.Write(fingerprint); + if (version == Version6) + { + pOut.WriteByte((byte)salt.Length); + pOut.Write(salt); + } + if (signature != null) { pOut.WriteObjects(signature); @@ -394,10 +477,18 @@ public override void Encode(BcpgOutputStream bcpgOut) } private static void EncodeLengthAndData( + int version, BcpgOutputStream pOut, byte[] data) { - pOut.WriteShort((short) data.Length); + if (version == Version6) + { + pOut.WriteInt(data.Length); + } + else + { + pOut.WriteShort((short)data.Length); + } pOut.Write(data); } diff --git a/crypto/src/bcpg/X25519PublicBCPGKey.cs b/crypto/src/bcpg/X25519PublicBCPGKey.cs new file mode 100644 index 000000000..b8eb52a4c --- /dev/null +++ b/crypto/src/bcpg/X25519PublicBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class X25519PublicBcpgKey + : OctetArrayBcpgKey + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-x + public const int length = 32; + + public X25519PublicBcpgKey(BcpgInputStream bcpgIn) + : base(length, bcpgIn) + { + } + + public X25519PublicBcpgKey(byte[] key) + : base(length, key) + { + } + } +} \ No newline at end of file diff --git a/crypto/src/bcpg/X25519SecretBCPGKey.cs b/crypto/src/bcpg/X25519SecretBCPGKey.cs new file mode 100644 index 000000000..3da6f01b6 --- /dev/null +++ b/crypto/src/bcpg/X25519SecretBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class X25519SecretBcpgKey + : OctetArrayBcpgKey + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-x + public const int length = 32; + + public X25519SecretBcpgKey(BcpgInputStream bcpgIn) + : base(length, bcpgIn) + { + } + + public X25519SecretBcpgKey(byte[] key) + : base(length, key) + { + } + } +} \ No newline at end of file diff --git a/crypto/src/bcpg/X448PublicBCPGKey.cs b/crypto/src/bcpg/X448PublicBCPGKey.cs new file mode 100644 index 000000000..9b35cbd3b --- /dev/null +++ b/crypto/src/bcpg/X448PublicBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class X448PublicBcpgKey + : OctetArrayBcpgKey + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-x4 + public const int length = 56; + + public X448PublicBcpgKey(BcpgInputStream bcpgIn) + : base(length, bcpgIn) + { + } + + public X448PublicBcpgKey(byte[] key) + : base(length, key) + { + } + } +} \ No newline at end of file diff --git a/crypto/src/bcpg/X448SecretBCPGKey.cs b/crypto/src/bcpg/X448SecretBCPGKey.cs new file mode 100644 index 000000000..a8ce5e809 --- /dev/null +++ b/crypto/src/bcpg/X448SecretBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class X448SecretBcpgKey + : OctetArrayBcpgKey + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-x4 + public const int length = 56; + + public X448SecretBcpgKey(BcpgInputStream bcpgIn) + : base(length, bcpgIn) + { + } + + public X448SecretBcpgKey(byte[] key) + : base(length, key) + { + } + } +} \ No newline at end of file diff --git a/crypto/src/openpgp/PgpPublicKey.cs b/crypto/src/openpgp/PgpPublicKey.cs index fa924ff37..06f9e2277 100644 --- a/crypto/src/openpgp/PgpPublicKey.cs +++ b/crypto/src/openpgp/PgpPublicKey.cs @@ -11,6 +11,7 @@ using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Utilities; using Org.BouncyCastle.Math; using Org.BouncyCastle.Math.EC; using Org.BouncyCastle.Math.EC.Rfc7748; @@ -25,6 +26,10 @@ namespace Org.BouncyCastle.Bcpg.OpenPgp public class PgpPublicKey : PgpObject { + private const byte v4FingerprintPreamble = 0x99; + private const byte v5FingerprintPreamble = 0x9A; + private const byte v6FingerprintPreamble = 0x9B; + // We default to these as they are specified as mandatory in RFC 6631. private static readonly PgpKdfParameters DefaultKdfParameters = new PgpKdfParameters(HashAlgorithmTag.Sha256, SymmetricKeyAlgorithmTag.Aes128); @@ -34,7 +39,7 @@ public static byte[] CalculateFingerprint(PublicKeyPacket publicPk) IBcpgKey key = publicPk.Key; IDigest digest; - if (publicPk.Version <= 3) + if (publicPk.Version <= PublicKeyPacket.Version3) { RsaPublicBcpgKey rK = (RsaPublicBcpgKey)key; @@ -56,11 +61,29 @@ public static byte[] CalculateFingerprint(PublicKeyPacket publicPk) { byte[] kBytes = publicPk.GetEncodedContents(); - digest = PgpUtilities.CreateDigest(HashAlgorithmTag.Sha1); + if (publicPk.Version == PublicKeyPacket.Version4) + { + digest = PgpUtilities.CreateDigest(HashAlgorithmTag.Sha1); + + digest.Update(v4FingerprintPreamble); + digest.Update((byte)(kBytes.Length >> 8)); + digest.Update((byte)kBytes.Length); + } + else if (publicPk.Version == PublicKeyPacket.Version5 || publicPk.Version == PublicKeyPacket.Version6) + { + digest = PgpUtilities.CreateDigest(HashAlgorithmTag.Sha256); + + digest.Update(publicPk.Version == PublicKeyPacket.Version5 ? v5FingerprintPreamble : v6FingerprintPreamble); + digest.Update((byte)(kBytes.Length >> 24)); + digest.Update((byte)(kBytes.Length >> 16)); + digest.Update((byte)(kBytes.Length >> 8)); + digest.Update((byte)kBytes.Length); + } + else + { + throw new PgpException("unsupported OpenPGP key packet version: " + publicPk.Version); + } - digest.Update(0x99); - digest.Update((byte)(kBytes.Length >> 8)); - digest.Update((byte)kBytes.Length); digest.BlockUpdate(kBytes, 0, kBytes.Length); } catch (Exception e) @@ -106,7 +129,7 @@ private void Init() this.fingerprint = CalculateFingerprint(publicPk); - if (publicPk.Version <= 3) + if (publicPk.Version <= PublicKeyPacket.Version3) { RsaPublicBcpgKey rK = (RsaPublicBcpgKey) key; @@ -115,14 +138,14 @@ private void Init() } else { - this.keyId = (long)(((ulong)fingerprint[fingerprint.Length - 8] << 56) - | ((ulong)fingerprint[fingerprint.Length - 7] << 48) - | ((ulong)fingerprint[fingerprint.Length - 6] << 40) - | ((ulong)fingerprint[fingerprint.Length - 5] << 32) - | ((ulong)fingerprint[fingerprint.Length - 4] << 24) - | ((ulong)fingerprint[fingerprint.Length - 3] << 16) - | ((ulong)fingerprint[fingerprint.Length - 2] << 8) - | (ulong)fingerprint[fingerprint.Length - 1]); + if (publicPk.Version == PublicKeyPacket.Version4) + { + this.keyId = (long)Pack.BE_To_UInt64(fingerprint, fingerprint.Length - 8); + } + else + { + this.keyId = (long)Pack.BE_To_UInt64(fingerprint); + } if (key is RsaPublicBcpgKey) { @@ -408,7 +431,7 @@ public byte[] GetTrustData() /// The number of valid seconds from creation time - zero means no expiry. public long GetValidSeconds() { - if (publicPk.Version <= 3) + if (publicPk.Version <= PublicKeyPacket.Version3) { return (long)publicPk.ValidDays * (24 * 60 * 60); } @@ -511,6 +534,8 @@ public bool IsEncryptionKey case PublicKeyAlgorithmTag.ElGamalGeneral: case PublicKeyAlgorithmTag.RsaEncrypt: case PublicKeyAlgorithmTag.RsaGeneral: + case PublicKeyAlgorithmTag.X25519: + case PublicKeyAlgorithmTag.X448: return true; default: return false; @@ -643,8 +668,25 @@ public AsymmetricKeyParameter GetKey() case PublicKeyAlgorithmTag.ElGamalGeneral: ElGamalPublicBcpgKey elK = (ElGamalPublicBcpgKey)publicPk.Key; return new ElGamalPublicKeyParameters(elK.Y, new ElGamalParameters(elK.P, elK.G)); + + case PublicKeyAlgorithmTag.Ed25519: + Ed25519PublicBcpgKey ed25519key = (Ed25519PublicBcpgKey)publicPk.Key; + return new Ed25519PublicKeyParameters(ed25519key.GetKey()); + + case PublicKeyAlgorithmTag.X25519: + X25519PublicBcpgKey x25519key = (X25519PublicBcpgKey)publicPk.Key; + return new X25519PublicKeyParameters(x25519key.GetKey()); + + case PublicKeyAlgorithmTag.Ed448: + Ed448PublicBcpgKey ed448key = (Ed448PublicBcpgKey)publicPk.Key; + return new Ed448PublicKeyParameters(ed448key.GetKey()); + + case PublicKeyAlgorithmTag.X448: + X448PublicBcpgKey x448key = (X448PublicBcpgKey)publicPk.Key; + return new X448PublicKeyParameters(x448key.GetKey()); + default: - throw new PgpException("unknown public key algorithm encountered"); + throw new PgpException("unknown public key algorithm encountered"); } } catch (PgpException) diff --git a/crypto/src/openpgp/PgpSecretKey.cs b/crypto/src/openpgp/PgpSecretKey.cs index 184621b5c..320c2956d 100644 --- a/crypto/src/openpgp/PgpSecretKey.cs +++ b/crypto/src/openpgp/PgpSecretKey.cs @@ -455,6 +455,8 @@ public bool IsSigningKey case PublicKeyAlgorithmTag.ECDsa: case PublicKeyAlgorithmTag.EdDsa_Legacy: case PublicKeyAlgorithmTag.ElGamalGeneral: + case PublicKeyAlgorithmTag.Ed25519: + case PublicKeyAlgorithmTag.Ed448: return true; default: return false; @@ -777,6 +779,28 @@ internal PgpPrivateKey DoExtractPrivateKey(byte[] rawPassPhrase, bool clearPassP ElGamalParameters elParams = new ElGamalParameters(elPub.P, elPub.G); privateKey = new ElGamalPrivateKeyParameters(elPriv.X, elParams); break; + + + case PublicKeyAlgorithmTag.Ed25519: + Ed25519SecretBcpgKey ed25519key = new Ed25519SecretBcpgKey(bcpgIn); + privateKey = new Ed25519PrivateKeyParameters(ed25519key.GetKey()); + break; + + case PublicKeyAlgorithmTag.X25519: + X25519SecretBcpgKey x25519key = new X25519SecretBcpgKey(bcpgIn); + privateKey = new X25519PrivateKeyParameters(x25519key.GetKey()); + break; + + case PublicKeyAlgorithmTag.Ed448: + Ed448SecretBcpgKey ed448key = new Ed448SecretBcpgKey(bcpgIn); + privateKey = new Ed448PrivateKeyParameters(ed448key.GetKey()); + break; + + case PublicKeyAlgorithmTag.X448: + X448SecretBcpgKey x448key = new X448SecretBcpgKey(bcpgIn); + privateKey = new X448PrivateKeyParameters(x448key.GetKey()); + break; + default: throw new PgpException("unknown public key algorithm encountered"); } diff --git a/crypto/src/openpgp/PgpSignature.cs b/crypto/src/openpgp/PgpSignature.cs index d6ffc0f74..4505c72c8 100644 --- a/crypto/src/openpgp/PgpSignature.cs +++ b/crypto/src/openpgp/PgpSignature.cs @@ -109,6 +109,12 @@ public void InitVerify(PgpPublicKey pubKey) try { sig.Init(false, key); + + if (Version == SignaturePacket.Version6) + { + byte[] salt = GetSignatureSalt(); + sig.BlockUpdate(salt, 0, salt.Length); + } } catch (InvalidKeyException e) { @@ -332,10 +338,15 @@ public byte[] GetSignatureTrailer() return sigPck.GetSignatureTrailer(); } - /// - /// Return true if the signature has either hashed or unhashed subpackets. - /// - public bool HasSubpackets + public byte[] GetSignatureSalt() + { + return sigPck.GetSignatureSalt(); + } + + /// + /// Return true if the signature has either hashed or unhashed subpackets. + /// + public bool HasSubpackets { get { diff --git a/crypto/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index c760131e7..150b2fe2a 100644 --- a/crypto/src/openpgp/PgpUtilities.cs +++ b/crypto/src/openpgp/PgpUtilities.cs @@ -562,6 +562,8 @@ internal static ISigner CreateSigner(PublicKeyAlgorithmTag publicKeyAlgorithm, H switch (publicKeyAlgorithm) { case PublicKeyAlgorithmTag.EdDsa_Legacy: + case PublicKeyAlgorithmTag.Ed25519: + case PublicKeyAlgorithmTag.Ed448: { ISigner signer; if (key is Ed25519PrivateKeyParameters || key is Ed25519PublicKeyParameters) diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs new file mode 100644 index 000000000..f6029f1d4 --- /dev/null +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -0,0 +1,273 @@ +using NUnit.Framework; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Agreement; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.Utilities.Test; +using System; +using System.Linq; +using System.Text; + +namespace Org.BouncyCastle.Bcpg.OpenPgp.Tests +{ + [TestFixture] + public class PgpCryptoRefreshTest + : SimpleTest + { + public override string Name => "PgpCryptoRefreshTest"; + + + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-key + private readonly byte[] v4Ed25519LegacyPubkeySample = Base64.Decode( + "xjMEU/NfCxYJKwYBBAHaRw8BAQdAPwmJlL3ZFu1AUxl5NOSofIBzOhKA1i+AEJku" + + "Q+47JAY="); + + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-sig + private readonly byte[] v4Ed25519LegacySignatureSample = Base64.Decode( + "iF4EABYIAAYFAlX5X5UACgkQjP3hIZeWWpr2IgD/VvkMypjiECY3vZg/2xbBMd/S" + + "ftgr9N3lYG4NdWrtM2YBANCcT6EVJ/A44PV/IgHYLy6iyQMyZfps60iehUuuYbQE"); + + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans + private readonly byte[] v6Certificate = Base64.Decode( + "xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf" + + "GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy" + + "KwwfHifBilZwj2Ul7Ce62azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lw" + + "gyU2kCcUmKfvBXbAf6rhRYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaE" + + "QsiPlR4zxP/TP7mhfVEe7XWPxtnMUMtf15OyA51YBM4qBmOHf+MZAAAAIIaTJINn" + + "+eUBXbki+PSAld2nhJh/LVmFsS+60WyvXkQ1wpsGGBsKAAAALAWCY4d/4wKbDCIh" + + "BssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAAQBIKbpGG2dWTX8" + + "j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805" + + "I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg=="); + + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-secret-key-transf + private readonly byte[] v6UnlockedSecretKey = Base64.Decode( + "xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB" + + "exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ" + + "BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6" + + "2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh" + + "RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe" + + "7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/" + + "LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG" + + "GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6" + + "2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE" + + "M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr" + + "k0mXubZvyl4GBg=="); + + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-locked-v6-secret-key + private readonly byte[] v6LockedSecretKey = Base64.Decode( + "xYIGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP9JgkC" + + "FARdb9ccngltHraRe25uHuyuAQQVtKipJ0+r5jL4dacGWSAheCWPpITYiyfyIOPS" + + "3gIDyg8f7strd1OB4+LZsUhcIjOMpVHgmiY/IutJkulneoBYwrEGHxsKAAAAQgWC" + + "Y4d/4wMLCQcFFQoOCAwCFgACmwMCHgkiIQbLGGxPBgmml+TVLfpscisMHx4nwYpW" + + "cI9lJewnutmsyQUnCQIHAgAAAACtKCAQPi19In7A5tfORHHbNr/JcIMlNpAnFJin" + + "7wV2wH+q4UWFs7kDsBJ+xP2i8CMEWi7Ha8tPlXGpZR4UruETeh1mhELIj5UeM8T/" + + "0z+5oX1RHu11j8bZzFDLX9eTsgOdWATHggZjh3/jGQAAACCGkySDZ/nlAV25Ivj0" + + "gJXdp4SYfy1ZhbEvutFsr15ENf0mCQIUBA5hhGgp2oaavg6mFUXcFMwBBBUuE8qf" + + "9Ock+xwusd+GAglBr5LVyr/lup3xxQvHXFSjjA2haXfoN6xUGRdDEHI6+uevKjVR" + + "v5oAxgu7eJpaXNjCmwYYGwoAAAAsBYJjh3/jApsMIiEGyxhsTwYJppfk1S36bHIr" + + "DB8eJ8GKVnCPZSXsJ7rZrMkAAAAABAEgpukYbZ1ZNfyP5WMUzbUnSGpaUSD5t2Ki" + + "Nacp8DkBClZRa2c3AMQzSDXa9jGhYzxjzVb5scHDzTkjyRZWRdTq8U6L4da+/+Kt" + + "ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG"); + + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-cleartext-signed-mes + private readonly string v6SampleCleartextSignedMessage = "What we need from the grocery store:\r\n\r\n- tofu\r\n- vegetables\r\n- noodles\r\n"; + private readonly byte[] v6SampleCleartextSignedMessageSignature = Base64.Decode( + "wpgGARsKAAAAKQWCY5ijYyIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6" + + "2azJAAAAAGk2IHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usJ9BvuAqo" + + "/FvLFuGWMbKAdA+epq7V4HOtAPlBWmU8QOd6aud+aSunHQaaEJ+iTFjP2OMW0KBr" + + "NK2ay45cX1IVAQ=="); + + [Test] + public void Version4Ed25519LegacyPubkeySampleTest() + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-key + PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v4Ed25519LegacyPubkeySample); + PgpPublicKey pubKey = pubRing.GetPublicKey(); + + IsEquals(pubKey.Algorithm, PublicKeyAlgorithmTag.EdDsa_Legacy); + IsEquals(pubKey.CreationTime.ToString("yyyyMMddHHmmss"), "20140819142827"); + + byte[] expectedFingerprint = Hex.Decode("C959BDBAFA32A2F89A153B678CFDE12197965A9A"); + IsEquals((ulong)pubKey.KeyId, 0x8CFDE12197965A9A); + IsTrue("wrong fingerprint", AreEqual(pubKey.GetFingerprint(), expectedFingerprint)); + } + + [Test] + public void Version4Ed25519LegacyCreateTest() + { + var key = new Ed25519PublicKeyParameters(Hex.Decode("3f098994bdd916ed4053197934e4a87c80733a1280d62f8010992e43ee3b2406")); + var pubKey = new PgpPublicKey(PublicKeyAlgorithmTag.EdDsa_Legacy, key, DateTime.Parse("2014-08-19 14:28:27Z")); + IsEquals(pubKey.Algorithm, PublicKeyAlgorithmTag.EdDsa_Legacy); + IsEquals(pubKey.CreationTime.ToString("yyyyMMddHHmmss"), "20140819142827"); + + byte[] expectedFingerprint = Hex.Decode("C959BDBAFA32A2F89A153B678CFDE12197965A9A"); + IsEquals((ulong)pubKey.KeyId, 0x8CFDE12197965A9A); + IsTrue("wrong fingerprint", AreEqual(pubKey.GetFingerprint(), expectedFingerprint)); + } + + [Test] + public void Version4Ed25519LegacySignatureSampleTest() + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-sig + PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v4Ed25519LegacyPubkeySample); + PgpPublicKey pubKey = pubRing.GetPublicKey(); + + PgpObjectFactory factory = new PgpObjectFactory(v4Ed25519LegacySignatureSample); + PgpSignatureList sigList = (PgpSignatureList)factory.NextPgpObject(); + PgpSignature signature = sigList[0]; + + IsEquals(signature.KeyId, pubKey.KeyId); + IsEquals(signature.KeyAlgorithm, PublicKeyAlgorithmTag.EdDsa_Legacy); + IsEquals(signature.HashAlgorithm, HashAlgorithmTag.Sha256); + IsEquals(signature.CreationTime.ToString("yyyyMMddHHmmss"), "20150916122453"); + + byte[] original = Encoding.UTF8.GetBytes("OpenPGP"); + signature.InitVerify(pubKey); + signature.Update(original); + + IsTrue("Failed generated signature check against original data", signature.Verify()); + } + + [Test] + public void Version6CertificateParsingTest() + { + /* + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans + * A Transferable Public Key consisting of: + * A v6 Ed25519 Public-Key packet + * A v6 direct key self-signature + * A v6 X25519 Public-Subkey packet + * A v6 subkey binding signature + */ + PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v6Certificate); + PgpPublicKey[] publicKeys = pubRing.GetPublicKeys().ToArray(); + IsEquals("wrong number of public keys", publicKeys.Length, 2); + + // master key + PgpPublicKey masterKey = publicKeys[0]; + FailIf("wrong detection of master key", !masterKey.IsMasterKey); + IsEquals(masterKey.Algorithm, PublicKeyAlgorithmTag.Ed25519); + IsEquals(masterKey.CreationTime.ToString("yyyyMMddHHmmss"), "20221130160803"); + byte[] expectedFingerprint = Hex.Decode("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9"); + IsEquals((ulong)masterKey.KeyId, 0xCB186C4F0609A697); + IsTrue("wrong master key fingerprint", AreEqual(masterKey.GetFingerprint(), expectedFingerprint)); + + // TODO Verify self signatures + + // subkey + PgpPublicKey subKey = publicKeys[1]; + FailIf("wrong detection of encryption subkey", !subKey.IsEncryptionKey); + IsEquals(subKey.Algorithm, PublicKeyAlgorithmTag.X25519); + expectedFingerprint = Hex.Decode("12C83F1E706F6308FE151A417743A1F033790E93E9978488D1DB378DA9930885"); + IsEquals(subKey.KeyId, 0x12C83F1E706F6308); + IsTrue("wrong sub key fingerprint", AreEqual(subKey.GetFingerprint(), expectedFingerprint)); + + // TODO Verify subkey binding signature + } + + [Test] + public void Version6UnlockedSecretKeyParsingTest() + { + /* + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-secret-key-transf + * A Transferable Secret Key consisting of: + * A v6 Ed25519 Secret-Key packet + * A v6 direct key self-signature + * A v6 X25519 Secret-Subkey packet + * A v6 subkey binding signature + */ + + PgpSecretKeyRing secretKeyRing = new PgpSecretKeyRing(v6UnlockedSecretKey); + PgpSecretKey[] secretKeys = secretKeyRing.GetSecretKeys().ToArray(); + IsEquals("wrong number of secret keys", secretKeys.Length, 2); + + // signing key + PgpSecretKey signingKey = secretKeys[0]; + IsEquals(signingKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.Ed25519); + IsEquals((ulong)signingKey.PublicKey.KeyId, 0xCB186C4F0609A697); + + AsymmetricCipherKeyPair signingKeyPair = GetKeyPair(signingKey); + IsTrue("signature test failed", SignThenVerifyEd25519Test(signingKeyPair)); + + // encryption key + PgpSecretKey encryptionKey = secretKeys[1]; + IsEquals(encryptionKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X25519); + IsEquals(encryptionKey.PublicKey.KeyId, 0x12C83F1E706F6308); + + AsymmetricCipherKeyPair alice = GetKeyPair(encryptionKey); + IAsymmetricCipherKeyPairGenerator kpGen = new X25519KeyPairGenerator(); + kpGen.Init(new X25519KeyGenerationParameters(new SecureRandom())); + AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); + + IsTrue("X25519 agreement failed", EncryptThenDecryptX25519Test(alice, bob)); + } + + [Test] + public void Version6SampleCleartextSignedMessageVerifySignatureTest() + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-cleartext-signed-mes + + PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v6Certificate); + PgpPublicKey pubKey = pubRing.GetPublicKey(); + + PgpObjectFactory factory = new PgpObjectFactory(v6SampleCleartextSignedMessageSignature); + PgpSignatureList sigList = (PgpSignatureList)factory.NextPgpObject(); + PgpSignature signature = sigList[0]; + + byte[] data = Encoding.UTF8.GetBytes(v6SampleCleartextSignedMessage); + signature.InitVerify(pubKey); + signature.Update(data); + + IsTrue("Failed generated signature check against original data", signature.Verify()); + } + + private static AsymmetricCipherKeyPair GetKeyPair(PgpSecretKey secretKey, string password = "") + { + return new AsymmetricCipherKeyPair( + secretKey.PublicKey.GetKey(), + secretKey.ExtractPrivateKey(password.ToCharArray()).Key); + } + + private static bool SignThenVerifyEd25519Test(AsymmetricCipherKeyPair signingKeyPair) + { + byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); + + ISigner signer = new Ed25519Signer(); + signer.Init(true, signingKeyPair.Private); + signer.BlockUpdate(data, 0, data.Length); + byte[] signature = signer.GenerateSignature(); + + signer.Init(false, signingKeyPair.Public); + signer.BlockUpdate(data, 0, data.Length); + return signer.VerifySignature(signature); + } + + private static bool EncryptThenDecryptX25519Test(AsymmetricCipherKeyPair alice, AsymmetricCipherKeyPair bob) + { + X25519Agreement agreeA = new X25519Agreement(); + agreeA.Init(alice.Private); + byte[] secretA = new byte[agreeA.AgreementSize]; + agreeA.CalculateAgreement(bob.Public, secretA, 0); + + X25519Agreement agreeB = new X25519Agreement(); + agreeB.Init(bob.Private); + byte[] secretB = new byte[agreeB.AgreementSize]; + agreeB.CalculateAgreement(alice.Public, secretB, 0); + + return Arrays.AreEqual(secretA, secretB); + } + + public override void PerformTest() + { + Version4Ed25519LegacyPubkeySampleTest(); + Version4Ed25519LegacySignatureSampleTest(); + Version6CertificateParsingTest(); + Version6UnlockedSecretKeyParsingTest(); + Version6SampleCleartextSignedMessageVerifySignatureTest(); + } + } +} \ No newline at end of file From 182c2aef4ee320636dd4a9a38593f5175fdcf989 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Wed, 14 Feb 2024 20:47:46 +0100 Subject: [PATCH 05/37] basic creation of v6 public keys --- crypto/src/bcpg/PublicKeyPacket.cs | 26 ++++++- crypto/src/bcpg/PublicSubkeyPacket.cs | 14 +++- crypto/src/openpgp/PgpPublicKey.cs | 74 ++++++++++++++----- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 45 +++++++++-- 4 files changed, 128 insertions(+), 31 deletions(-) diff --git a/crypto/src/bcpg/PublicKeyPacket.cs b/crypto/src/bcpg/PublicKeyPacket.cs index 26cc069d9..6449c03f4 100644 --- a/crypto/src/bcpg/PublicKeyPacket.cs +++ b/crypto/src/bcpg/PublicKeyPacket.cs @@ -15,6 +15,8 @@ public class PublicKeyPacket public const int Version5 = 5; public const int Version6 = 6; + public const int DefaultVersion = Version4; + private readonly int version; private readonly long time; private readonly int validDays; @@ -84,16 +86,32 @@ internal PublicKeyPacket( } } + + /// Construct a public key packet. + public PublicKeyPacket( + int version, + PublicKeyAlgorithmTag algorithm, + DateTime time, + IBcpgKey key) + { + this.version = version; + this.time = DateTimeUtilities.DateTimeToUnixMs(time) / 1000L; + this.algorithm = algorithm; + this.key = key; + + if (version == Version5 || version == Version6) + { + v6KeyLen = ((BcpgObject)key).GetEncoded().Length; + } + } + /// Construct a version 4 public key packet. public PublicKeyPacket( PublicKeyAlgorithmTag algorithm, DateTime time, IBcpgKey key) + :this(DefaultVersion, algorithm, time, key) { - this.version = Version4; - this.time = DateTimeUtilities.DateTimeToUnixMs(time) / 1000L; - this.algorithm = algorithm; - this.key = key; } public virtual int Version diff --git a/crypto/src/bcpg/PublicSubkeyPacket.cs b/crypto/src/bcpg/PublicSubkeyPacket.cs index 0e1065b72..b9c7d73f5 100644 --- a/crypto/src/bcpg/PublicSubkeyPacket.cs +++ b/crypto/src/bcpg/PublicSubkeyPacket.cs @@ -13,12 +13,22 @@ internal PublicSubkeyPacket( { } - /// Construct a version 4 public subkey packet. + /// Construct a public subkey packet. + public PublicSubkeyPacket( + int version, + PublicKeyAlgorithmTag algorithm, + DateTime time, + IBcpgKey key) + : base(version, algorithm, time, key) + { + } + + /// Construct a version 4 public subkey packet. public PublicSubkeyPacket( PublicKeyAlgorithmTag algorithm, DateTime time, IBcpgKey key) - : base(algorithm, time, key) + : base(DefaultVersion, algorithm, time, key) { } diff --git a/crypto/src/openpgp/PgpPublicKey.cs b/crypto/src/openpgp/PgpPublicKey.cs index 06f9e2277..a2689d4e5 100644 --- a/crypto/src/openpgp/PgpPublicKey.cs +++ b/crypto/src/openpgp/PgpPublicKey.cs @@ -196,6 +196,12 @@ private void Init() } } + + public PgpPublicKey(PublicKeyAlgorithmTag algorithm, AsymmetricKeyParameter pubKey, DateTime time) + :this(PublicKeyPacket.DefaultVersion, algorithm, pubKey, time) + { + } + /// /// Create a PgpPublicKey from the passed in lightweight one. /// @@ -208,7 +214,7 @@ private void Init() /// Date of creation. /// If pubKey is not public. /// On key creation problem. - public PgpPublicKey(PublicKeyAlgorithmTag algorithm, AsymmetricKeyParameter pubKey, DateTime time) + public PgpPublicKey(int version, PublicKeyAlgorithmTag algorithm, AsymmetricKeyParameter pubKey, DateTime time) { if (pubKey.IsPrivate) throw new ArgumentException("Expected a public key", nameof(pubKey)); @@ -248,44 +254,72 @@ public PgpPublicKey(PublicKeyAlgorithmTag algorithm, AsymmetricKeyParameter pubK } else if (pubKey is Ed25519PublicKeyParameters ed25519PubKey) { - byte[] pointEnc = new byte[1 + Ed25519PublicKeyParameters.KeySize]; - pointEnc[0] = 0x40; - ed25519PubKey.Encode(pointEnc, 1); - bcpgKey = new EdDsaPublicBcpgKey(GnuObjectIdentifiers.Ed25519, new BigInteger(1, pointEnc)); + if (algorithm == PublicKeyAlgorithmTag.Ed25519) + { + bcpgKey = new Ed25519PublicBcpgKey(ed25519PubKey.GetEncoded()); + } + else + { + byte[] pointEnc = new byte[1 + Ed25519PublicKeyParameters.KeySize]; + pointEnc[0] = 0x40; + ed25519PubKey.Encode(pointEnc, 1); + bcpgKey = new EdDsaPublicBcpgKey(GnuObjectIdentifiers.Ed25519, new BigInteger(1, pointEnc)); + } } else if (pubKey is Ed448PublicKeyParameters ed448PubKey) { - byte[] pointEnc = new byte[Ed448PublicKeyParameters.KeySize]; - ed448PubKey.Encode(pointEnc, 0); - bcpgKey = new EdDsaPublicBcpgKey(EdECObjectIdentifiers.id_Ed448, new BigInteger(1, pointEnc)); + if (algorithm == PublicKeyAlgorithmTag.Ed448) + { + bcpgKey = new Ed448PublicBcpgKey(ed448PubKey.GetEncoded()); + } + else + { + byte[] pointEnc = new byte[Ed448PublicKeyParameters.KeySize]; + ed448PubKey.Encode(pointEnc, 0); + bcpgKey = new EdDsaPublicBcpgKey(EdECObjectIdentifiers.id_Ed448, new BigInteger(1, pointEnc)); + } } else if (pubKey is X25519PublicKeyParameters x25519PubKey) { - byte[] pointEnc = new byte[1 + X25519PublicKeyParameters.KeySize]; - pointEnc[0] = 0x40; - x25519PubKey.Encode(pointEnc, 1); + if (algorithm == PublicKeyAlgorithmTag.X25519) + { + bcpgKey = new X25519PublicBcpgKey(x25519PubKey.GetEncoded()); + } + else + { + byte[] pointEnc = new byte[1 + X25519PublicKeyParameters.KeySize]; + pointEnc[0] = 0x40; + x25519PubKey.Encode(pointEnc, 1); - PgpKdfParameters kdfParams = DefaultKdfParameters; + PgpKdfParameters kdfParams = DefaultKdfParameters; - bcpgKey = new ECDHPublicBcpgKey(CryptlibObjectIdentifiers.curvey25519, new BigInteger(1, pointEnc), - kdfParams.HashAlgorithm, kdfParams.SymmetricWrapAlgorithm); + bcpgKey = new ECDHPublicBcpgKey(CryptlibObjectIdentifiers.curvey25519, new BigInteger(1, pointEnc), + kdfParams.HashAlgorithm, kdfParams.SymmetricWrapAlgorithm); + } } else if (pubKey is X448PublicKeyParameters x448PubKey) { - byte[] pointEnc = new byte[X448PublicKeyParameters.KeySize]; - x448PubKey.Encode(pointEnc, 0); + if (algorithm == PublicKeyAlgorithmTag.X448) + { + bcpgKey = new X448PublicBcpgKey(x448PubKey.GetEncoded()); + } + else + { + byte[] pointEnc = new byte[X448PublicKeyParameters.KeySize]; + x448PubKey.Encode(pointEnc, 0); - PgpKdfParameters kdfParams = DefaultKdfParameters; + PgpKdfParameters kdfParams = DefaultKdfParameters; - bcpgKey = new ECDHPublicBcpgKey(EdECObjectIdentifiers.id_X448, new BigInteger(1, pointEnc), - kdfParams.HashAlgorithm, kdfParams.SymmetricWrapAlgorithm); + bcpgKey = new ECDHPublicBcpgKey(EdECObjectIdentifiers.id_X448, new BigInteger(1, pointEnc), + kdfParams.HashAlgorithm, kdfParams.SymmetricWrapAlgorithm); + } } else { throw new PgpException("unknown key class"); } - this.publicPk = new PublicKeyPacket(algorithm, time, bcpgKey); + this.publicPk = new PublicKeyPacket(version, algorithm, time, bcpgKey); this.ids = new List(); this.idSigs = new List>(); diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index f6029f1d4..2748d9233 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -169,6 +169,32 @@ public void Version6CertificateParsingTest() // TODO Verify subkey binding signature } + [Test] + public void Version6PublicKeyCreationTest() + { + /* + * Create a v6 Ed25519 pubkey with the same key material and creation datetime as the test vector + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans + * the check the fingerprint and verify a signature + */ + byte[] keyMaterial = Hex.Decode("f94da7bb48d60a61e567706a6587d0331999bb9d891a08242ead84543df895a3"); + var key = new Ed25519PublicKeyParameters(keyMaterial); + var pubKey = new PgpPublicKey(PublicKeyPacket.Version6, PublicKeyAlgorithmTag.Ed25519, key, DateTime.Parse("2022-11-30 16:08:03Z")); + + IsEquals(pubKey.Algorithm, PublicKeyAlgorithmTag.Ed25519); + IsEquals(pubKey.CreationTime.ToString("yyyyMMddHHmmss"), "20221130160803"); + byte[] expectedFingerprint = Hex.Decode("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9"); + IsEquals((ulong)pubKey.KeyId, 0xCB186C4F0609A697); + IsTrue("wrong master key fingerprint", AreEqual(pubKey.GetFingerprint(), expectedFingerprint)); + + bool signatureOk = VerifySignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes(v6SampleCleartextSignedMessage), + pubKey); + + IsTrue("Failed generated signature check against original data", signatureOk); + } + [Test] public void Version6UnlockedSecretKeyParsingTest() { @@ -214,15 +240,23 @@ public void Version6SampleCleartextSignedMessageVerifySignatureTest() PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v6Certificate); PgpPublicKey pubKey = pubRing.GetPublicKey(); - PgpObjectFactory factory = new PgpObjectFactory(v6SampleCleartextSignedMessageSignature); + bool signatureOk = VerifySignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes(v6SampleCleartextSignedMessage), + pubKey); + + IsTrue("Failed generated signature check against original data", signatureOk); + } + + private static bool VerifySignature(byte[] sigPacket, byte[] data, PgpPublicKey signer) + { + PgpObjectFactory factory = new PgpObjectFactory(sigPacket); PgpSignatureList sigList = (PgpSignatureList)factory.NextPgpObject(); PgpSignature signature = sigList[0]; - byte[] data = Encoding.UTF8.GetBytes(v6SampleCleartextSignedMessage); - signature.InitVerify(pubKey); + signature.InitVerify(signer); signature.Update(data); - - IsTrue("Failed generated signature check against original data", signature.Verify()); + return signature.Verify(); } private static AsymmetricCipherKeyPair GetKeyPair(PgpSecretKey secretKey, string password = "") @@ -266,6 +300,7 @@ public override void PerformTest() Version4Ed25519LegacyPubkeySampleTest(); Version4Ed25519LegacySignatureSampleTest(); Version6CertificateParsingTest(); + Version6PublicKeyCreationTest(); Version6UnlockedSecretKeyParsingTest(); Version6SampleCleartextSignedMessageVerifySignatureTest(); } From 31f2b9841e2603b658d9fc01532bf648556f7788 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 15 Feb 2024 08:47:22 +0100 Subject: [PATCH 06/37] check correct salt size in v6 signature packets --- crypto/src/bcpg/SignaturePacket.cs | 12 +++++++++++- crypto/src/openpgp/PgpUtilities.cs | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crypto/src/bcpg/SignaturePacket.cs b/crypto/src/bcpg/SignaturePacket.cs index 79dc8cebe..4fcd27be1 100644 --- a/crypto/src/bcpg/SignaturePacket.cs +++ b/crypto/src/bcpg/SignaturePacket.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.IO; - +using Org.BouncyCastle.Bcpg.OpenPgp; using Org.BouncyCastle.Bcpg.Sig; using Org.BouncyCastle.Crypto.Utilities; using Org.BouncyCastle.Utilities; @@ -162,6 +162,16 @@ internal SignaturePacket(BcpgInputStream bcpgIn) if (version == Version6) { int saltSize = bcpgIn.ReadByte(); + + if (saltSize != PgpUtilities.GetSaltSize(hashAlgorithm)) + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-4-and-6-signature-p + // The salt size MUST match the value defined for the hash algorithm as specified in Table 23 + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#hash-algorithms-registry + + throw new IOException($"invalid salt size for v6 signature: expected {PgpUtilities.GetSaltSize(hashAlgorithm)} got {saltSize}"); + } + salt = new byte[saltSize]; bcpgIn.ReadFully(salt); } diff --git a/crypto/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index 150b2fe2a..5560b0023 100644 --- a/crypto/src/openpgp/PgpUtilities.cs +++ b/crypto/src/openpgp/PgpUtilities.cs @@ -115,6 +115,28 @@ public static string GetDigestName( } } + /// + /// Returns the V6 signature salt size for a hash algorithm. + /// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-hash-algorithms + /// + public static int GetSaltSize(HashAlgorithmTag hashAlgorithm) + { + switch (hashAlgorithm) + { + case HashAlgorithmTag.Sha256: + case HashAlgorithmTag.Sha224: + case HashAlgorithmTag.Sha3_256: + return 16; + case HashAlgorithmTag.Sha384: + return 24; + case HashAlgorithmTag.Sha512: + case HashAlgorithmTag.Sha3_512: + return 32; + default: + return 0; + } + } + public static int GetDigestIDForName(string name) { if (NameToHashID.TryGetValue(name, out var hashAlgorithmTag)) From 3edb70201e578da7d3a2ab634f8243f519e0a7b7 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 15 Feb 2024 17:40:50 +0100 Subject: [PATCH 07/37] refactored SecretKeyPacket (based on java version) --- crypto/src/bcpg/AeadUtils.cs | 75 +++++++ crypto/src/bcpg/S2k.cs | 1 + crypto/src/bcpg/SecretKeyPacket.cs | 205 +++++++++++++++--- crypto/src/bcpg/SecretSubkeyPacket.cs | 14 +- crypto/src/openpgp/PgpUtilities.cs | 7 +- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 25 +++ 6 files changed, 289 insertions(+), 38 deletions(-) create mode 100644 crypto/src/bcpg/AeadUtils.cs diff --git a/crypto/src/bcpg/AeadUtils.cs b/crypto/src/bcpg/AeadUtils.cs new file mode 100644 index 000000000..922a4860c --- /dev/null +++ b/crypto/src/bcpg/AeadUtils.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using Org.BouncyCastle.Bcpg.OpenPgp; +using Org.BouncyCastle.Utilities; + +namespace Org.BouncyCastle.Bcpg +{ + public sealed class AeadUtils + { + /** + * Return the length of the IV used by the given AEAD algorithm in octets. + * + * @param aeadAlgorithmTag AEAD algorithm identifier + * @return length of the IV + */ + public static int GetIVLength(AeadAlgorithmTag aeadAlgorithmTag) + { + switch (aeadAlgorithmTag) + { + case AeadAlgorithmTag.Eax: + return 16; + case AeadAlgorithmTag.Ocb: + return 15; + case AeadAlgorithmTag.Gcm: + return 12; + default: + throw new ArgumentException($"Invalid AEAD algorithm tag: {aeadAlgorithmTag}"); + } + } + + /** + * Return the length of the authentication tag used by the given AEAD algorithm in octets. + * + * @param aeadAlgorithmTag AEAD algorithm identifier + * @return length of the auth tag + */ + public static int GetAuthTagLength(AeadAlgorithmTag aeadAlgorithmTag) + { + switch (aeadAlgorithmTag) + { + case AeadAlgorithmTag.Eax: + case AeadAlgorithmTag.Ocb: + case AeadAlgorithmTag.Gcm: + return 16; + default: + throw new ArgumentException($"Invalid AEAD algorithm tag: {aeadAlgorithmTag}"); + } + } + + /** + * Split a given byte array containing
m
bytes of key and
n-8
bytes of IV into + * two separate byte arrays. + *
m
is the key length of the cipher algorithm, while
n
is the IV length of the AEAD algorithm. + * Note, that the IV is filled with
n-8
bytes only, the remainder is left as 0s. + * Return an array of both arrays with the key and index 0 and the IV at index 1. + * + * @param messageKeyAndIv
m+n-8
bytes of concatenated message key and IV + * @param cipherAlgo symmetric cipher algorithm + * @param aeadAlgo AEAD algorithm + * @return array of arrays containing message key and IV + */ + public static byte[][] SplitMessageKeyAndIv(byte[] messageKeyAndIv, SymmetricKeyAlgorithmTag cipherAlgo, AeadAlgorithmTag aeadAlgo) + { + int keyLen = PgpUtilities.GetKeySizeInOctets(cipherAlgo); + int ivLen = GetIVLength(aeadAlgo); + byte[] messageKey = new byte[keyLen]; + byte[] iv = new byte[ivLen]; + + Array.Copy(messageKeyAndIv, messageKey, messageKey.Length); + Array.Copy(messageKeyAndIv, messageKey.Length, iv, 0, ivLen-8); + + return new byte[][] { messageKey, iv }; + } + } +} \ No newline at end of file diff --git a/crypto/src/bcpg/S2k.cs b/crypto/src/bcpg/S2k.cs index 8446fd1ec..d5f31143d 100644 --- a/crypto/src/bcpg/S2k.cs +++ b/crypto/src/bcpg/S2k.cs @@ -15,6 +15,7 @@ public class S2k public const int Simple = 0; public const int Salted = 1; public const int SaltedAndIterated = 3; + public const int Argon2 = 4; public const int GnuDummyS2K = 101; public const int GnuProtectionModeNoPrivateKey = 1; public const int GnuProtectionModeDivertToCard = 2; diff --git a/crypto/src/bcpg/SecretKeyPacket.cs b/crypto/src/bcpg/SecretKeyPacket.cs index c5b7e8126..7e825c945 100644 --- a/crypto/src/bcpg/SecretKeyPacket.cs +++ b/crypto/src/bcpg/SecretKeyPacket.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using Org.BouncyCastle.Pqc.Crypto.SphincsPlus; using Org.BouncyCastle.Utilities; namespace Org.BouncyCastle.Bcpg @@ -8,9 +9,54 @@ namespace Org.BouncyCastle.Bcpg public class SecretKeyPacket : ContainedPacket //, PublicKeyAlgorithmTag { - public const int UsageNone = 0x00; - public const int UsageChecksum = 0xff; - public const int UsageSha1 = 0xfe; + + /** + * Unprotected. + */ + public const int UsageNone = 0x00; + + /** + * Malleable CFB. + * Malleable-CFB-encrypted keys are vulnerable to corruption attacks + * that can cause leakage of secret data when the secret key is used. + * + * @see + * Klíma, V. and T. Rosa, + * "Attack on Private Signature Keys of the OpenPGP Format, + * PGP(TM) Programs and Other Applications Compatible with OpenPGP" + * @see + * Bruseghini, L., Paterson, K. G., and D. Huigens, + * "Victory by KO: Attacking OpenPGP Using Key Overwriting" + * @deprecated Use of MalleableCFB is deprecated. + * For v4 keys, use {@link #USAGE_SHA1} instead. + * For v6 keys use {@link #USAGE_AEAD} instead. + */ + public const int UsageChecksum = 0xff; + + /** + * CFB. + * CFB-encrypted keys are vulnerable to corruption attacks that can + * cause leakage of secret data when the secret key is use. + * + * @see + * Klíma, V. and T. Rosa, + * "Attack on Private Signature Keys of the OpenPGP Format, + * PGP(TM) Programs and Other Applications Compatible with OpenPGP" + * @see + * Bruseghini, L., Paterson, K. G., and D. Huigens, + * "Victory by KO: Attacking OpenPGP Using Key Overwriting" + */ + public const int UsageSha1 = 0xfe; + + /** + * AEAD. + * This usage protects against above-mentioned attacks. + * Passphrase-protected secret key material in a v6 Secret Key or + * v6 Secret Subkey packet SHOULD be protected with AEAD encryption + * unless it will be transferred to an implementation that is known + * to not support AEAD. + * Users should migrate to AEAD with all due speed. + */ public const int UsageAead = 0xfd; @@ -20,7 +66,10 @@ public class SecretKeyPacket private readonly SymmetricKeyAlgorithmTag encAlgorithm; private readonly S2k s2k; private readonly byte[] iv; - private readonly AeadAlgorithmTag aeadAlgo; + private readonly AeadAlgorithmTag aeadAlgorithm; + + private bool HasS2KSpecifier + => (s2kUsage == UsageChecksum || s2kUsage == UsageSha1 || s2kUsage == UsageAead); internal SecretKeyPacket( BcpgInputStream bcpgIn) @@ -34,38 +83,53 @@ internal SecretKeyPacket( pubKeyPacket = new PublicKeyPacket(bcpgIn); } - s2kUsage = bcpgIn.ReadByte(); + int version = pubKeyPacket.Version; + s2kUsage = bcpgIn.ReadByte(); - if (s2kUsage != UsageNone && pubKeyPacket.Version == PublicKeyPacket.Version6) + if (version == PublicKeyPacket.Version6 && s2kUsage != UsageNone) { // TODO: Use length to parse unknown parameters int conditionalParameterLength = bcpgIn.ReadByte(); } - if (s2kUsage == UsageChecksum || s2kUsage == UsageSha1 || s2kUsage == UsageAead) + if (HasS2KSpecifier) { - encAlgorithm = (SymmetricKeyAlgorithmTag) bcpgIn.ReadByte(); - if (s2kUsage == UsageAead) - { - aeadAlgo = (AeadAlgorithmTag)bcpgIn.ReadByte(); - } - if (pubKeyPacket.Version == 6 && (s2kUsage == UsageSha1 || s2kUsage == UsageAead)) + encAlgorithm = (SymmetricKeyAlgorithmTag)bcpgIn.ReadByte(); + } + else + { + encAlgorithm = (SymmetricKeyAlgorithmTag)s2kUsage; + } + + if (s2kUsage == UsageAead) + { + aeadAlgorithm = (AeadAlgorithmTag)bcpgIn.ReadByte(); + } + + if (HasS2KSpecifier) + { + if (version == PublicKeyPacket.Version6) { // TODO: Use length to parse unknown S2Ks int s2kLen = bcpgIn.ReadByte(); } s2k = new S2k(bcpgIn); } - else + if (s2kUsage == UsageAead) { - encAlgorithm = (SymmetricKeyAlgorithmTag) s2kUsage; + iv = new byte[AeadUtils.GetIVLength(aeadAlgorithm)]; + bcpgIn.ReadFully(iv); } - if (!(s2k != null && s2k.Type == S2k.GnuDummyS2K && s2k.ProtectionMode == 0x01)) + bool isGNUDummyNoPrivateKey = s2k != null + && s2k.Type == S2k.GnuDummyS2K + && s2k.ProtectionMode == S2k.GnuProtectionModeNoPrivateKey; + + if (!(isGNUDummyNoPrivateKey)) { - if (s2kUsage != 0) - { - if (((int) encAlgorithm) < 7) + if (s2kUsage != 0 && iv == null) + { + if ((int)encAlgorithm < 7) { iv = new byte[8]; } @@ -77,7 +141,7 @@ internal SecretKeyPacket( } } - secKeyData = bcpgIn.ReadAll(); + secKeyData = bcpgIn.ReadAll(); } public SecretKeyPacket( @@ -120,12 +184,49 @@ public SecretKeyPacket( this.secKeyData = secKeyData; } - public SymmetricKeyAlgorithmTag EncAlgorithm + + public SecretKeyPacket( + PublicKeyPacket pubKeyPacket, + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, + int s2kUsage, + S2k s2k, + byte[] iv, + byte[] secKeyData) + { + this.pubKeyPacket = pubKeyPacket; + this.encAlgorithm = encAlgorithm; + this.aeadAlgorithm = aeadAlgorithm; + this.s2kUsage = s2kUsage; + this.s2k = s2k; + this.iv = Arrays.Clone(iv); + this.secKeyData = secKeyData; + + if (s2k != null && s2k.Type == S2k.Argon2 && s2kUsage != UsageAead) + { + throw new ArgumentException("Argon2 is only used with AEAD (S2K usage octet 253)"); + } + + if (pubKeyPacket.Version == PublicKeyPacket.Version6) + { + if (s2kUsage == UsageChecksum) + { + throw new ArgumentException("Version 6 keys MUST NOT use S2K usage UsageChecksum"); + } + } + } + + public SymmetricKeyAlgorithmTag EncAlgorithm { get { return encAlgorithm; } } - public int S2kUsage + public AeadAlgorithmTag GetAeadAlgorithm() + { + return aeadAlgorithm; + } + + public int S2kUsage { get { return s2kUsage; } } @@ -150,31 +251,63 @@ public byte[] GetSecretKeyData() return secKeyData; } - public byte[] GetEncodedContents() + + + private byte[] EncodeConditionalParameters() { - MemoryStream bOut = new MemoryStream(); - using (var pOut = new BcpgOutputStream(bOut)) + using (MemoryStream conditionalParameters = new MemoryStream()) { - pOut.Write(pubKeyPacket.GetEncodedContents()); - pOut.WriteByte((byte)s2kUsage); - - if (s2kUsage == UsageChecksum || s2kUsage == UsageSha1) + if (HasS2KSpecifier) { - pOut.WriteByte((byte)encAlgorithm); - pOut.WriteObject(s2k); + conditionalParameters.WriteByte((byte)encAlgorithm); + if (s2kUsage == UsageAead) + { + conditionalParameters.WriteByte((byte)aeadAlgorithm); + } + byte[] encodedS2K = s2k.GetEncoded(); + if (pubKeyPacket.Version == PublicKeyPacket.Version6) + { + conditionalParameters.WriteByte((byte)encodedS2K.Length); + } + conditionalParameters.Write(encodedS2K, 0, encodedS2K.Length); } - if (iv != null) { - pOut.Write(iv); + // since USAGE_AEAD and other types that use an IV are mutually exclusive, + // we use the IV field for both v4 IVs and v6 AEAD nonces + conditionalParameters.Write(iv, 0, iv.Length); } - if (secKeyData != null && secKeyData.Length > 0) + return conditionalParameters.ToArray(); + } + } + + public byte[] GetEncodedContents() + { + using (MemoryStream bOut = new MemoryStream()) + { + using (var pOut = new BcpgOutputStream(bOut)) { - pOut.Write(secKeyData); + pOut.Write(pubKeyPacket.GetEncodedContents()); + pOut.WriteByte((byte)s2kUsage); + + // conditional parameters + byte[] conditionalParameters = EncodeConditionalParameters(); + if (pubKeyPacket.Version == PublicKeyPacket.Version6 && s2kUsage != UsageNone) + { + pOut.WriteByte((byte)conditionalParameters.Length); + } + pOut.Write(conditionalParameters); + + // encrypted secret key + if (secKeyData != null && secKeyData.Length > 0) + { + pOut.Write(secKeyData); + } + pOut.Close(); } + return bOut.ToArray(); } - return bOut.ToArray(); } public override void Encode(BcpgOutputStream bcpgOut) diff --git a/crypto/src/bcpg/SecretSubkeyPacket.cs b/crypto/src/bcpg/SecretSubkeyPacket.cs index 2d405aec2..fd05f81b8 100644 --- a/crypto/src/bcpg/SecretSubkeyPacket.cs +++ b/crypto/src/bcpg/SecretSubkeyPacket.cs @@ -34,7 +34,19 @@ public SecretSubkeyPacket( { } - public override void Encode(BcpgOutputStream bcpgOut) + public SecretSubkeyPacket( + PublicKeyPacket pubKeyPacket, + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, + int s2kUsage, + S2k s2k, + byte[] iv, + byte[] secKeyData) + :base(pubKeyPacket, encAlgorithm, aeadAlgorithm, s2kUsage, s2k, iv, secKeyData) + { + } + + public override void Encode(BcpgOutputStream bcpgOut) { bcpgOut.WritePacket(PacketTag.SecretSubkey, GetEncodedContents()); } diff --git a/crypto/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index 5560b0023..08418d126 100644 --- a/crypto/src/openpgp/PgpUtilities.cs +++ b/crypto/src/openpgp/PgpUtilities.cs @@ -272,7 +272,12 @@ public static int GetKeySize(SymmetricKeyAlgorithmTag algorithm) return keySize; } - public static KeyParameter MakeKey( + public static int GetKeySizeInOctets(SymmetricKeyAlgorithmTag algorithm) + { + return (GetKeySize(algorithm) + 7) / 8; + } + + public static KeyParameter MakeKey( SymmetricKeyAlgorithmTag algorithm, byte[] keyBytes) { diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 2748d9233..5e3382346 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -9,6 +9,7 @@ using Org.BouncyCastle.Utilities.Encoders; using Org.BouncyCastle.Utilities.Test; using System; +using System.IO; using System.Linq; using System.Text; @@ -167,6 +168,18 @@ public void Version6CertificateParsingTest() IsTrue("wrong sub key fingerprint", AreEqual(subKey.GetFingerprint(), expectedFingerprint)); // TODO Verify subkey binding signature + + // Encode test + using (MemoryStream ms = new MemoryStream()) + { + using (BcpgOutputStream bs = new BcpgOutputStream(ms, newFormatOnly: true)) + { + pubRing.Encode(bs); + } + + byte[] encoded = ms.ToArray(); + IsTrue(AreEqual(encoded, v6Certificate)); + } } [Test] @@ -230,6 +243,18 @@ public void Version6UnlockedSecretKeyParsingTest() AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); IsTrue("X25519 agreement failed", EncryptThenDecryptX25519Test(alice, bob)); + + // Encode test + using (MemoryStream ms = new MemoryStream()) + { + using (BcpgOutputStream bs = new BcpgOutputStream(ms, newFormatOnly: true)) + { + secretKeyRing.Encode(bs); + } + + byte[] encoded = ms.ToArray(); + IsTrue(AreEqual(encoded, v6UnlockedSecretKey)); + } } [Test] From ad9b9b2eaf6308bfe666ef097d8abf7b0fc6de2e Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 15 Feb 2024 20:17:02 +0100 Subject: [PATCH 08/37] refactored S2k class (based on java version) --- crypto/src/bcpg/S2k.cs | 178 +++++++++++++----- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 42 +++++ 2 files changed, 168 insertions(+), 52 deletions(-) diff --git a/crypto/src/bcpg/S2k.cs b/crypto/src/bcpg/S2k.cs index d5f31143d..3dafe8759 100644 --- a/crypto/src/bcpg/S2k.cs +++ b/crypto/src/bcpg/S2k.cs @@ -1,6 +1,5 @@ using System; using System.IO; - using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.IO; @@ -20,71 +19,110 @@ public class S2k public const int GnuProtectionModeNoPrivateKey = 1; public const int GnuProtectionModeDivertToCard = 2; - internal int type; - internal HashAlgorithmTag algorithm; - internal byte[] iv; - internal int itCount = -1; - internal int protectionMode = -1; + private readonly int type; + private readonly HashAlgorithmTag algorithm; + private readonly byte[] iv; + + private readonly int itCount = -1; + private readonly int protectionMode = -1; + + // params for Argon2 + private readonly int passes; + private readonly int parallelism; + private readonly int memorySizeExponent; internal S2k( Stream inStr) { type = inStr.ReadByte(); - algorithm = (HashAlgorithmTag) inStr.ReadByte(); - // - // if this happens we have a dummy-S2k packet. - // - if (type != GnuDummyS2K) - { - if (type != 0) - { - iv = new byte[8]; - if (Streams.ReadFully(inStr, iv, 0, iv.Length) < iv.Length) - throw new EndOfStreamException(); - - if (type == 3) - { - itCount = inStr.ReadByte(); - } - } - } - else + switch (type) { - inStr.ReadByte(); // G - inStr.ReadByte(); // N - inStr.ReadByte(); // U - protectionMode = inStr.ReadByte(); // protection mode + case Simple: + algorithm = (HashAlgorithmTag)inStr.ReadByte(); + break; + + case Salted: + algorithm = (HashAlgorithmTag)inStr.ReadByte(); + iv = new byte[8]; + Streams.ReadFully(inStr, iv); + break; + + case SaltedAndIterated: + algorithm = (HashAlgorithmTag)inStr.ReadByte(); + iv = new byte[8]; + Streams.ReadFully(inStr, iv); + itCount = inStr.ReadByte(); + break; + + case Argon2: + iv = new byte[16]; + Streams.ReadFully(inStr, iv); + passes = inStr.ReadByte(); + parallelism = inStr.ReadByte(); + memorySizeExponent = inStr.ReadByte(); + break; + + case GnuDummyS2K: + algorithm = (HashAlgorithmTag)inStr.ReadByte(); + inStr.ReadByte(); // G + inStr.ReadByte(); // N + inStr.ReadByte(); // U + protectionMode = inStr.ReadByte(); // protection mode + break; + + default: + throw new UnsupportedPacketVersionException($"Invalid S2K type: {type}"); } + } + /// Constructs a specifier for a simple S2K generation + /// the digest algorithm to use. public S2k( HashAlgorithmTag algorithm) { - this.type = 0; + this.type = Simple; this.algorithm = algorithm; } + /// Constructs a specifier for a salted S2K generation + /// the digest algorithm to use. + /// the salt to apply to input to the key generation public S2k( HashAlgorithmTag algorithm, byte[] iv) { - this.type = 1; + this.type = Salted; this.algorithm = algorithm; - this.iv = iv; + this.iv = Arrays.Clone(iv); } + /// Constructs a specifier for a salted and iterated S2K generation + /// the digest algorithm to iterate. + /// the salt to apply to input to the key generation + /// the single byte iteration count specifier public S2k( HashAlgorithmTag algorithm, byte[] iv, int itCount) { - this.type = 3; + this.type = SaltedAndIterated; this.algorithm = algorithm; - this.iv = iv; + this.iv = Arrays.Clone(iv); this.itCount = itCount; } + /// Constructs a specifier for an S2K method using Argon2 + public S2k(byte[] salt, int passes, int parallelism, int memorySizeExponent) + { + this.type = Argon2; + this.iv = Arrays.Clone(salt); + this.passes = passes; + this.parallelism = parallelism; + this.memorySizeExponent = memorySizeExponent; + } + public virtual int Type { get { return type; } @@ -114,30 +152,66 @@ public virtual int ProtectionMode get { return protectionMode; } } + /// The number of passes - only if Argon2 + public int Passes + { + get { return passes; } + } + + /// The degree of parallelism - only if Argon2 + public int Parallelism + { + get { return parallelism; } + } + + /// The memory size exponent - only if Argon2 + public int MemorySizeExponent + { + get { return memorySizeExponent; } + } + public override void Encode( BcpgOutputStream bcpgOut) { - bcpgOut.WriteByte((byte) type); - bcpgOut.WriteByte((byte) algorithm); - - if (type != GnuDummyS2K) + switch (type) { - if (type != 0) - { + case Simple: + bcpgOut.WriteByte((byte)type); + bcpgOut.WriteByte((byte)algorithm); + break; + + case Salted: + bcpgOut.WriteByte((byte)type); + bcpgOut.WriteByte((byte)algorithm); bcpgOut.Write(iv); - } + break; - if (type == 3) - { - bcpgOut.WriteByte((byte) itCount); - } - } - else - { - bcpgOut.WriteByte((byte) 'G'); - bcpgOut.WriteByte((byte) 'N'); - bcpgOut.WriteByte((byte) 'U'); - bcpgOut.WriteByte((byte) protectionMode); + case SaltedAndIterated: + bcpgOut.WriteByte((byte)type); + bcpgOut.WriteByte((byte)algorithm); + bcpgOut.Write(iv); + bcpgOut.WriteByte((byte)itCount); + break; + + case Argon2: + bcpgOut.WriteByte((byte)type); + bcpgOut.Write(iv); + bcpgOut.WriteByte((byte)passes); + bcpgOut.WriteByte((byte)parallelism); + bcpgOut.WriteByte((byte)memorySizeExponent); + break; + + case GnuDummyS2K: + bcpgOut.WriteByte((byte)type); + bcpgOut.WriteByte((byte)algorithm); + bcpgOut.WriteByte((byte)'G'); + bcpgOut.WriteByte((byte)'N'); + bcpgOut.WriteByte((byte)'U'); + bcpgOut.WriteByte((byte)protectionMode); + break; + + default: + throw new InvalidOperationException($"Unknown S2K type {type}"); } } } diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 5e3382346..e89e82e36 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -257,6 +257,45 @@ public void Version6UnlockedSecretKeyParsingTest() } } + [Test] + public void Version6LockedSecretKeyParsingTest() + { + /* + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-locked-v6-secret-key + * The same secret key as in Version6UnlockedSecretKeyParsingTest, but the secret key + * material is locked with a passphrase using AEAD and Argon2. + * + * AEAD/Argon passphrase decryption is not implemented yet, so we just test + * parsing and encoding + */ + + PgpSecretKeyRing secretKeyRing = new PgpSecretKeyRing(v6LockedSecretKey); + PgpSecretKey[] secretKeys = secretKeyRing.GetSecretKeys().ToArray(); + IsEquals("wrong number of secret keys", secretKeys.Length, 2); + + // signing key + PgpSecretKey signingKey = secretKeys[0]; + IsEquals(signingKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.Ed25519); + IsEquals((ulong)signingKey.PublicKey.KeyId, 0xCB186C4F0609A697); + + // encryption key + PgpSecretKey encryptionKey = secretKeys[1]; + IsEquals(encryptionKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X25519); + IsEquals(encryptionKey.PublicKey.KeyId, 0x12C83F1E706F6308); + + // Encode test + using (MemoryStream ms = new MemoryStream()) + { + using (BcpgOutputStream bs = new BcpgOutputStream(ms, newFormatOnly: true)) + { + secretKeyRing.Encode(bs); + } + + byte[] encoded = ms.ToArray(); + IsTrue(AreEqual(encoded, v6LockedSecretKey)); + } + } + [Test] public void Version6SampleCleartextSignedMessageVerifySignatureTest() { @@ -324,9 +363,12 @@ public override void PerformTest() { Version4Ed25519LegacyPubkeySampleTest(); Version4Ed25519LegacySignatureSampleTest(); + Version4Ed25519LegacyCreateTest(); + Version6CertificateParsingTest(); Version6PublicKeyCreationTest(); Version6UnlockedSecretKeyParsingTest(); + Version6LockedSecretKeyParsingTest(); Version6SampleCleartextSignedMessageVerifySignatureTest(); } } From 244eaa02aa556e82345b87fca3913fbece601e14 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 22 Feb 2024 17:55:23 +0100 Subject: [PATCH 09/37] Packet criticality --- crypto/src/bcpg/AeadEncDataPacket.cs | 4 +- crypto/src/bcpg/BcpgInputStream.cs | 10 ++-- crypto/src/bcpg/CompressedDataPacket.cs | 2 +- crypto/src/bcpg/ContainedPacket.cs | 6 +- crypto/src/bcpg/ExperimentalPacket.cs | 7 +-- crypto/src/bcpg/InputStreamPacket.cs | 11 +++- crypto/src/bcpg/LiteralDataPacket.cs | 8 +-- crypto/src/bcpg/MarkerPacket.cs | 1 + crypto/src/bcpg/ModDetectionCodePacket.cs | 6 +- crypto/src/bcpg/OnePassSignaturePacket.cs | 16 ++--- crypto/src/bcpg/Packet.cs | 30 +++++++++- crypto/src/bcpg/PaddingPacket.cs | 3 + crypto/src/bcpg/PublicKeyEncSessionPacket.cs | 12 ++-- crypto/src/bcpg/PublicKeyPacket.cs | 21 ++++++- crypto/src/bcpg/PublicSubkeyPacket.cs | 4 +- crypto/src/bcpg/SecretKeyPacket.cs | 59 ++++++++++++++++--- crypto/src/bcpg/SecretSubkeyPacket.cs | 8 +-- crypto/src/bcpg/SignaturePacket.cs | 2 + crypto/src/bcpg/SymmetricEncDataPacket.cs | 2 +- .../src/bcpg/SymmetricEncIntegrityPacket.cs | 2 +- .../src/bcpg/SymmetricKeyEncSessionPacket.cs | 8 ++- crypto/src/bcpg/TrustPacket.cs | 10 ++-- crypto/src/bcpg/UserAttributePacket.cs | 2 + crypto/src/bcpg/UserIdPacket.cs | 3 + .../src/openpgp/PgpEncryptedDataGenerator.cs | 15 +++-- 25 files changed, 191 insertions(+), 61 deletions(-) diff --git a/crypto/src/bcpg/AeadEncDataPacket.cs b/crypto/src/bcpg/AeadEncDataPacket.cs index 2c7ec97f6..61368904b 100644 --- a/crypto/src/bcpg/AeadEncDataPacket.cs +++ b/crypto/src/bcpg/AeadEncDataPacket.cs @@ -21,7 +21,7 @@ public class AeadEncDataPacket private readonly byte[] m_iv; public AeadEncDataPacket(BcpgInputStream bcpgIn) - : base(bcpgIn) + : base(bcpgIn, PacketTag.ReservedAeadEncryptedData) { m_version = (byte)bcpgIn.ReadByte(); if (m_version != 1) @@ -37,7 +37,7 @@ public AeadEncDataPacket(BcpgInputStream bcpgIn) public AeadEncDataPacket(SymmetricKeyAlgorithmTag algorithm, AeadAlgorithmTag aeadAlgorithm, int chunkSize, byte[] iv) - : base(null) + : base(null, PacketTag.ReservedAeadEncryptedData) { m_version = 1; m_algorithm = algorithm; diff --git a/crypto/src/bcpg/BcpgInputStream.cs b/crypto/src/bcpg/BcpgInputStream.cs index efd258c9d..c118a26e1 100644 --- a/crypto/src/bcpg/BcpgInputStream.cs +++ b/crypto/src/bcpg/BcpgInputStream.cs @@ -9,7 +9,7 @@ namespace Org.BouncyCastle.Bcpg public class BcpgInputStream : BaseInputStream { - private Stream m_in; + private readonly Stream m_in; private bool next = false; private int nextB; @@ -135,7 +135,7 @@ public Packet ReadPacket() } bool newPacket = (hdr & 0x40) != 0; - PacketTag tag = 0; + PacketTag tag = PacketTag.Reserved; int bodyLen = 0; bool partial = false; @@ -206,7 +206,7 @@ public Packet ReadPacket() switch (tag) { case PacketTag.Reserved: - return new InputStreamPacket(objStream); + return new InputStreamPacket(objStream, PacketTag.Reserved); case PacketTag.PublicKeyEncryptedSession: return new PublicKeyEncSessionPacket(objStream); case PacketTag.Signature: @@ -241,6 +241,8 @@ public Packet ReadPacket() return new SymmetricEncIntegrityPacket(objStream); case PacketTag.ModificationDetectionCode: return new ModDetectionCodePacket(objStream); + case PacketTag.ReservedAeadEncryptedData: + return new AeadEncDataPacket(objStream); case PacketTag.Padding: return new PaddingPacket(objStream); case PacketTag.Experimental1: @@ -285,7 +287,7 @@ protected override void Dispose(bool disposing) private class PartialInputStream : BaseInputStream { - private BcpgInputStream m_in; + private readonly BcpgInputStream m_in; private bool partial; private int dataLength; diff --git a/crypto/src/bcpg/CompressedDataPacket.cs b/crypto/src/bcpg/CompressedDataPacket.cs index 2432825eb..dbebed125 100644 --- a/crypto/src/bcpg/CompressedDataPacket.cs +++ b/crypto/src/bcpg/CompressedDataPacket.cs @@ -10,7 +10,7 @@ public class CompressedDataPacket internal CompressedDataPacket( BcpgInputStream bcpgIn) - : base(bcpgIn) + : base(bcpgIn, PacketTag.CompressedData) { this.algorithm = (CompressionAlgorithmTag) bcpgIn.ReadByte(); } diff --git a/crypto/src/bcpg/ContainedPacket.cs b/crypto/src/bcpg/ContainedPacket.cs index ca39fd985..812e36aa6 100644 --- a/crypto/src/bcpg/ContainedPacket.cs +++ b/crypto/src/bcpg/ContainedPacket.cs @@ -1,4 +1,3 @@ -using System; using System.IO; namespace Org.BouncyCastle.Bcpg @@ -7,6 +6,11 @@ namespace Org.BouncyCastle.Bcpg public abstract class ContainedPacket : Packet { + protected ContainedPacket(PacketTag packetTag) + : base(packetTag) + { + } + public byte[] GetEncoded() { MemoryStream bOut = new MemoryStream(); diff --git a/crypto/src/bcpg/ExperimentalPacket.cs b/crypto/src/bcpg/ExperimentalPacket.cs index 3d413db09..dea370b59 100644 --- a/crypto/src/bcpg/ExperimentalPacket.cs +++ b/crypto/src/bcpg/ExperimentalPacket.cs @@ -7,22 +7,19 @@ namespace Org.BouncyCastle.Bcpg public class ExperimentalPacket : ContainedPacket { - private readonly PacketTag m_tag; private readonly byte[] m_contents; internal ExperimentalPacket(PacketTag tag, BcpgInputStream bcpgIn) + :base(tag) { - m_tag = tag; m_contents = bcpgIn.ReadAll(); } - public PacketTag Tag => m_tag; - public byte[] GetContents() => (byte[])m_contents.Clone(); public override void Encode(BcpgOutputStream bcpgOut) { - bcpgOut.WritePacket(m_tag, m_contents); + bcpgOut.WritePacket(Tag, m_contents); } } } diff --git a/crypto/src/bcpg/InputStreamPacket.cs b/crypto/src/bcpg/InputStreamPacket.cs index d4946ebf2..97d61e528 100644 --- a/crypto/src/bcpg/InputStreamPacket.cs +++ b/crypto/src/bcpg/InputStreamPacket.cs @@ -5,7 +5,16 @@ public class InputStreamPacket { private readonly BcpgInputStream bcpgIn; - public InputStreamPacket(BcpgInputStream bcpgIn) + // for API backward compatibility + // it's unlikely this is being used, but just in case we'll mark + // unknown inputs as reserved. + public InputStreamPacket(BcpgInputStream bcpgIn) + : this(bcpgIn, PacketTag.Reserved) + { + } + + public InputStreamPacket(BcpgInputStream bcpgIn, PacketTag packetTag) + :base(packetTag) { this.bcpgIn = bcpgIn; } diff --git a/crypto/src/bcpg/LiteralDataPacket.cs b/crypto/src/bcpg/LiteralDataPacket.cs index b4d28a201..aac423a3f 100644 --- a/crypto/src/bcpg/LiteralDataPacket.cs +++ b/crypto/src/bcpg/LiteralDataPacket.cs @@ -9,13 +9,13 @@ namespace Org.BouncyCastle.Bcpg public class LiteralDataPacket : InputStreamPacket { - private int format; - private byte[] fileName; - private long modDate; + private readonly int format; + private readonly byte[] fileName; + private readonly long modDate; internal LiteralDataPacket( BcpgInputStream bcpgIn) - : base(bcpgIn) + : base(bcpgIn, PacketTag.LiteralData) { format = bcpgIn.ReadByte(); int len = bcpgIn.ReadByte(); diff --git a/crypto/src/bcpg/MarkerPacket.cs b/crypto/src/bcpg/MarkerPacket.cs index f2c450793..b5a8e9639 100644 --- a/crypto/src/bcpg/MarkerPacket.cs +++ b/crypto/src/bcpg/MarkerPacket.cs @@ -10,6 +10,7 @@ public class MarkerPacket private readonly byte[] marker = { (byte)0x50, (byte)0x47, (byte)0x50 }; public MarkerPacket(BcpgInputStream bcpgIn) + :base(PacketTag.Marker) { bcpgIn.ReadFully(marker); } diff --git a/crypto/src/bcpg/ModDetectionCodePacket.cs b/crypto/src/bcpg/ModDetectionCodePacket.cs index ae8283aef..56cd8d7ca 100644 --- a/crypto/src/bcpg/ModDetectionCodePacket.cs +++ b/crypto/src/bcpg/ModDetectionCodePacket.cs @@ -11,9 +11,10 @@ public class ModDetectionCodePacket internal ModDetectionCodePacket( BcpgInputStream bcpgIn) + :base(PacketTag.ModificationDetectionCode) { if (bcpgIn == null) - throw new ArgumentNullException("bcpgIn"); + throw new ArgumentNullException(nameof(bcpgIn)); this.digest = new byte[20]; bcpgIn.ReadFully(this.digest); @@ -21,9 +22,10 @@ internal ModDetectionCodePacket( public ModDetectionCodePacket( byte[] digest) + : base(PacketTag.ModificationDetectionCode) { if (digest == null) - throw new ArgumentNullException("digest"); + throw new ArgumentNullException(nameof(digest)); this.digest = (byte[]) digest.Clone(); } diff --git a/crypto/src/bcpg/OnePassSignaturePacket.cs b/crypto/src/bcpg/OnePassSignaturePacket.cs index 4efae1d63..161e84357 100644 --- a/crypto/src/bcpg/OnePassSignaturePacket.cs +++ b/crypto/src/bcpg/OnePassSignaturePacket.cs @@ -7,15 +7,16 @@ namespace Org.BouncyCastle.Bcpg public class OnePassSignaturePacket : ContainedPacket { - private int version; - private int sigType; - private HashAlgorithmTag hashAlgorithm; - private PublicKeyAlgorithmTag keyAlgorithm; - private long keyId; - private int nested; + private readonly int version; + private readonly int sigType; + private readonly HashAlgorithmTag hashAlgorithm; + private readonly PublicKeyAlgorithmTag keyAlgorithm; + private readonly long keyId; + private readonly int nested; internal OnePassSignaturePacket( BcpgInputStream bcpgIn) + :base(PacketTag.OnePassSignature) { version = bcpgIn.ReadByte(); sigType = bcpgIn.ReadByte(); @@ -40,7 +41,8 @@ public OnePassSignaturePacket( PublicKeyAlgorithmTag keyAlgorithm, long keyId, bool isNested) - { + : base(PacketTag.OnePassSignature) + { this.version = 3; this.sigType = sigType; this.hashAlgorithm = hashAlgorithm; diff --git a/crypto/src/bcpg/Packet.cs b/crypto/src/bcpg/Packet.cs index 83f6d1f74..a3f84fbf7 100644 --- a/crypto/src/bcpg/Packet.cs +++ b/crypto/src/bcpg/Packet.cs @@ -1,7 +1,35 @@ +using System; + namespace Org.BouncyCastle.Bcpg { public class Packet //: PacketTag { + private readonly PacketTag packetTag; + + // for API backward compatibility + // it's unlikely this is being used, but just in case we'll mark + // unknown inputs as reserved. + public Packet() + : this(PacketTag.Reserved) + { + } + + public Packet(PacketTag packetTag) + { + this.packetTag = packetTag; + } + + public PacketTag Tag => packetTag; + + /// + /// Returns whether the packet is to be considered critical for v6 implementations. + /// * Packets with tags less or equal to 39 are critical. + /// * Tags 40 to 59 are reserved for unassigned, non-critical packets. + /// * Tags 60 to 63 are non-critical private or experimental packets. + /// + /// + public bool IsCritical => (int)Tag <= 39; } -} + +} \ No newline at end of file diff --git a/crypto/src/bcpg/PaddingPacket.cs b/crypto/src/bcpg/PaddingPacket.cs index 4063c8a16..d91ed9b28 100644 --- a/crypto/src/bcpg/PaddingPacket.cs +++ b/crypto/src/bcpg/PaddingPacket.cs @@ -9,17 +9,20 @@ public class PaddingPacket private readonly byte[] padding; public PaddingPacket(BcpgInputStream bcpgIn) + :base(PacketTag.Padding) { padding = bcpgIn.ReadAll(); } public PaddingPacket(int length, BcpgInputStream bcpgIn) + : base(PacketTag.Padding) { padding = new byte[length]; bcpgIn.ReadFully(padding); } public PaddingPacket(byte[] padding) + : base(PacketTag.Padding) { this.padding = Arrays.Clone(padding); } diff --git a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs index 25d297890..b38b19bd5 100644 --- a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs @@ -11,13 +11,14 @@ namespace Org.BouncyCastle.Bcpg public class PublicKeyEncSessionPacket : ContainedPacket //, PublicKeyAlgorithmTag { - private int version; - private long keyId; - private PublicKeyAlgorithmTag algorithm; - private byte[][] data; + private readonly int version; + private readonly long keyId; + private readonly PublicKeyAlgorithmTag algorithm; + private readonly byte[][] data; internal PublicKeyEncSessionPacket( BcpgInputStream bcpgIn) + :base(PacketTag.PublicKeyEncryptedSession) { version = bcpgIn.ReadByte(); @@ -59,7 +60,8 @@ public PublicKeyEncSessionPacket( long keyId, PublicKeyAlgorithmTag algorithm, byte[][] data) - { + : base(PacketTag.PublicKeyEncryptedSession) + { this.version = 3; this.keyId = keyId; this.algorithm = algorithm; diff --git a/crypto/src/bcpg/PublicKeyPacket.cs b/crypto/src/bcpg/PublicKeyPacket.cs index 6449c03f4..9d6d16839 100644 --- a/crypto/src/bcpg/PublicKeyPacket.cs +++ b/crypto/src/bcpg/PublicKeyPacket.cs @@ -25,8 +25,15 @@ public class PublicKeyPacket private readonly long v6KeyLen; - internal PublicKeyPacket( - BcpgInputStream bcpgIn) + internal PublicKeyPacket(BcpgInputStream bcpgIn) + : this(bcpgIn, PacketTag.PublicKey) + { + } + + protected PublicKeyPacket( + BcpgInputStream bcpgIn, + PacketTag tag) + : base(tag) { version = bcpgIn.ReadByte(); @@ -93,6 +100,16 @@ public PublicKeyPacket( PublicKeyAlgorithmTag algorithm, DateTime time, IBcpgKey key) + : this(version, algorithm, time, key, PacketTag.PublicKey) + { } + + protected PublicKeyPacket( + int version, + PublicKeyAlgorithmTag algorithm, + DateTime time, + IBcpgKey key, + PacketTag tag) + : base(tag) { this.version = version; this.time = DateTimeUtilities.DateTimeToUnixMs(time) / 1000L; diff --git a/crypto/src/bcpg/PublicSubkeyPacket.cs b/crypto/src/bcpg/PublicSubkeyPacket.cs index b9c7d73f5..0e70790a1 100644 --- a/crypto/src/bcpg/PublicSubkeyPacket.cs +++ b/crypto/src/bcpg/PublicSubkeyPacket.cs @@ -19,7 +19,7 @@ public PublicSubkeyPacket( PublicKeyAlgorithmTag algorithm, DateTime time, IBcpgKey key) - : base(version, algorithm, time, key) + : base(version, algorithm, time, key, PacketTag.PublicSubkey) { } @@ -28,7 +28,7 @@ public PublicSubkeyPacket( PublicKeyAlgorithmTag algorithm, DateTime time, IBcpgKey key) - : base(DefaultVersion, algorithm, time, key) + : base(DefaultVersion, algorithm, time, key, PacketTag.PublicSubkey) { } diff --git a/crypto/src/bcpg/SecretKeyPacket.cs b/crypto/src/bcpg/SecretKeyPacket.cs index 7e825c945..d237ef469 100644 --- a/crypto/src/bcpg/SecretKeyPacket.cs +++ b/crypto/src/bcpg/SecretKeyPacket.cs @@ -71,8 +71,14 @@ public class SecretKeyPacket private bool HasS2KSpecifier => (s2kUsage == UsageChecksum || s2kUsage == UsageSha1 || s2kUsage == UsageAead); - internal SecretKeyPacket( - BcpgInputStream bcpgIn) + internal SecretKeyPacket(BcpgInputStream bcpgIn) + : this(bcpgIn, PacketTag.SecretKey) + { + } + + protected SecretKeyPacket( + BcpgInputStream bcpgIn, PacketTag tag) + :base(tag) { if (this is SecretSubkeyPacket) { @@ -144,12 +150,25 @@ internal SecretKeyPacket( secKeyData = bcpgIn.ReadAll(); } - public SecretKeyPacket( + + public SecretKeyPacket( + PublicKeyPacket pubKeyPacket, + SymmetricKeyAlgorithmTag encAlgorithm, + S2k s2k, + byte[] iv, + byte[] secKeyData) + : this(pubKeyPacket, encAlgorithm, s2k, iv, secKeyData, PacketTag.SecretKey) + { + } + + protected SecretKeyPacket( PublicKeyPacket pubKeyPacket, SymmetricKeyAlgorithmTag encAlgorithm, S2k s2k, byte[] iv, - byte[] secKeyData) + byte[] secKeyData, + PacketTag tag) + :base(tag) { this.pubKeyPacket = pubKeyPacket; this.encAlgorithm = encAlgorithm; @@ -168,13 +187,26 @@ public SecretKeyPacket( this.secKeyData = secKeyData; } - public SecretKeyPacket( + public SecretKeyPacket( + PublicKeyPacket pubKeyPacket, + SymmetricKeyAlgorithmTag encAlgorithm, + int s2kUsage, + S2k s2k, + byte[] iv, + byte[] secKeyData) + : this(pubKeyPacket, encAlgorithm, s2kUsage, s2k, iv, secKeyData, PacketTag.SecretKey) + { + } + + protected SecretKeyPacket( PublicKeyPacket pubKeyPacket, SymmetricKeyAlgorithmTag encAlgorithm, int s2kUsage, S2k s2k, byte[] iv, - byte[] secKeyData) + byte[] secKeyData, + PacketTag tag) + :base(tag) { this.pubKeyPacket = pubKeyPacket; this.encAlgorithm = encAlgorithm; @@ -184,7 +216,6 @@ public SecretKeyPacket( this.secKeyData = secKeyData; } - public SecretKeyPacket( PublicKeyPacket pubKeyPacket, SymmetricKeyAlgorithmTag encAlgorithm, @@ -193,6 +224,20 @@ public SecretKeyPacket( S2k s2k, byte[] iv, byte[] secKeyData) + : this(pubKeyPacket, encAlgorithm, aeadAlgorithm, s2kUsage, s2k, iv, secKeyData, PacketTag.SecretKey) + { + } + + protected SecretKeyPacket( + PublicKeyPacket pubKeyPacket, + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, + int s2kUsage, + S2k s2k, + byte[] iv, + byte[] secKeyData, + PacketTag tag) + :base(tag) { this.pubKeyPacket = pubKeyPacket; this.encAlgorithm = encAlgorithm; diff --git a/crypto/src/bcpg/SecretSubkeyPacket.cs b/crypto/src/bcpg/SecretSubkeyPacket.cs index fd05f81b8..5fb249a25 100644 --- a/crypto/src/bcpg/SecretSubkeyPacket.cs +++ b/crypto/src/bcpg/SecretSubkeyPacket.cs @@ -9,7 +9,7 @@ public class SecretSubkeyPacket { internal SecretSubkeyPacket( BcpgInputStream bcpgIn) - : base(bcpgIn) + : base(bcpgIn, PacketTag.SecretSubkey) { } @@ -19,7 +19,7 @@ public SecretSubkeyPacket( S2k s2k, byte[] iv, byte[] secKeyData) - : base(pubKeyPacket, encAlgorithm, s2k, iv, secKeyData) + : base(pubKeyPacket, encAlgorithm, s2k, iv, secKeyData, PacketTag.SecretSubkey) { } @@ -30,7 +30,7 @@ public SecretSubkeyPacket( S2k s2k, byte[] iv, byte[] secKeyData) - : base(pubKeyPacket, encAlgorithm, s2kUsage, s2k, iv, secKeyData) + : base(pubKeyPacket, encAlgorithm, s2kUsage, s2k, iv, secKeyData, PacketTag.SecretSubkey) { } @@ -42,7 +42,7 @@ public SecretSubkeyPacket( S2k s2k, byte[] iv, byte[] secKeyData) - :base(pubKeyPacket, encAlgorithm, aeadAlgorithm, s2kUsage, s2k, iv, secKeyData) + :base(pubKeyPacket, encAlgorithm, aeadAlgorithm, s2kUsage, s2k, iv, secKeyData, PacketTag.SecretSubkey) { } diff --git a/crypto/src/bcpg/SignaturePacket.cs b/crypto/src/bcpg/SignaturePacket.cs index 4fcd27be1..45f9749a2 100644 --- a/crypto/src/bcpg/SignaturePacket.cs +++ b/crypto/src/bcpg/SignaturePacket.cs @@ -36,6 +36,7 @@ public class SignaturePacket private readonly byte[] salt; internal SignaturePacket(BcpgInputStream bcpgIn) + :base(PacketTag.Signature) { version = bcpgIn.ReadByte(); @@ -282,6 +283,7 @@ public SignaturePacket( SignatureSubpacket[] unhashedData, byte[] fingerprint, MPInteger[] signature) + : base(PacketTag.Signature) { this.version = version; this.signatureType = signatureType; diff --git a/crypto/src/bcpg/SymmetricEncDataPacket.cs b/crypto/src/bcpg/SymmetricEncDataPacket.cs index 17ee55bb7..013a7fd39 100644 --- a/crypto/src/bcpg/SymmetricEncDataPacket.cs +++ b/crypto/src/bcpg/SymmetricEncDataPacket.cs @@ -8,7 +8,7 @@ public class SymmetricEncDataPacket { public SymmetricEncDataPacket( BcpgInputStream bcpgIn) - : base(bcpgIn) + : base(bcpgIn, PacketTag.SymmetricKeyEncrypted) { } } diff --git a/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs b/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs index a9b6d0678..1de72a4a0 100644 --- a/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs +++ b/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs @@ -10,7 +10,7 @@ public class SymmetricEncIntegrityPacket internal SymmetricEncIntegrityPacket( BcpgInputStream bcpgIn) - : base(bcpgIn) + : base(bcpgIn, PacketTag.SymmetricEncryptedIntegrityProtected) { version = bcpgIn.ReadByte(); } diff --git a/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs b/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs index 901088e33..f053b431a 100644 --- a/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs @@ -9,13 +9,14 @@ namespace Org.BouncyCastle.Bcpg public class SymmetricKeyEncSessionPacket : ContainedPacket { - private int version; - private SymmetricKeyAlgorithmTag encAlgorithm; - private S2k s2k; + private readonly int version; + private readonly SymmetricKeyAlgorithmTag encAlgorithm; + private readonly S2k s2k; private readonly byte[] secKeyData; public SymmetricKeyEncSessionPacket( BcpgInputStream bcpgIn) + :base(PacketTag.SymmetricKeyEncryptedSessionKey) { version = bcpgIn.ReadByte(); encAlgorithm = (SymmetricKeyAlgorithmTag) bcpgIn.ReadByte(); @@ -29,6 +30,7 @@ public SymmetricKeyEncSessionPacket( SymmetricKeyAlgorithmTag encAlgorithm, S2k s2k, byte[] secKeyData) + : base(PacketTag.SymmetricKeyEncryptedSessionKey) { this.version = 4; this.encAlgorithm = encAlgorithm; diff --git a/crypto/src/bcpg/TrustPacket.cs b/crypto/src/bcpg/TrustPacket.cs index a2a30177f..1dd02f7f3 100644 --- a/crypto/src/bcpg/TrustPacket.cs +++ b/crypto/src/bcpg/TrustPacket.cs @@ -10,19 +10,21 @@ public class TrustPacket private readonly byte[] levelAndTrustAmount; public TrustPacket(BcpgInputStream bcpgIn) + :base(PacketTag.Trust) { MemoryStream bOut = new MemoryStream(); - - int ch; + + int ch; while ((ch = bcpgIn.ReadByte()) >= 0) { bOut.WriteByte((byte) ch); } - - levelAndTrustAmount = bOut.ToArray(); + + levelAndTrustAmount = bOut.ToArray(); } public TrustPacket(int trustCode) + : base(PacketTag.Trust) { this.levelAndTrustAmount = new byte[]{ (byte)trustCode }; } diff --git a/crypto/src/bcpg/UserAttributePacket.cs b/crypto/src/bcpg/UserAttributePacket.cs index e976f1215..8d1261f70 100644 --- a/crypto/src/bcpg/UserAttributePacket.cs +++ b/crypto/src/bcpg/UserAttributePacket.cs @@ -13,6 +13,7 @@ public class UserAttributePacket public UserAttributePacket( BcpgInputStream bcpgIn) + :base(PacketTag.UserAttribute) { UserAttributeSubpacketsParser sIn = new UserAttributeSubpacketsParser(bcpgIn); UserAttributeSubpacket sub; @@ -28,6 +29,7 @@ public UserAttributePacket( public UserAttributePacket( UserAttributeSubpacket[] subpackets) + : base(PacketTag.UserAttribute) { this.subpackets = subpackets; } diff --git a/crypto/src/bcpg/UserIdPacket.cs b/crypto/src/bcpg/UserIdPacket.cs index 7d3d3a846..86ae6d7b3 100644 --- a/crypto/src/bcpg/UserIdPacket.cs +++ b/crypto/src/bcpg/UserIdPacket.cs @@ -14,16 +14,19 @@ public class UserIdPacket private readonly byte[] idData; public UserIdPacket(BcpgInputStream bcpgIn) + :base(PacketTag.UserId) { this.idData = bcpgIn.ReadAll(); } public UserIdPacket(string id) + : base(PacketTag.UserId) { this.idData = Encoding.UTF8.GetBytes(id); } public UserIdPacket(byte[] rawId) + : base(PacketTag.UserId) { this.idData = Arrays.Clone(rawId); } diff --git a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs index ac847ddb6..13648d1e8 100644 --- a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs +++ b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs @@ -23,8 +23,8 @@ public class PgpEncryptedDataGenerator private BcpgOutputStream pOut; private CipherStream cOut; private IBufferedCipher c; - private bool withIntegrityPacket; - private bool oldFormat; + private readonly bool withIntegrityPacket; + private readonly bool oldFormat; private DigestStream digestOut; private abstract class EncMethod @@ -34,18 +34,24 @@ private abstract class EncMethod protected SymmetricKeyAlgorithmTag encAlgorithm; protected KeyParameter key; - public abstract void AddSessionInfo(byte[] si, SecureRandom random); + protected EncMethod(PacketTag packetTag) + : base(packetTag) + { + } + + public abstract void AddSessionInfo(byte[] si, SecureRandom random); } private class PbeMethod : EncMethod { - private S2k s2k; + private readonly S2k s2k; internal PbeMethod( SymmetricKeyAlgorithmTag encAlgorithm, S2k s2k, KeyParameter key) + : base(PacketTag.SymmetricKeyEncryptedSessionKey) { this.encAlgorithm = encAlgorithm; this.s2k = s2k; @@ -87,6 +93,7 @@ private class PubMethod internal byte[][] data; internal PubMethod(PgpPublicKey pubKey, bool sessionKeyObfuscation) + : base(PacketTag.PublicKeyEncryptedSession) { this.pubKey = pubKey; this.sessionKeyObfuscation = sessionKeyObfuscation; From 683e76f3efe7e34f9b7a182c8b276f415221ebde Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 22 Feb 2024 19:04:42 +0100 Subject: [PATCH 10/37] verify v6 key binding signatures --- crypto/src/openpgp/PgpPublicKey.cs | 47 ++++++++++++++----- crypto/src/openpgp/PgpSignature.cs | 26 ++++++++-- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 14 ++++-- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/crypto/src/openpgp/PgpPublicKey.cs b/crypto/src/openpgp/PgpPublicKey.cs index a2689d4e5..43fe698e4 100644 --- a/crypto/src/openpgp/PgpPublicKey.cs +++ b/crypto/src/openpgp/PgpPublicKey.cs @@ -30,6 +30,37 @@ public class PgpPublicKey private const byte v5FingerprintPreamble = 0x9A; private const byte v6FingerprintPreamble = 0x9B; + internal static byte FingerprintPreamble(int version) + { + switch (version) + { + case PublicKeyPacket.Version4: + return v4FingerprintPreamble; + case PublicKeyPacket.Version5: + return v5FingerprintPreamble; + case PublicKeyPacket.Version6: + return v6FingerprintPreamble; + default: + throw new PgpException($"unsupported OpenPGP key packet version: {version}"); + } + } + private static IDigest CreateDigestForFingerprint(int version) + { + switch (version) + { + case PublicKeyPacket.Version2: + case PublicKeyPacket.Version3: + return PgpUtilities.CreateDigest(HashAlgorithmTag.MD5); + case PublicKeyPacket.Version4: + return PgpUtilities.CreateDigest(HashAlgorithmTag.Sha1); + case PublicKeyPacket.Version5: + case PublicKeyPacket.Version6: + return PgpUtilities.CreateDigest(HashAlgorithmTag.Sha256); + default: + throw new PgpException($"unsupported OpenPGP key packet version: {version}"); + } + } + // We default to these as they are specified as mandatory in RFC 6631. private static readonly PgpKdfParameters DefaultKdfParameters = new PgpKdfParameters(HashAlgorithmTag.Sha256, SymmetricKeyAlgorithmTag.Aes128); @@ -37,7 +68,7 @@ public class PgpPublicKey public static byte[] CalculateFingerprint(PublicKeyPacket publicPk) { IBcpgKey key = publicPk.Key; - IDigest digest; + IDigest digest = CreateDigestForFingerprint(publicPk.Version); if (publicPk.Version <= PublicKeyPacket.Version3) { @@ -45,8 +76,6 @@ public static byte[] CalculateFingerprint(PublicKeyPacket publicPk) try { - digest = PgpUtilities.CreateDigest(HashAlgorithmTag.MD5); - UpdateDigest(digest, rK.Modulus); UpdateDigest(digest, rK.PublicExponent); } @@ -59,21 +88,17 @@ public static byte[] CalculateFingerprint(PublicKeyPacket publicPk) { try { + digest.Update(FingerprintPreamble(publicPk.Version)); + byte[] kBytes = publicPk.GetEncodedContents(); if (publicPk.Version == PublicKeyPacket.Version4) { - digest = PgpUtilities.CreateDigest(HashAlgorithmTag.Sha1); - - digest.Update(v4FingerprintPreamble); digest.Update((byte)(kBytes.Length >> 8)); digest.Update((byte)kBytes.Length); } else if (publicPk.Version == PublicKeyPacket.Version5 || publicPk.Version == PublicKeyPacket.Version6) { - digest = PgpUtilities.CreateDigest(HashAlgorithmTag.Sha256); - - digest.Update(publicPk.Version == PublicKeyPacket.Version5 ? v5FingerprintPreamble : v6FingerprintPreamble); digest.Update((byte)(kBytes.Length >> 24)); digest.Update((byte)(kBytes.Length >> 16)); digest.Update((byte)(kBytes.Length >> 8)); @@ -81,7 +106,7 @@ public static byte[] CalculateFingerprint(PublicKeyPacket publicPk) } else { - throw new PgpException("unsupported OpenPGP key packet version: " + publicPk.Version); + throw new PgpException($"unsupported OpenPGP key packet version: {publicPk.Version}"); } digest.BlockUpdate(kBytes, 0, kBytes.Length); @@ -1390,4 +1415,4 @@ public static PgpPublicKey Join(PgpPublicKey key, PgpPublicKey copy, bool joinTr return merged; } } -} +} \ No newline at end of file diff --git a/crypto/src/openpgp/PgpSignature.cs b/crypto/src/openpgp/PgpSignature.cs index 4505c72c8..01d2a142e 100644 --- a/crypto/src/openpgp/PgpSignature.cs +++ b/crypto/src/openpgp/PgpSignature.cs @@ -228,10 +228,25 @@ private void UpdateWithPublicKey(PgpPublicKey key) { byte[] keyBytes = GetEncodedPublicKey(key); - this.Update( - (byte) 0x99, - (byte)(keyBytes.Length >> 8), - (byte)(keyBytes.Length)); + this.Update(PgpPublicKey.FingerprintPreamble(key.Version)); + + switch (key.Version) + { + case PublicKeyPacket.Version4: + this.Update( + (byte)(keyBytes.Length >> 8), + (byte)(keyBytes.Length)); + break; + case PublicKeyPacket.Version5: + case PublicKeyPacket.Version6: + this.Update( + (byte)(keyBytes.Length >> 24), + (byte)(keyBytes.Length >> 16), + (byte)(keyBytes.Length >> 8), + (byte)(keyBytes.Length)); + break; + } + this.Update(keyBytes); } @@ -306,7 +321,8 @@ public bool VerifyCertification( PgpPublicKey pubKey) { if (SignatureType != KeyRevocation - && SignatureType != SubkeyRevocation) + && SignatureType != SubkeyRevocation + && SignatureType != DirectKey) { throw new InvalidOperationException("signature is not a key signature"); } diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index e89e82e36..3ce1eccb1 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -157,7 +157,11 @@ public void Version6CertificateParsingTest() IsEquals((ulong)masterKey.KeyId, 0xCB186C4F0609A697); IsTrue("wrong master key fingerprint", AreEqual(masterKey.GetFingerprint(), expectedFingerprint)); - // TODO Verify self signatures + // Verify direct key self-signature + PgpSignature selfSig = masterKey.GetSignatures().First(); + IsTrue(selfSig.SignatureType == PgpSignature.DirectKey); + selfSig.InitVerify(masterKey); + FailIf("self signature verification failed", !selfSig.VerifyCertification(masterKey)); // subkey PgpPublicKey subKey = publicKeys[1]; @@ -167,7 +171,11 @@ public void Version6CertificateParsingTest() IsEquals(subKey.KeyId, 0x12C83F1E706F6308); IsTrue("wrong sub key fingerprint", AreEqual(subKey.GetFingerprint(), expectedFingerprint)); - // TODO Verify subkey binding signature + // Verify subkey binding signature + PgpSignature bindingSig = subKey.GetSignatures().First(); + IsTrue(bindingSig.SignatureType == PgpSignature.SubkeyBinding); + bindingSig.InitVerify(masterKey); + FailIf("subkey binding signature verification failed", !bindingSig.VerifyCertification(masterKey, subKey)); // Encode test using (MemoryStream ms = new MemoryStream()) @@ -188,7 +196,7 @@ public void Version6PublicKeyCreationTest() /* * Create a v6 Ed25519 pubkey with the same key material and creation datetime as the test vector * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans - * the check the fingerprint and verify a signature + * then check the fingerprint and verify a signature */ byte[] keyMaterial = Hex.Decode("f94da7bb48d60a61e567706a6587d0331999bb9d891a08242ead84543df895a3"); var key = new Ed25519PublicKeyParameters(keyMaterial); From 31050849ac786370d557d57cae8e032d8629ee74 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Fri, 23 Feb 2024 18:46:21 +0100 Subject: [PATCH 11/37] parse and verify v6 One-Pass Signatures --- crypto/src/bcpg/OnePassSignaturePacket.cs | 164 ++++++++++++++++++---- crypto/src/openpgp/PgpOnePassSignature.cs | 17 ++- 2 files changed, 153 insertions(+), 28 deletions(-) diff --git a/crypto/src/bcpg/OnePassSignaturePacket.cs b/crypto/src/bcpg/OnePassSignaturePacket.cs index 161e84357..cde5501b4 100644 --- a/crypto/src/bcpg/OnePassSignaturePacket.cs +++ b/crypto/src/bcpg/OnePassSignaturePacket.cs @@ -1,40 +1,91 @@ -using System; +using Org.BouncyCastle.Bcpg.OpenPgp; +using Org.BouncyCastle.Crypto.Utilities; +using Org.BouncyCastle.Utilities; using System.IO; namespace Org.BouncyCastle.Bcpg { - /// Generic signature object - public class OnePassSignaturePacket + /// Generic signature object + public class OnePassSignaturePacket : ContainedPacket { - private readonly int version; + public const int Version3 = 3; + public const int Version6 = 6; + + private readonly int version; private readonly int sigType; private readonly HashAlgorithmTag hashAlgorithm; - private readonly PublicKeyAlgorithmTag keyAlgorithm; - private readonly long keyId; + private readonly PublicKeyAlgorithmTag keyAlgorithm; + private readonly long keyId; private readonly int nested; - internal OnePassSignaturePacket( + // fields for V6 + private readonly byte[] salt; + private readonly byte[] fingerprint; + + private void EnforceConstraints() + { + int expectedSaltSize = PgpUtilities.GetSaltSize(hashAlgorithm); + + if (version == Version6 && salt.Length != expectedSaltSize) + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-one-pass-signature-packet-t + // The salt size MUST match the value defined for the hash algorithm as specified in Table 23 + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#hash-algorithms-registry + throw new IOException($"invalid salt size for v6 signature: expected {expectedSaltSize} got {salt.Length}"); + } + } + + internal OnePassSignaturePacket( BcpgInputStream bcpgIn) :base(PacketTag.OnePassSignature) { version = bcpgIn.ReadByte(); + if (version != Version3 && version != Version6) + { + throw new UnsupportedPacketVersionException($"unsupported OpenPGP One Pass Signature packet version: {version}"); + } + sigType = bcpgIn.ReadByte(); hashAlgorithm = (HashAlgorithmTag) bcpgIn.ReadByte(); keyAlgorithm = (PublicKeyAlgorithmTag) bcpgIn.ReadByte(); - keyId |= (long)bcpgIn.ReadByte() << 56; - keyId |= (long)bcpgIn.ReadByte() << 48; - keyId |= (long)bcpgIn.ReadByte() << 40; - keyId |= (long)bcpgIn.ReadByte() << 32; - keyId |= (long)bcpgIn.ReadByte() << 24; - keyId |= (long)bcpgIn.ReadByte() << 16; - keyId |= (long)bcpgIn.ReadByte() << 8; - keyId |= (uint)bcpgIn.ReadByte(); + if (version == Version3) + { + keyId |= (long)bcpgIn.ReadByte() << 56; + keyId |= (long)bcpgIn.ReadByte() << 48; + keyId |= (long)bcpgIn.ReadByte() << 40; + keyId |= (long)bcpgIn.ReadByte() << 32; + keyId |= (long)bcpgIn.ReadByte() << 24; + keyId |= (long)bcpgIn.ReadByte() << 16; + keyId |= (long)bcpgIn.ReadByte() << 8; + keyId |= (uint)bcpgIn.ReadByte(); + } + else + { + //Version 6 + int saltSize = bcpgIn.ReadByte(); + salt = new byte[saltSize]; + bcpgIn.ReadFully(salt); + + fingerprint = new byte[32]; + bcpgIn.ReadFully(fingerprint); + keyId = (long)Pack.BE_To_UInt64(fingerprint); + } nested = bcpgIn.ReadByte(); - } + EnforceConstraints(); + } + + /// + /// Create a Version 3 OPS Packet + /// + /// + /// + /// + /// + /// public OnePassSignaturePacket( int sigType, HashAlgorithmTag hashAlgorithm, @@ -43,15 +94,51 @@ public OnePassSignaturePacket( bool isNested) : base(PacketTag.OnePassSignature) { - this.version = 3; + this.version = Version3; this.sigType = sigType; this.hashAlgorithm = hashAlgorithm; this.keyAlgorithm = keyAlgorithm; this.keyId = keyId; this.nested = (isNested) ? 0 : 1; + + EnforceConstraints(); + } + + /// + /// Create a Version 6 OPS Packet + /// + /// + /// + /// + /// + /// + /// + public OnePassSignaturePacket( + int sigType, + HashAlgorithmTag hashAlgorithm, + PublicKeyAlgorithmTag keyAlgorithm, + byte[] salt, + byte[] fingerprint, + bool isNested) + : base(PacketTag.OnePassSignature) + { + this.version = Version6; + this.sigType = sigType; + this.hashAlgorithm = hashAlgorithm; + this.keyAlgorithm = keyAlgorithm; + this.salt = Arrays.Clone(salt); + this.fingerprint = Arrays.Clone(fingerprint); + this.keyId = (long)Pack.BE_To_UInt64(fingerprint); + this.nested = (isNested) ? 0 : 1; + + EnforceConstraints(); + } + + public int Version { + get { return version; } } - public int SignatureType + public int SignatureType { get { return sigType; } } @@ -73,17 +160,40 @@ public long KeyId get { return keyId; } } - public override void Encode(BcpgOutputStream bcpgOut) + public byte[] GetFingerprint() + { + return Arrays.Clone(fingerprint); + } + + public byte[] GetSignatureSalt() + { + return Arrays.Clone(salt); + } + + public override void Encode(BcpgOutputStream bcpgOut) { - MemoryStream bOut = new MemoryStream(); - using (var pOut = new BcpgOutputStream(bOut)) - { - pOut.Write((byte)version, (byte)sigType, (byte)hashAlgorithm, (byte)keyAlgorithm); - pOut.WriteLong(keyId); - pOut.WriteByte((byte)nested); - } + using (MemoryStream bOut = new MemoryStream()) + { + using (var pOut = new BcpgOutputStream(bOut)) + { + pOut.Write((byte)version, (byte)sigType, (byte)hashAlgorithm, (byte)keyAlgorithm); + + if (version == Version3) + { + pOut.WriteLong(keyId); + } + else + { + // V6 + pOut.WriteByte((byte)salt.Length); + pOut.Write(salt); + pOut.Write(fingerprint); + } + pOut.WriteByte((byte)nested); + } - bcpgOut.WritePacket(PacketTag.OnePassSignature, bOut.ToArray()); + bcpgOut.WritePacket(PacketTag.OnePassSignature, bOut.ToArray()); + } } } } diff --git a/crypto/src/openpgp/PgpOnePassSignature.cs b/crypto/src/openpgp/PgpOnePassSignature.cs index c14e72bf7..834943853 100644 --- a/crypto/src/openpgp/PgpOnePassSignature.cs +++ b/crypto/src/openpgp/PgpOnePassSignature.cs @@ -55,6 +55,11 @@ public void InitVerify(PgpPublicKey pubKey) try { sig.Init(false, key); + if (sigPack.Version == OnePassSignaturePacket.Version6) + { + byte[] salt = sigPack.GetSignatureSalt(); + sig.BlockUpdate(salt, 0, salt.Length); + } } catch (InvalidKeyException e) { @@ -174,7 +179,17 @@ public PublicKeyAlgorithmTag KeyAlgorithm get { return sigPack.KeyAlgorithm; } } - public byte[] GetEncoded() + public byte[] GetSignatureSalt() + { + return sigPack.GetSignatureSalt(); + } + + public byte[] GetFingerprint() + { + return sigPack.GetFingerprint(); + } + + public byte[] GetEncoded() { var bOut = new MemoryStream(); From a613784591f9ee07e0d3fd8f5ca729e108f92667 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Wed, 28 Feb 2024 21:40:11 +0100 Subject: [PATCH 12/37] generate v6 signatures --- crypto/src/bcpg/SignaturePacket.cs | 128 +++++++-- crypto/src/openpgp/PgpKeyPair.cs | 2 +- crypto/src/openpgp/PgpOnePassSignature.cs | 11 + crypto/src/openpgp/PgpPrivateKey.cs | 51 +++- crypto/src/openpgp/PgpPublicKey.cs | 2 +- crypto/src/openpgp/PgpSecretKey.cs | 2 +- crypto/src/openpgp/PgpSignature.cs | 51 +++- crypto/src/openpgp/PgpSignatureGenerator.cs | 249 +++++++++++++----- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 129 ++++++--- 9 files changed, 487 insertions(+), 138 deletions(-) diff --git a/crypto/src/bcpg/SignaturePacket.cs b/crypto/src/bcpg/SignaturePacket.cs index 45f9749a2..2c56b05bf 100644 --- a/crypto/src/bcpg/SignaturePacket.cs +++ b/crypto/src/bcpg/SignaturePacket.cs @@ -20,10 +20,13 @@ public class SignaturePacket public const int Version5 = 5; public const int Version6 = 6; + public const int DefaultVersion = Version4; + private readonly int version; private readonly int signatureType; private long creationTime; - private readonly long keyId; + private long keyId; + private bool keyIdAlreadySet = false; private readonly PublicKeyAlgorithmTag keyAlgorithm; private readonly HashAlgorithmTag hashAlgorithm; private readonly MPInteger[] signature; @@ -34,6 +37,39 @@ public class SignaturePacket // fields for v6 signatures private readonly byte[] salt; + private byte[] issuerFingerprint = null; + + private void CheckIssuerSubpacket(SignatureSubpacket p) + { + if (p is IssuerFingerprint issuerFingerprintPkt && !(issuerFingerprint is null)) + { + issuerFingerprint = issuerFingerprintPkt.GetFingerprint(); + + if (issuerFingerprintPkt.KeyVersion == PublicKeyPacket.Version4) + { + keyId = (long)Pack.BE_To_UInt64(issuerFingerprint, issuerFingerprint.Length - 8); + } + else + { + // v5 or v6 + keyId = (long)Pack.BE_To_UInt64(issuerFingerprint); + } + keyIdAlreadySet = true; + } + + else if (p is IssuerKeyId issuerKeyId && !keyIdAlreadySet) + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-issuer-key-id + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#issuer-fingerprint-subpacket + // V6 signatures MUST NOT include an IssuerKeyId subpacket and SHOULD include an IssuerFingerprint subpacket + if (version == Version6) + { + throw new IOException("V6 signatures MUST NOT include an IssuerKeyId subpacket"); + } + keyId = issuerKeyId.KeyId; + keyIdAlreadySet = true; + } + } internal SignaturePacket(BcpgInputStream bcpgIn) :base(PacketTag.Signature) @@ -102,11 +138,9 @@ internal SignaturePacket(BcpgInputStream bcpgIn) foreach (var p in hashedData) { - if (p is IssuerKeyId issuerKeyId) - { - keyId = issuerKeyId.KeyId; - } - else if (p is SignatureCreationTime sigCreationTime) + CheckIssuerSubpacket(p); + + if (p is SignatureCreationTime sigCreationTime) { creationTime = DateTimeUtilities.DateTimeToUnixMs(sigCreationTime.GetTime()); } @@ -144,10 +178,7 @@ internal SignaturePacket(BcpgInputStream bcpgIn) foreach (var p in unhashedData) { - if (p is IssuerKeyId issuerKeyId) - { - keyId = issuerKeyId.KeyId; - } + CheckIssuerSubpacket(p); } } else @@ -226,7 +257,7 @@ internal SignaturePacket(BcpgInputStream bcpgIn) } } - /** + /** * Generate a version 4 signature packet. * * @param signatureType @@ -246,7 +277,7 @@ public SignaturePacket( SignatureSubpacket[] unhashedData, byte[] fingerprint, MPInteger[] signature) - : this(Version4, signatureType, keyId, keyAlgorithm, hashAlgorithm, hashedData, unhashedData, fingerprint, signature) + : this(Version4, signatureType, keyId, keyAlgorithm, hashAlgorithm, hashedData, unhashedData, fingerprint, null, null, signature) { } @@ -268,7 +299,7 @@ public SignaturePacket( long creationTime, byte[] fingerprint, MPInteger[] signature) - : this(version, signatureType, keyId, keyAlgorithm, hashAlgorithm, null, null, fingerprint, signature) + : this(version, signatureType, keyId, keyAlgorithm, hashAlgorithm, null, null, fingerprint, null, null, signature) { this.creationTime = creationTime; } @@ -283,6 +314,55 @@ public SignaturePacket( SignatureSubpacket[] unhashedData, byte[] fingerprint, MPInteger[] signature) + :this(version, signatureType, keyId, keyAlgorithm, hashAlgorithm, hashedData, unhashedData, fingerprint, null, null, signature) + { + } + + public SignaturePacket( + int version, + int signatureType, + long keyId, + PublicKeyAlgorithmTag keyAlgorithm, + HashAlgorithmTag hashAlgorithm, + SignatureSubpacket[] hashedData, + SignatureSubpacket[] unhashedData, + byte[] fingerprint, + byte[] salt, + byte[] issuerFingerprint, + byte[] signatureEncoding) + : this(version, signatureType, keyId, keyAlgorithm, hashAlgorithm, hashedData, unhashedData, fingerprint, salt, issuerFingerprint) + { + this.signatureEncoding = Arrays.Clone(signatureEncoding); + } + + public SignaturePacket( + int version, + int signatureType, + long keyId, + PublicKeyAlgorithmTag keyAlgorithm, + HashAlgorithmTag hashAlgorithm, + SignatureSubpacket[] hashedData, + SignatureSubpacket[] unhashedData, + byte[] fingerprint, + byte[] salt, + byte[] issuerFingerprint, + MPInteger[] signature) + : this(version, signatureType, keyId, keyAlgorithm, hashAlgorithm, hashedData, unhashedData, fingerprint, salt, issuerFingerprint) + { + this.signature = signature; + } + + private SignaturePacket( + int version, + int signatureType, + long keyId, + PublicKeyAlgorithmTag keyAlgorithm, + HashAlgorithmTag hashAlgorithm, + SignatureSubpacket[] hashedData, + SignatureSubpacket[] unhashedData, + byte[] fingerprint, + byte[] salt, + byte[] issuerFingerprint) : base(PacketTag.Signature) { this.version = version; @@ -292,16 +372,17 @@ public SignaturePacket( this.hashAlgorithm = hashAlgorithm; this.hashedData = hashedData; this.unhashedData = unhashedData; - this.fingerprint = fingerprint; - this.signature = signature; + this.fingerprint = Arrays.Clone(fingerprint); + this.salt = Arrays.Clone(salt); + this.issuerFingerprint = Arrays.Clone(issuerFingerprint); - if (hashedData != null) - { - SetCreationTime(); - } - } + if (hashedData != null) + { + SetCreationTime(); + } + } - public int Version => version; + public int Version => version; public int SignatureType => signatureType; @@ -311,6 +392,11 @@ public SignaturePacket( */ public long KeyId => keyId; + public byte[] GetIssuerFingerprint() + { + return Arrays.Clone(issuerFingerprint); + } + /** * Return the signatures fingerprint. * @return fingerprint (digest prefix) of the signature diff --git a/crypto/src/openpgp/PgpKeyPair.cs b/crypto/src/openpgp/PgpKeyPair.cs index 9cf78fa6f..65ef5e829 100644 --- a/crypto/src/openpgp/PgpKeyPair.cs +++ b/crypto/src/openpgp/PgpKeyPair.cs @@ -34,7 +34,7 @@ public PgpKeyPair( DateTime time) { this.pub = new PgpPublicKey(algorithm, pubKey, time); - this.priv = new PgpPrivateKey(pub.KeyId, pub.PublicKeyPacket, privKey); + this.priv = new PgpPrivateKey(pub, privKey); } /// Create a key pair from a PgpPrivateKey and a PgpPublicKey. diff --git a/crypto/src/openpgp/PgpOnePassSignature.cs b/crypto/src/openpgp/PgpOnePassSignature.cs index 834943853..cbc889ca0 100644 --- a/crypto/src/openpgp/PgpOnePassSignature.cs +++ b/crypto/src/openpgp/PgpOnePassSignature.cs @@ -152,6 +152,17 @@ public void Update(ReadOnlySpan input) /// Verify the calculated signature against the passed in PgpSignature. public bool Verify(PgpSignature pgpSig) { + // the versions of the Signature and the One-Pass Signature must be aligned as specified in + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#signed-message-versions + if (pgpSig.Version == SignaturePacket.Version6 && sigPack.Version != OnePassSignaturePacket.Version6) + { + return false; + } + if (pgpSig.Version < SignaturePacket.Version6 && sigPack.Version != OnePassSignaturePacket.Version3) + { + return false; + } + byte[] trailer = pgpSig.GetSignatureTrailer(); sig.BlockUpdate(trailer, 0, trailer.Length); diff --git a/crypto/src/openpgp/PgpPrivateKey.cs b/crypto/src/openpgp/PgpPrivateKey.cs index 61487a5b2..57b3bdca6 100644 --- a/crypto/src/openpgp/PgpPrivateKey.cs +++ b/crypto/src/openpgp/PgpPrivateKey.cs @@ -1,16 +1,45 @@ using System; using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Utilities; namespace Org.BouncyCastle.Bcpg.OpenPgp { /// General class to contain a private key for use with other OpenPGP objects. public class PgpPrivateKey { + private readonly int version; private readonly long keyID; + private readonly byte[] fingerprint; private readonly PublicKeyPacket publicKeyPacket; private readonly AsymmetricKeyParameter privateKey; + /// + /// Create a PgpPrivateKey from associated public key, and a regular private key. + /// + /// the corresponding public key + /// the private key data packet to be associated with this private key. + public PgpPrivateKey(PgpPublicKey pubKey, AsymmetricKeyParameter privateKey) + :this(pubKey.KeyId, pubKey.GetFingerprint(), pubKey.PublicKeyPacket, privateKey) + { + } + + private PgpPrivateKey( + long keyID, + byte[] fingerprint, + PublicKeyPacket publicKeyPacket, + AsymmetricKeyParameter privateKey) + { + if (!privateKey.IsPrivate) + throw new ArgumentException("Expected a private key", nameof(privateKey)); + + this.version = publicKeyPacket.Version; + this.keyID = keyID; + this.fingerprint = fingerprint; + this.publicKeyPacket = publicKeyPacket; + this.privateKey = privateKey; + } + /// /// Create a PgpPrivateKey from a keyID, the associated public data packet, and a regular private key. /// @@ -21,13 +50,8 @@ public PgpPrivateKey( long keyID, PublicKeyPacket publicKeyPacket, AsymmetricKeyParameter privateKey) + :this(keyID, null, publicKeyPacket, privateKey) { - if (!privateKey.IsPrivate) - throw new ArgumentException("Expected a private key", "privateKey"); - - this.keyID = keyID; - this.publicKeyPacket = publicKeyPacket; - this.privateKey = privateKey; } /// The keyId associated with the contained private key. @@ -36,6 +60,21 @@ public long KeyId get { return keyID; } } + /// + /// The version of the contained private key. + /// + public int Version { + get { return version; } + } + + /// + /// The Fingerprint associated with the contained private key. + /// + public byte[] GetFingerprint() + { + return Arrays.Clone(fingerprint); + } + /// The public key packet associated with this private key, if available. public PublicKeyPacket PublicKeyPacket { diff --git a/crypto/src/openpgp/PgpPublicKey.cs b/crypto/src/openpgp/PgpPublicKey.cs index 43fe698e4..61be4b6a4 100644 --- a/crypto/src/openpgp/PgpPublicKey.cs +++ b/crypto/src/openpgp/PgpPublicKey.cs @@ -569,7 +569,7 @@ public long KeyId /// The fingerprint of the public key public byte[] GetFingerprint() { - return (byte[]) fingerprint.Clone(); + return Arrays.Clone(fingerprint); } /// diff --git a/crypto/src/openpgp/PgpSecretKey.cs b/crypto/src/openpgp/PgpSecretKey.cs index 320c2956d..e93023f33 100644 --- a/crypto/src/openpgp/PgpSecretKey.cs +++ b/crypto/src/openpgp/PgpSecretKey.cs @@ -805,7 +805,7 @@ internal PgpPrivateKey DoExtractPrivateKey(byte[] rawPassPhrase, bool clearPassP throw new PgpException("unknown public key algorithm encountered"); } - return new PgpPrivateKey(KeyId, pubPk, privateKey); + return new PgpPrivateKey(pub, privateKey); } catch (PgpException) { diff --git a/crypto/src/openpgp/PgpSignature.cs b/crypto/src/openpgp/PgpSignature.cs index 01d2a142e..32126a46a 100644 --- a/crypto/src/openpgp/PgpSignature.cs +++ b/crypto/src/openpgp/PgpSignature.cs @@ -228,17 +228,17 @@ private void UpdateWithPublicKey(PgpPublicKey key) { byte[] keyBytes = GetEncodedPublicKey(key); - this.Update(PgpPublicKey.FingerprintPreamble(key.Version)); + this.Update(PgpPublicKey.FingerprintPreamble(Version)); - switch (key.Version) + switch (Version) { - case PublicKeyPacket.Version4: + case SignaturePacket.Version4: this.Update( (byte)(keyBytes.Length >> 8), (byte)(keyBytes.Length)); break; - case PublicKeyPacket.Version5: - case PublicKeyPacket.Version6: + case SignaturePacket.Version5: + case SignaturePacket.Version6: this.Update( (byte)(keyBytes.Length >> 24), (byte)(keyBytes.Length >> 16), @@ -343,7 +343,12 @@ public long KeyId get { return sigPck.KeyId; } } - /// The creation time of this signature. + public byte[] GetIssuerFingerprint() + { + return sigPck.GetIssuerFingerprint(); + } + + /// The creation time of this signature. public DateTime CreationTime { get { return DateTimeUtilities.UnixMsToDateTime(sigPck.CreationTime); } @@ -541,8 +546,13 @@ public static PgpSignature Join(PgpSignature sig1, PgpSignature sig2) } SignatureSubpacket[] unhashed = merged.ToArray(); - return new PgpSignature( - new SignaturePacket( + + SignaturePacket sigpkt; + + if (sig1.KeyAlgorithm == PublicKeyAlgorithmTag.Ed25519 || sig1.KeyAlgorithm == PublicKeyAlgorithmTag.Ed448) + { + sigpkt = new SignaturePacket( + sig1.Version, sig1.SignatureType, sig1.KeyId, sig1.KeyAlgorithm, @@ -550,9 +560,28 @@ public static PgpSignature Join(PgpSignature sig1, PgpSignature sig2) sig1.GetHashedSubPackets().ToSubpacketArray(), unhashed, sig1.GetDigestPrefix(), - sig1.sigPck.GetSignature() - ) - ); + sig1.GetSignatureSalt(), + sig1.GetIssuerFingerprint(), + sig1.sigPck.GetSignatureBytes()); + } + else + { + sigpkt = new SignaturePacket( + sig1.Version, + sig1.SignatureType, + sig1.KeyId, + sig1.KeyAlgorithm, + sig1.HashAlgorithm, + sig1.GetHashedSubPackets().ToSubpacketArray(), + unhashed, + sig1.GetDigestPrefix(), + sig1.GetSignatureSalt(), + sig1.GetIssuerFingerprint(), + sig1.sigPck.GetSignature()); + } + + + return new PgpSignature(sigpkt); } } } diff --git a/crypto/src/openpgp/PgpSignatureGenerator.cs b/crypto/src/openpgp/PgpSignatureGenerator.cs index 7ff771997..dbfe93e6f 100644 --- a/crypto/src/openpgp/PgpSignatureGenerator.cs +++ b/crypto/src/openpgp/PgpSignatureGenerator.cs @@ -14,17 +14,21 @@ namespace Org.BouncyCastle.Bcpg.OpenPgp /// Generator for PGP signatures. public class PgpSignatureGenerator { - private static readonly SignatureSubpacket[] EmptySignatureSubpackets = new SignatureSubpacket[0]; + private static readonly SignatureSubpacket[] EmptySignatureSubpackets = Array.Empty(); private readonly PublicKeyAlgorithmTag keyAlgorithm; private readonly HashAlgorithmTag hashAlgorithm; + private int version; + private PgpPrivateKey privKey; private ISigner sig; - private IDigest dig; + private readonly IDigest dig; private int signatureType; private byte lastb; + private byte[] salt; + private SignatureSubpacket[] unhashed = EmptySignatureSubpackets; private SignatureSubpacket[] hashed = EmptySignatureSubpackets; @@ -45,9 +49,13 @@ public void InitSign(int sigType, PgpPrivateKey privKey) InitSign(sigType, privKey, null); } - /// Initialise the generator for signing. - public void InitSign(int sigType, PgpPrivateKey privKey, SecureRandom random) + /// Initialise the generator for signing. + public void InitSign(int sigType, PgpPrivateKey privKey, SecureRandom random) { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-signature-packet-type-id-2 + // An implementation MUST generate a version 6 signature when signing with a version 6 key. + // An implementation MUST generate a version 4 signature when signing with a version 4 key. + this.version = privKey.Version; this.privKey = privKey; this.signatureType = sigType; @@ -60,7 +68,9 @@ public void InitSign(int sigType, PgpPrivateKey privKey, SecureRandom random) ICipherParameters cp = key; // TODO Ask SignerUtilities whether random is permitted? - if (keyAlgorithm == PublicKeyAlgorithmTag.EdDsa_Legacy) + if (keyAlgorithm == PublicKeyAlgorithmTag.EdDsa_Legacy + || keyAlgorithm == PublicKeyAlgorithmTag.Ed25519 + || keyAlgorithm == PublicKeyAlgorithmTag.Ed448) { // EdDSA signers don't expect a SecureRandom } @@ -70,6 +80,19 @@ public void InitSign(int sigType, PgpPrivateKey privKey, SecureRandom random) } sig.Init(true, cp); + + // salt for v6 signatures + if(version == SignaturePacket.Version6) + { + if (random is null) + { + throw new ArgumentNullException(nameof(random), "v6 signatures requires a SecureRandom() for salt generation"); + } + int saltSize = PgpUtilities.GetSaltSize(hashAlgorithm); + salt = new byte[saltSize]; + random.NextBytes(salt); + sig.BlockUpdate(salt, 0, salt.Length); + } } catch (InvalidKeyException e) { @@ -190,9 +213,23 @@ public void SetUnhashedSubpackets( public PgpOnePassSignature GenerateOnePassVersion( bool isNested) { - return new PgpOnePassSignature( - new OnePassSignaturePacket( - signatureType, hashAlgorithm, keyAlgorithm, privKey.KeyId, isNested)); + OnePassSignaturePacket opsPkt; + + // the versions of the Signature and the One-Pass Signature must be aligned as specified in + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#signed-message-versions + if (version == SignaturePacket.Version6) + { + opsPkt = new OnePassSignaturePacket( + signatureType, hashAlgorithm, keyAlgorithm, salt, privKey.GetFingerprint(), isNested); + + } + else + { + opsPkt = new OnePassSignaturePacket( + signatureType, hashAlgorithm, keyAlgorithm, privKey.KeyId, isNested); + } + + return new PgpOnePassSignature(opsPkt); } /// Return a signature object containing the current signature state. @@ -205,36 +242,65 @@ public PgpSignature Generate() hPkts = InsertSubpacket(hPkts, new SignatureCreationTime(false, DateTime.UtcNow)); } - if (!IsPacketPresent(hashed, SignatureSubpacketTag.IssuerKeyId) - && !IsPacketPresent(unhashed, SignatureSubpacketTag.IssuerKeyId)) - { - unhPkts = InsertSubpacket(unhPkts, new IssuerKeyId(false, privKey.KeyId)); - } + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-issuer-key-id + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#issuer-fingerprint-subpacket + bool containsIssuerKeyId = IsPacketPresent(hashed, SignatureSubpacketTag.IssuerKeyId) || IsPacketPresent(unhashed, SignatureSubpacketTag.IssuerKeyId); + bool containsIssuerKeyFpr = IsPacketPresent(hashed, SignatureSubpacketTag.IssuerFingerprint) || IsPacketPresent(unhashed, SignatureSubpacketTag.IssuerFingerprint); + switch (version) + { + case SignaturePacket.Version4: + // TODO enforce constraint: if a v4 signature contains both IssuerKeyId and IssuerFingerprint + // subpackets, then the KeyId MUST match the low 64 bits of the fingerprint. + if (!containsIssuerKeyId) + { + unhPkts = InsertSubpacket(unhPkts, new IssuerKeyId(false, privKey.KeyId)); + } + break; + case SignaturePacket.Version6: + // V6 signatures MUST NOT include an IssuerKeyId subpacket and SHOULD include an IssuerFingerprint subpacket + if (containsIssuerKeyId) + { + // TODO enforce constraint: v6 signatures MUST NOT include an IssuerKeyId subpacket + } + if (!containsIssuerKeyFpr) + { + unhPkts = InsertSubpacket(unhPkts, new IssuerFingerprint(false, privKey.Version, privKey.GetFingerprint())); + } + break; + } - int version = 4; byte[] hData; try { - MemoryStream hOut = new MemoryStream(); - - for (int i = 0; i != hPkts.Length; i++) + using (MemoryStream hOut = new MemoryStream()) { - hPkts[i].Encode(hOut); - } - - byte[] data = hOut.ToArray(); - MemoryStream sOut = new MemoryStream(data.Length + 6); - sOut.WriteByte((byte)version); - sOut.WriteByte((byte)signatureType); - sOut.WriteByte((byte)keyAlgorithm); - sOut.WriteByte((byte)hashAlgorithm); - sOut.WriteByte((byte)(data.Length >> 8)); - sOut.WriteByte((byte)data.Length); - sOut.Write(data, 0, data.Length); - - hData = sOut.ToArray(); + for (int i = 0; i != hPkts.Length; i++) + { + hPkts[i].Encode(hOut); + } + + byte[] data = hOut.ToArray(); + + using (MemoryStream sOut = new MemoryStream(data.Length + 6)) + { + sOut.WriteByte((byte)version); + sOut.WriteByte((byte)signatureType); + sOut.WriteByte((byte)keyAlgorithm); + sOut.WriteByte((byte)hashAlgorithm); + if (version == SignaturePacket.Version6) + { + sOut.WriteByte((byte)(data.Length >> 24)); + sOut.WriteByte((byte)(data.Length >> 16)); + } + sOut.WriteByte((byte)(data.Length >> 8)); + sOut.WriteByte((byte)data.Length); + sOut.Write(data, 0, data.Length); + + hData = sOut.ToArray(); + } + } } catch (IOException e) { @@ -261,41 +327,75 @@ public PgpSignature Generate() byte[] digest = DigestUtilities.DoFinal(dig); byte[] fingerPrint = new byte[2]{ digest[0], digest[1] }; - MPInteger[] sigValues; - if (keyAlgorithm == PublicKeyAlgorithmTag.EdDsa_Legacy) - { - int sigLen = sigBytes.Length; - if (sigLen == Ed25519.SignatureSize) + SignaturePacket sigPkt; + + if (keyAlgorithm == PublicKeyAlgorithmTag.Ed25519 || keyAlgorithm == PublicKeyAlgorithmTag.Ed448) + { + sigPkt = new SignaturePacket( + version, + signatureType, + privKey.KeyId, + keyAlgorithm, + hashAlgorithm, + hPkts, + unhPkts, + fingerPrint, + version == SignaturePacket.Version6 ? salt : null, + privKey.GetFingerprint(), + sigBytes); + } + else + { + MPInteger[] sigValues; + if (keyAlgorithm == PublicKeyAlgorithmTag.EdDsa_Legacy) { - sigValues = new MPInteger[2]{ - new MPInteger(new BigInteger(1, sigBytes, 0, 32)), - new MPInteger(new BigInteger(1, sigBytes, 32, 32)) - }; + int sigLen = sigBytes.Length; + + if (sigLen == Ed25519.SignatureSize) + { + sigValues = new MPInteger[2] + { + new MPInteger(new BigInteger(1, sigBytes, 0, 32)), + new MPInteger(new BigInteger(1, sigBytes, 32, 32)) + }; + } + else if (sigLen == Ed448.SignatureSize) + { + sigValues = new MPInteger[2] + { + new MPInteger(new BigInteger(1, Arrays.Prepend(sigBytes, 0x40))), + new MPInteger(BigInteger.Zero) + }; + } + else + { + throw new InvalidOperationException(); + } } - else if (sigLen == Ed448.SignatureSize) - { - sigValues = new MPInteger[2]{ - new MPInteger(new BigInteger(1, Arrays.Prepend(sigBytes, 0x40))), - new MPInteger(BigInteger.Zero) - }; - } - else + else if (keyAlgorithm == PublicKeyAlgorithmTag.RsaSign || keyAlgorithm == PublicKeyAlgorithmTag.RsaGeneral) { - throw new InvalidOperationException(); + sigValues = PgpUtilities.RsaSigToMpi(sigBytes); } - } - else if (keyAlgorithm == PublicKeyAlgorithmTag.RsaSign || keyAlgorithm == PublicKeyAlgorithmTag.RsaGeneral) - { - sigValues = PgpUtilities.RsaSigToMpi(sigBytes); - } - else - { - sigValues = PgpUtilities.DsaSigToMpi(sigBytes); + else + { + sigValues = PgpUtilities.DsaSigToMpi(sigBytes); + } + + sigPkt = new SignaturePacket( + version, + signatureType, + privKey.KeyId, + keyAlgorithm, + hashAlgorithm, + hPkts, + unhPkts, + fingerPrint, + version == SignaturePacket.Version6 ? salt : null, + privKey.GetFingerprint(), + sigValues); } - return new PgpSignature( - new SignaturePacket(signatureType, privKey.KeyId, keyAlgorithm, - hashAlgorithm, hPkts, unhPkts, fingerPrint, sigValues)); + return new PgpSignature(sigPkt); } /// Generate a certification for the passed in ID and key. @@ -387,7 +487,7 @@ private static bool IsPacketPresent(SignatureSubpacket[] packets, SignatureSubpa return false; } - private static SignatureSubpacket[] InsertSubpacket(SignatureSubpacket[] packets, SignatureSubpacket subpacket) + private static SignatureSubpacket[] InsertSubpacket(SignatureSubpacket[] packets, SignatureSubpacket subpacket) { return Arrays.Prepend(packets, subpacket); } @@ -408,11 +508,26 @@ private void UpdateWithPublicKey( { byte[] keyBytes = GetEncodedPublicKey(key); - Update( - 0x99, - (byte)(keyBytes.Length >> 8), - (byte)(keyBytes.Length)); - Update(keyBytes); - } + this.Update(PgpPublicKey.FingerprintPreamble(version)); + + switch (version) + { + case SignaturePacket.Version4: + this.Update( + (byte)(keyBytes.Length >> 8), + (byte)(keyBytes.Length)); + break; + case SignaturePacket.Version5: + case SignaturePacket.Version6: + this.Update( + (byte)(keyBytes.Length >> 24), + (byte)(keyBytes.Length >> 16), + (byte)(keyBytes.Length >> 8), + (byte)(keyBytes.Length)); + break; + } + + this.Update(keyBytes); + } } } diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 3ce1eccb1..ce70c015f 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -3,7 +3,6 @@ using Org.BouncyCastle.Crypto.Agreement; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Crypto.Signers; using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.Encoders; @@ -100,6 +99,9 @@ public void Version4Ed25519LegacyPubkeySampleTest() [Test] public void Version4Ed25519LegacyCreateTest() { + // create a v4 EdDsa_Legacy Pubkey with the same key material and creation datetime as the test vector + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-key + // then check KeyId/Fingerprint var key = new Ed25519PublicKeyParameters(Hex.Decode("3f098994bdd916ed4053197934e4a87c80733a1280d62f8010992e43ee3b2406")); var pubKey = new PgpPublicKey(PublicKeyAlgorithmTag.EdDsa_Legacy, key, DateTime.Parse("2014-08-19 14:28:27Z")); IsEquals(pubKey.Algorithm, PublicKeyAlgorithmTag.EdDsa_Legacy); @@ -126,11 +128,12 @@ public void Version4Ed25519LegacySignatureSampleTest() IsEquals(signature.HashAlgorithm, HashAlgorithmTag.Sha256); IsEquals(signature.CreationTime.ToString("yyyyMMddHHmmss"), "20150916122453"); - byte[] original = Encoding.UTF8.GetBytes("OpenPGP"); - signature.InitVerify(pubKey); - signature.Update(original); - - IsTrue("Failed generated signature check against original data", signature.Verify()); + byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); + VerifySignature(signature, data, pubKey); + + // test with wrong data, verification should fail + data = Encoding.UTF8.GetBytes("OpePGP"); + VerifySignature(signature, data, pubKey, shouldFail: true); } [Test] @@ -208,12 +211,16 @@ public void Version6PublicKeyCreationTest() IsEquals((ulong)pubKey.KeyId, 0xCB186C4F0609A697); IsTrue("wrong master key fingerprint", AreEqual(pubKey.GetFingerprint(), expectedFingerprint)); - bool signatureOk = VerifySignature( + VerifyEncodedSignature( v6SampleCleartextSignedMessageSignature, Encoding.UTF8.GetBytes(v6SampleCleartextSignedMessage), pubKey); - IsTrue("Failed generated signature check against original data", signatureOk); + VerifyEncodedSignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes("wrongdata"), + pubKey, + shouldFail: true); } [Test] @@ -237,8 +244,24 @@ public void Version6UnlockedSecretKeyParsingTest() IsEquals(signingKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.Ed25519); IsEquals((ulong)signingKey.PublicKey.KeyId, 0xCB186C4F0609A697); - AsymmetricCipherKeyPair signingKeyPair = GetKeyPair(signingKey); - IsTrue("signature test failed", SignThenVerifyEd25519Test(signingKeyPair)); + // generate and verify a v6 signature + byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); + byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); + PgpSignatureGenerator sigGen = new PgpSignatureGenerator(signingKey.PublicKey.Algorithm, HashAlgorithmTag.Sha512); + PgpSignatureSubpacketGenerator spkGen = new PgpSignatureSubpacketGenerator(); + PgpPrivateKey privKey = signingKey.ExtractPrivateKey("".ToCharArray()); + spkGen.SetIssuerFingerprint(false, signingKey); + sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); + sigGen.Update(data); + sigGen.SetHashedSubpackets(spkGen.Generate()); + PgpSignature signature = sigGen.Generate(); + + VerifySignature(signature, data, signingKey.PublicKey); + VerifySignature(signature, wrongData, signingKey.PublicKey, shouldFail: true); + + byte[] encodedSignature = signature.GetEncoded(); + VerifyEncodedSignature(encodedSignature, data, signingKey.PublicKey); + VerifyEncodedSignature(encodedSignature, wrongData, signingKey.PublicKey, shouldFail: true); // encryption key PgpSecretKey encryptionKey = secretKeys[1]; @@ -263,6 +286,41 @@ public void Version6UnlockedSecretKeyParsingTest() byte[] encoded = ms.ToArray(); IsTrue(AreEqual(encoded, v6UnlockedSecretKey)); } + + // generate and verify a v6 userid self-cert + string userId = "Alice "; + string wrongUserId = "Bob "; + sigGen.InitSign(PgpSignature.PositiveCertification, privKey, new SecureRandom()); + signature = sigGen.GenerateCertification(userId, signingKey.PublicKey); + signature.InitVerify(signingKey.PublicKey); + if (!signature.VerifyCertification(userId, signingKey.PublicKey)) + { + Fail("self-cert verification failed."); + } + signature.InitVerify(signingKey.PublicKey); + if (signature.VerifyCertification(wrongUserId, signingKey.PublicKey)) + { + Fail("self-cert verification failed."); + } + PgpPublicKey key = PgpPublicKey.AddCertification(signingKey.PublicKey, userId, signature); + byte[] keyEnc = key.GetEncoded(); + PgpPublicKeyRing tmpRing = new PgpPublicKeyRing(keyEnc); + key = tmpRing.GetPublicKey(); + IsTrue(key.GetUserIds().Contains(userId)); + + // generate and verify a v6 cert revocation + sigGen.InitSign(PgpSignature.KeyRevocation, privKey, new SecureRandom()); + signature = sigGen.GenerateCertification(signingKey.PublicKey); + signature.InitVerify(signingKey.PublicKey); + if (!signature.VerifyCertification(signingKey.PublicKey)) + { + Fail("revocation verification failed."); + } + key = PgpPublicKey.AddCertification(signingKey.PublicKey, signature); + keyEnc = key.GetEncoded(); + tmpRing = new PgpPublicKeyRing(keyEnc); + key = tmpRing.GetPublicKey(); + IsTrue(key.IsRevoked()); } [Test] @@ -312,23 +370,48 @@ public void Version6SampleCleartextSignedMessageVerifySignatureTest() PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v6Certificate); PgpPublicKey pubKey = pubRing.GetPublicKey(); - bool signatureOk = VerifySignature( + VerifyEncodedSignature( v6SampleCleartextSignedMessageSignature, Encoding.UTF8.GetBytes(v6SampleCleartextSignedMessage), pubKey); - IsTrue("Failed generated signature check against original data", signatureOk); + VerifyEncodedSignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes("wrongdata"), + pubKey, + shouldFail: true); } - private static bool VerifySignature(byte[] sigPacket, byte[] data, PgpPublicKey signer) + private void VerifySignature(PgpSignature signature, byte[] data, PgpPublicKey signer, bool shouldFail = false) + { + IsEquals(signature.KeyAlgorithm, signer.Algorithm); + // the version of the signature is bound to the version of the signing key + IsEquals(signature.Version, signer.Version); + + if (signature.KeyId != 0) + { + IsEquals(signature.KeyId, signer.KeyId); + } + byte[] issuerFpt = signature.GetIssuerFingerprint(); + if (issuerFpt != null) + { + IsTrue(AreEqual(issuerFpt, signer.GetFingerprint())); + } + + signature.InitVerify(signer); + signature.Update(data); + + bool result = signature.Verify() != shouldFail; + IsTrue("signature test failed", result); + } + + private void VerifyEncodedSignature(byte[] sigPacket, byte[] data, PgpPublicKey signer, bool shouldFail = false) { PgpObjectFactory factory = new PgpObjectFactory(sigPacket); PgpSignatureList sigList = (PgpSignatureList)factory.NextPgpObject(); PgpSignature signature = sigList[0]; - signature.InitVerify(signer); - signature.Update(data); - return signature.Verify(); + VerifySignature(signature, data, signer, shouldFail); } private static AsymmetricCipherKeyPair GetKeyPair(PgpSecretKey secretKey, string password = "") @@ -338,20 +421,6 @@ private static AsymmetricCipherKeyPair GetKeyPair(PgpSecretKey secretKey, string secretKey.ExtractPrivateKey(password.ToCharArray()).Key); } - private static bool SignThenVerifyEd25519Test(AsymmetricCipherKeyPair signingKeyPair) - { - byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); - - ISigner signer = new Ed25519Signer(); - signer.Init(true, signingKeyPair.Private); - signer.BlockUpdate(data, 0, data.Length); - byte[] signature = signer.GenerateSignature(); - - signer.Init(false, signingKeyPair.Public); - signer.BlockUpdate(data, 0, data.Length); - return signer.VerifySignature(signature); - } - private static bool EncryptThenDecryptX25519Test(AsymmetricCipherKeyPair alice, AsymmetricCipherKeyPair bob) { X25519Agreement agreeA = new X25519Agreement(); From ceaa293ba2590916948a8600cc2b80d2472dc17b Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 29 Feb 2024 17:48:02 +0100 Subject: [PATCH 13/37] test v6 inline signature generate and verify --- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 106 +++++++++++++++++- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index ce70c015f..5633d9a63 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -81,6 +81,18 @@ public class PgpCryptoRefreshTest "/FvLFuGWMbKAdA+epq7V4HOtAPlBWmU8QOd6aud+aSunHQaaEJ+iTFjP2OMW0KBr" + "NK2ay45cX1IVAQ=="); + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-inline-signed-messag + private readonly byte[] v6SampleInlineSignedMessage = Base64.Decode( + "xEYGAQobIHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usyxhsTwYJppfk" + + "1S36bHIrDB8eJ8GKVnCPZSXsJ7rZrMkBy0p1AAAAAABXaGF0IHdlIG5lZWQgZnJv" + + "bSB0aGUgZ3JvY2VyeSBzdG9yZToKCi0gdG9mdQotIHZlZ2V0YWJsZXMKLSBub29k" + + "bGVzCsKYBgEbCgAAACkFgmOYo2MiIQbLGGxPBgmml+TVLfpscisMHx4nwYpWcI9l" + + "JewnutmsyQAAAABpNiB2SV9QIYiQ9/Xi7jwYIlFPcFAPVR2G5ckh5ATjSlP7rCfQ" + + "b7gKqPxbyxbhljGygHQPnqau1eBzrQD5QVplPEDnemrnfmkrpx0GmhCfokxYz9jj" + + "FtCgazStmsuOXF9SFQE="); + + private readonly char[] emptyPassphrase = Array.Empty(); + [Test] public void Version4Ed25519LegacyPubkeySampleTest() { @@ -120,7 +132,7 @@ public void Version4Ed25519LegacySignatureSampleTest() PgpPublicKey pubKey = pubRing.GetPublicKey(); PgpObjectFactory factory = new PgpObjectFactory(v4Ed25519LegacySignatureSample); - PgpSignatureList sigList = (PgpSignatureList)factory.NextPgpObject(); + PgpSignatureList sigList = factory.NextPgpObject() as PgpSignatureList; PgpSignature signature = sigList[0]; IsEquals(signature.KeyId, pubKey.KeyId); @@ -249,7 +261,7 @@ public void Version6UnlockedSecretKeyParsingTest() byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); PgpSignatureGenerator sigGen = new PgpSignatureGenerator(signingKey.PublicKey.Algorithm, HashAlgorithmTag.Sha512); PgpSignatureSubpacketGenerator spkGen = new PgpSignatureSubpacketGenerator(); - PgpPrivateKey privKey = signingKey.ExtractPrivateKey("".ToCharArray()); + PgpPrivateKey privKey = signingKey.ExtractPrivateKey(emptyPassphrase); spkGen.SetIssuerFingerprint(false, signingKey); sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); sigGen.Update(data); @@ -382,6 +394,92 @@ public void Version6SampleCleartextSignedMessageVerifySignatureTest() shouldFail: true); } + [Test] + public void Version6SampleInlineSignedMessageVerifySignatureTest() + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-inline-signed-messag + PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v6Certificate); + PgpPublicKey pubKey = pubRing.GetPublicKey(); + + VerifyInlineSignature(v6SampleInlineSignedMessage, pubKey); + } + + [Test] + public void Version6GenerateAndVerifyInlineSignatureTest() + { + PgpSecretKeyRing secretKeyRing = new PgpSecretKeyRing(v6UnlockedSecretKey); + PgpSecretKey signingKey = secretKeyRing.GetSecretKey(); + PgpPrivateKey privKey = signingKey.ExtractPrivateKey(emptyPassphrase); + byte[] data = Encoding.UTF8.GetBytes("OpenPGP\nOpenPGP"); + byte[] inlineSignatureMessage; + + using (MemoryStream ms = new MemoryStream()) + { + using (BcpgOutputStream bcOut = new BcpgOutputStream(ms, newFormatOnly: true)) + { + PgpSignatureGenerator sGen = new PgpSignatureGenerator(signingKey.PublicKey.Algorithm, HashAlgorithmTag.Sha384); + sGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); + sGen.GenerateOnePassVersion(false).Encode(bcOut); + + PgpLiteralDataGenerator lGen = new PgpLiteralDataGenerator(); + DateTime modificationTime = DateTime.UtcNow; + using (var lOut = lGen.Open( + new UncloseableStream(bcOut), + PgpLiteralData.Utf8, + "_CONSOLE", + data.Length, + modificationTime)) + { + lOut.Write(data, 0, data.Length); + sGen.Update(data); + } + + sGen.Generate().Encode(bcOut); + } + + inlineSignatureMessage = ms.ToArray(); + } + + VerifyInlineSignature(inlineSignatureMessage, signingKey.PublicKey); + // corrupt data + inlineSignatureMessage[88] = 80; + VerifyInlineSignature(inlineSignatureMessage, signingKey.PublicKey, shouldFail: true); + } + + private void VerifyInlineSignature(byte[] message, PgpPublicKey signer, bool shouldFail = false) + { + byte[] data; + PgpObjectFactory factory = new PgpObjectFactory(message); + + PgpOnePassSignatureList p1 = factory.NextPgpObject() as PgpOnePassSignatureList; + PgpOnePassSignature ops = p1[0]; + + PgpLiteralData p2 = factory.NextPgpObject() as PgpLiteralData; + Stream dIn = p2.GetInputStream(); + + ops.InitVerify(signer); + + using (MemoryStream ms = new MemoryStream()) + { + byte[] buffer = new byte[30]; + int bytesRead; + while ((bytesRead = dIn.Read(buffer, 0, buffer.Length)) > 0) + { + ops.Update(buffer, 0, bytesRead); + ms.Write(buffer, 0, bytesRead); + } + + data = ms.ToArray(); + } + PgpSignatureList p3 = factory.NextPgpObject() as PgpSignatureList; + PgpSignature sig = p3[0]; + + bool result = ops.Verify(sig) != shouldFail; + IsTrue("signature test failed", result); + + VerifySignature(sig, data, signer, shouldFail); + } + private void VerifySignature(PgpSignature signature, byte[] data, PgpPublicKey signer, bool shouldFail = false) { IsEquals(signature.KeyAlgorithm, signer.Algorithm); @@ -408,7 +506,7 @@ private void VerifySignature(PgpSignature signature, byte[] data, PgpPublicKey s private void VerifyEncodedSignature(byte[] sigPacket, byte[] data, PgpPublicKey signer, bool shouldFail = false) { PgpObjectFactory factory = new PgpObjectFactory(sigPacket); - PgpSignatureList sigList = (PgpSignatureList)factory.NextPgpObject(); + PgpSignatureList sigList = factory.NextPgpObject() as PgpSignatureList; PgpSignature signature = sigList[0]; VerifySignature(signature, data, signer, shouldFail); @@ -447,6 +545,8 @@ public override void PerformTest() Version6UnlockedSecretKeyParsingTest(); Version6LockedSecretKeyParsingTest(); Version6SampleCleartextSignedMessageVerifySignatureTest(); + Version6SampleInlineSignedMessageVerifySignatureTest(); + Version6GenerateAndVerifyInlineSignatureTest(); } } } \ No newline at end of file From f4ac85cb30215d43b6996e999c1213dd8e77c2f3 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 29 Feb 2024 21:08:19 +0100 Subject: [PATCH 14/37] doc comments --- crypto/src/bcpg/AeadUtils.cs | 47 ++++++++-------- crypto/src/bcpg/OctetArrayBCPGKey.cs | 7 +-- crypto/src/bcpg/SecretKeyPacket.cs | 83 ++++++++++++++-------------- 3 files changed, 66 insertions(+), 71 deletions(-) diff --git a/crypto/src/bcpg/AeadUtils.cs b/crypto/src/bcpg/AeadUtils.cs index 922a4860c..e7e63c74e 100644 --- a/crypto/src/bcpg/AeadUtils.cs +++ b/crypto/src/bcpg/AeadUtils.cs @@ -7,12 +7,12 @@ namespace Org.BouncyCastle.Bcpg { public sealed class AeadUtils { - /** - * Return the length of the IV used by the given AEAD algorithm in octets. - * - * @param aeadAlgorithmTag AEAD algorithm identifier - * @return length of the IV - */ + /// + /// Return the length of the IV used by the given AEAD algorithm in octets. + /// + /// AEAD algorithm identifier + /// length of the IV + /// Thrown when aeadAlgorithmTag is unknown/invalid public static int GetIVLength(AeadAlgorithmTag aeadAlgorithmTag) { switch (aeadAlgorithmTag) @@ -28,12 +28,12 @@ public static int GetIVLength(AeadAlgorithmTag aeadAlgorithmTag) } } - /** - * Return the length of the authentication tag used by the given AEAD algorithm in octets. - * - * @param aeadAlgorithmTag AEAD algorithm identifier - * @return length of the auth tag - */ + /// + /// Return the length of the authentication tag used by the given AEAD algorithm in octets. + /// + /// AEAD algorithm identifier + /// length of the auth tag + /// Thrown when aeadAlgorithmTag is unknown/invalid public static int GetAuthTagLength(AeadAlgorithmTag aeadAlgorithmTag) { switch (aeadAlgorithmTag) @@ -47,18 +47,17 @@ public static int GetAuthTagLength(AeadAlgorithmTag aeadAlgorithmTag) } } - /** - * Split a given byte array containing
m
bytes of key and
n-8
bytes of IV into - * two separate byte arrays. - *
m
is the key length of the cipher algorithm, while
n
is the IV length of the AEAD algorithm. - * Note, that the IV is filled with
n-8
bytes only, the remainder is left as 0s. - * Return an array of both arrays with the key and index 0 and the IV at index 1. - * - * @param messageKeyAndIv
m+n-8
bytes of concatenated message key and IV - * @param cipherAlgo symmetric cipher algorithm - * @param aeadAlgo AEAD algorithm - * @return array of arrays containing message key and IV - */ + /// + /// Split a given byte array containing m bytes of key and n-8 bytes of IV into + /// two separate byte arrays. + /// m is the key length of the cipher algorithm, while n is the IV length of the AEAD algorithm. + /// Note, that the IV is filled with
n-8
bytes only, the remainder is left as 0s. + /// Return an array of both arrays with the key and index 0 and the IV at index 1. + ///
+ /// m+n-8 bytes of concatenated message key and IV + /// symmetric cipher algorithm + /// AEAD algorithm + /// array of arrays containing message key and IV public static byte[][] SplitMessageKeyAndIv(byte[] messageKeyAndIv, SymmetricKeyAlgorithmTag cipherAlgo, AeadAlgorithmTag aeadAlgo) { int keyLen = PgpUtilities.GetKeySizeInOctets(cipherAlgo); diff --git a/crypto/src/bcpg/OctetArrayBCPGKey.cs b/crypto/src/bcpg/OctetArrayBCPGKey.cs index 1bf09958e..73d0d34a4 100644 --- a/crypto/src/bcpg/OctetArrayBCPGKey.cs +++ b/crypto/src/bcpg/OctetArrayBCPGKey.cs @@ -3,10 +3,9 @@ namespace Org.BouncyCastle.Bcpg { - /** - * Public/Secret BcpgKey which is encoded as an array of octets rather than an MPI - * - */ + /// + /// Public/Secret BcpgKey which is encoded as an array of octets rather than an MPI + /// public abstract class OctetArrayBcpgKey : BcpgObject, IBcpgKey { diff --git a/crypto/src/bcpg/SecretKeyPacket.cs b/crypto/src/bcpg/SecretKeyPacket.cs index d237ef469..985fc28cf 100644 --- a/crypto/src/bcpg/SecretKeyPacket.cs +++ b/crypto/src/bcpg/SecretKeyPacket.cs @@ -1,7 +1,6 @@ +using Org.BouncyCastle.Utilities; using System; using System.IO; -using Org.BouncyCastle.Pqc.Crypto.SphincsPlus; -using Org.BouncyCastle.Utilities; namespace Org.BouncyCastle.Bcpg { @@ -10,53 +9,51 @@ public class SecretKeyPacket : ContainedPacket //, PublicKeyAlgorithmTag { - /** - * Unprotected. - */ + /// + /// Unprotected secret key + /// public const int UsageNone = 0x00; - /** - * Malleable CFB. - * Malleable-CFB-encrypted keys are vulnerable to corruption attacks - * that can cause leakage of secret data when the secret key is used. - * - * @see - * Klíma, V. and T. Rosa, - * "Attack on Private Signature Keys of the OpenPGP Format, - * PGP(TM) Programs and Other Applications Compatible with OpenPGP" - * @see - * Bruseghini, L., Paterson, K. G., and D. Huigens, - * "Victory by KO: Attacking OpenPGP Using Key Overwriting" - * @deprecated Use of MalleableCFB is deprecated. - * For v4 keys, use {@link #USAGE_SHA1} instead. - * For v6 keys use {@link #USAGE_AEAD} instead. - */ + /// + /// Malleable CFB. + /// Malleable-CFB-encrypted keys are vulnerable to corruption attacks + /// that can cause leakage of secret data when the secret key is used. + /// + /// + /// Klíma, V.and T. Rosa, + /// "Attack on Private Signature Keys of the OpenPGP Format, + /// PGP(TM) Programs and Other Applications Compatible with OpenPGP" + /// + /// Bruseghini, L., Paterson, K.G., and D. Huigens, + /// "Victory by KO: Attacking OpenPGP Using Key Overwriting" + /// public const int UsageChecksum = 0xff; - /** - * CFB. - * CFB-encrypted keys are vulnerable to corruption attacks that can - * cause leakage of secret data when the secret key is use. - * - * @see - * Klíma, V. and T. Rosa, - * "Attack on Private Signature Keys of the OpenPGP Format, - * PGP(TM) Programs and Other Applications Compatible with OpenPGP" - * @see - * Bruseghini, L., Paterson, K. G., and D. Huigens, - * "Victory by KO: Attacking OpenPGP Using Key Overwriting" - */ + + /// + /// CFB. + /// CFB-encrypted keys are vulnerable to corruption attacks that can + /// cause leakage of secret data when the secret key is use. + /// + /// + /// Klíma, V. and T.Rosa, + /// "Attack on Private Signature Keys of the OpenPGP Format, + /// PGP(TM) Programs and Other Applications Compatible with OpenPGP" + /// + /// Bruseghini, L., Paterson, K.G., and D. Huigens, + /// "Victory by KO: Attacking OpenPGP Using Key Overwriting" + /// public const int UsageSha1 = 0xfe; - /** - * AEAD. - * This usage protects against above-mentioned attacks. - * Passphrase-protected secret key material in a v6 Secret Key or - * v6 Secret Subkey packet SHOULD be protected with AEAD encryption - * unless it will be transferred to an implementation that is known - * to not support AEAD. - * Users should migrate to AEAD with all due speed. - */ + /// + /// AEAD. + /// This usage protects against corruption attacks. + /// Passphrase-protected secret key material in a v6 Secret Key or + /// v6 Secret Subkey packet SHOULD be protected with AEAD encryption + /// unless it will be transferred to an implementation that is known + /// to not support AEAD. + /// Users should migrate to AEAD with all due speed. + /// public const int UsageAead = 0xfd; From a8ce4b0b1f3276a6bf543d7b55593a3627359ee2 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Sat, 2 Mar 2024 15:51:33 +0100 Subject: [PATCH 15/37] v5 keys and signature verification, tests on multiple v4+v6 signatures --- crypto/src/bcpg/SignaturePacket.cs | 129 +++-- crypto/src/openpgp/PgpLiteralData.cs | 38 ++ crypto/src/openpgp/PgpOnePassSignature.cs | 25 +- crypto/src/openpgp/PgpSignature.cs | 18 +- .../test/PgpInteroperabilityTestSuite.cs | 448 ++++++++++++++++++ 5 files changed, 605 insertions(+), 53 deletions(-) create mode 100644 crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs diff --git a/crypto/src/bcpg/SignaturePacket.cs b/crypto/src/bcpg/SignaturePacket.cs index 2c56b05bf..5af884387 100644 --- a/crypto/src/bcpg/SignaturePacket.cs +++ b/crypto/src/bcpg/SignaturePacket.cs @@ -41,7 +41,7 @@ public class SignaturePacket private void CheckIssuerSubpacket(SignatureSubpacket p) { - if (p is IssuerFingerprint issuerFingerprintPkt && !(issuerFingerprint is null)) + if (p is IssuerFingerprint issuerFingerprintPkt && issuerFingerprint is null) { issuerFingerprint = issuerFingerprintPkt.GetFingerprint(); @@ -406,13 +406,19 @@ public byte[] GetFingerprint() return Arrays.Clone(fingerprint); } - /** + /** * return the signature trailer that must be included with the data * to reconstruct the signature * * @return byte[] */ + public byte[] GetSignatureTrailer() + { + return GetSignatureTrailer(Array.Empty()); + } + + public byte[] GetSignatureTrailer(byte[] additionalMetadata) { if (version == Version3) { @@ -424,56 +430,93 @@ public byte[] GetSignatureTrailer() return trailer; } - MemoryStream sOut = new MemoryStream(); - - sOut.WriteByte((byte)Version); - sOut.WriteByte((byte)SignatureType); - sOut.WriteByte((byte)KeyAlgorithm); - sOut.WriteByte((byte)HashAlgorithm); - - // Mark position an reserve two bytes (version4) or four bytes (version6) - // for length - long lengthPosition = sOut.Position; - if (version == Version6) + using (MemoryStream sOut = new MemoryStream()) { + sOut.WriteByte((byte)Version); + sOut.WriteByte((byte)SignatureType); + sOut.WriteByte((byte)KeyAlgorithm); + sOut.WriteByte((byte)HashAlgorithm); + + // Mark position an reserve two bytes (version4) or four bytes (version6) + // for length + long lengthPosition = sOut.Position; + if (version == Version6) + { + sOut.WriteByte(0x00); + sOut.WriteByte(0x00); + } sOut.WriteByte(0x00); sOut.WriteByte(0x00); - } - sOut.WriteByte(0x00); - sOut.WriteByte(0x00); - SignatureSubpacket[] hashed = GetHashedSubPackets(); - for (int i = 0; i != hashed.Length; i++) - { - hashed[i].Encode(sOut); - } + SignatureSubpacket[] hashed = GetHashedSubPackets(); + for (int i = 0; i != hashed.Length; i++) + { + hashed[i].Encode(sOut); + } - ushort dataLength = Convert.ToUInt16(sOut.Position - lengthPosition - 2); - if (version == Version6) - { - dataLength -= 2; - } + ushort dataLength = Convert.ToUInt16(sOut.Position - lengthPosition - 2); + if (version == Version6) + { + dataLength -= 2; + } - uint hDataLength = Convert.ToUInt32(sOut.Position); + uint hDataLength = Convert.ToUInt32(sOut.Position); - sOut.WriteByte((byte)Version); - sOut.WriteByte(0xff); - sOut.WriteByte((byte)(hDataLength >> 24)); - sOut.WriteByte((byte)(hDataLength >> 16)); - sOut.WriteByte((byte)(hDataLength >> 8)); - sOut.WriteByte((byte)(hDataLength )); + // Additional metadata for v5 signatures + // https://www.ietf.org/archive/id/draft-ietf-openpgp-rfc4880bis-10.html#name-computing-signatures + // Only for document signatures (type 0x00 or 0x01) the following three data items are + // hashed here: + // * the one-octet content format, + // * the file name as a string (one octet length, followed by the file name) + // * a four-octet number that indicates a date, + // The three data items hashed for document signatures need to mirror the values of the + // Literal Data packet. + // For detached and cleartext signatures 6 zero bytes are hashed instead. - // Reset position and fill in length - sOut.Position = lengthPosition; - if (version == Version6) - { - sOut.WriteByte((byte)(dataLength >> 24)); - sOut.WriteByte((byte)(dataLength >> 16)); - } - sOut.WriteByte((byte)(dataLength >> 8)); - sOut.WriteByte((byte)(dataLength )); + if (version == Version5 && (signatureType == 0x00 || signatureType == 0x01)) + { + if (additionalMetadata != null && additionalMetadata.Length > 0) + { + sOut.Write(additionalMetadata, 0, additionalMetadata.Length); + } + else + { + sOut.WriteByte(0x00); + sOut.WriteByte(0x00); + sOut.WriteByte(0x00); + sOut.WriteByte(0x00); + sOut.WriteByte(0x00); + sOut.WriteByte(0x00); + } + } + + sOut.WriteByte((byte)Version); + sOut.WriteByte(0xff); + + if (version == Version5) + { + sOut.WriteByte((byte)((ulong)hDataLength >> 56)); + sOut.WriteByte((byte)((ulong)hDataLength >> 48)); + sOut.WriteByte((byte)((ulong)hDataLength >> 40)); + sOut.WriteByte((byte)((ulong)hDataLength >> 32)); + } + sOut.WriteByte((byte)(hDataLength >> 24)); + sOut.WriteByte((byte)(hDataLength >> 16)); + sOut.WriteByte((byte)(hDataLength >> 8)); + sOut.WriteByte((byte)(hDataLength )); - return sOut.ToArray(); + // Reset position and fill in length + sOut.Position = lengthPosition; + if (version == Version6) + { + sOut.WriteByte((byte)(dataLength >> 24)); + sOut.WriteByte((byte)(dataLength >> 16)); + } + sOut.WriteByte((byte)(dataLength >> 8)); + sOut.WriteByte((byte)(dataLength )); + + return sOut.ToArray(); + } } public PublicKeyAlgorithmTag KeyAlgorithm => keyAlgorithm; diff --git a/crypto/src/openpgp/PgpLiteralData.cs b/crypto/src/openpgp/PgpLiteralData.cs index 92fafe657..f9abb5ed4 100644 --- a/crypto/src/openpgp/PgpLiteralData.cs +++ b/crypto/src/openpgp/PgpLiteralData.cs @@ -62,5 +62,43 @@ public Stream GetDataStream() { return GetInputStream(); } + + /// + /// Additional metadata for v5 signatures + /// https://www.ietf.org/archive/id/draft-ietf-openpgp-rfc4880bis-10.html#name-computing-signatures + /// Only for document signatures (type 0x00 or 0x01) the following three data items are hashed: + /// * the one-octet content format, + /// * the file name as a string (one octet length, followed by the file name) + /// * a four-octet number that indicates a date, + /// The three data items hashed for document signatures need to mirror the values of the + /// Literal Data packet. + /// For detached and cleartext signatures 6 zero bytes are hashed instead. + /// + /// Signature version + /// + public byte[] GetMetadata(int sigVersion) + { + // only v5 signatures requires additional metadata + if (sigVersion != SignaturePacket.Version5) + { + return Array.Empty(); + } + + using (var ms = new MemoryStream()) + { + byte[] rawFileName = data.GetRawFileName(); + long modTime = data.ModificationTime / 1000; + ms.WriteByte((byte)Format); + ms.WriteByte((byte)rawFileName.Length); + ms.Write(rawFileName, 0, rawFileName.Length); + + ms.WriteByte((byte)(modTime >> 24)); + ms.WriteByte((byte)(modTime >> 16)); + ms.WriteByte((byte)(modTime >> 8)); + ms.WriteByte((byte)modTime); + + return ms.ToArray(); + } + } } } diff --git a/crypto/src/openpgp/PgpOnePassSignature.cs b/crypto/src/openpgp/PgpOnePassSignature.cs index cbc889ca0..704cc7bd9 100644 --- a/crypto/src/openpgp/PgpOnePassSignature.cs +++ b/crypto/src/openpgp/PgpOnePassSignature.cs @@ -1,14 +1,11 @@ -using System; -using System.IO; - using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; -using Org.BouncyCastle.Utilities; +using System; +using System.IO; namespace Org.BouncyCastle.Bcpg.OpenPgp { - /// A one pass signature object. + /// A one pass signature object. public class PgpOnePassSignature { private static OnePassSignaturePacket Cast(Packet packet) @@ -151,6 +148,11 @@ public void Update(ReadOnlySpan input) /// Verify the calculated signature against the passed in PgpSignature. public bool Verify(PgpSignature pgpSig) + { + return Verify(pgpSig, Array.Empty()); + } + + public bool Verify(PgpSignature pgpSig, byte[] additionalMetadata) { // the versions of the Signature and the One-Pass Signature must be aligned as specified in // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#signed-message-versions @@ -162,8 +164,8 @@ public bool Verify(PgpSignature pgpSig) { return false; } - - byte[] trailer = pgpSig.GetSignatureTrailer(); + // Additional metadata for v5 signatures + byte[] trailer = pgpSig.GetSignatureTrailer(additionalMetadata); sig.BlockUpdate(trailer, 0, trailer.Length); @@ -175,7 +177,12 @@ public long KeyId get { return sigPack.KeyId; } } - public int SignatureType + public int Version + { + get { return sigPack.Version; } + } + + public int SignatureType { get { return sigPack.SignatureType; } } diff --git a/crypto/src/openpgp/PgpSignature.cs b/crypto/src/openpgp/PgpSignature.cs index 32126a46a..b543bb9d2 100644 --- a/crypto/src/openpgp/PgpSignature.cs +++ b/crypto/src/openpgp/PgpSignature.cs @@ -213,7 +213,17 @@ public bool Verify() return sig.VerifySignature(GetSignature()); } - private void UpdateWithIdData(int header, byte[] idBytes) + public bool Verify(byte[] additionalMetadata) + { + // Additional metadata for v5 signatures + byte[] trailer = sigPck.GetSignatureTrailer(additionalMetadata); + + sig.BlockUpdate(trailer, 0, trailer.Length); + + return sig.VerifySignature(GetSignature()); + } + + private void UpdateWithIdData(int header, byte[] idBytes) { this.Update( (byte) header, @@ -359,6 +369,12 @@ public byte[] GetSignatureTrailer() return sigPck.GetSignatureTrailer(); } + public byte[] GetSignatureTrailer(byte[] additionalMetadata) + { + // Additional metadata for v5 signatures + return sigPck.GetSignatureTrailer(additionalMetadata); + } + public byte[] GetSignatureSalt() { return sigPck.GetSignatureSalt(); diff --git a/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs b/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs new file mode 100644 index 000000000..351304752 --- /dev/null +++ b/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs @@ -0,0 +1,448 @@ +using NUnit.Framework; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.Utilities.Test; +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace Org.BouncyCastle.Bcpg.OpenPgp.Tests +{ + [TestFixture] + public class PgpInteroperabilityTestSuite + : SimpleTest + { + // v4 key "Alice" from "OpenPGP Example Keys and Certificates" + // https://www.ietf.org/archive/id/draft-bre-openpgp-samples-01.html#name-alices-ed25519-samples + private static readonly byte[] alicePubkey = Base64.Decode( + "mDMEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U" + + "b7O1u120JkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+iJAE" + + "ExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy" + + "MVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO" + + "dypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gK4" + + "OARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s" + + "E9+eviIDAQgHiHgEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb" + + "DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn" + + "0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE="); + + private static readonly byte[] aliceSecretkey = Base64.Decode( + "lFgEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U" + + "b7O1u10AAP9XBeW6lzGOLx7zHH9AsUDUTb2pggYGMzd0P3ulJ2AfvQ4RtCZBbGlj" + + "ZSBMb3ZlbGFjZSA8YWxpY2VAb3BlbnBncC5leGFtcGxlPoiQBBMWCAA4AhsDBQsJ" + + "CAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE64W7X6M6deFelE5j8jFVDE9H444FAl2l" + + "nzoACgkQ8jFVDE9H447pKwD6A5xwUqIDprBzrHfahrImaYEZzncqb25vkLV2arYf" + + "a78A/R3AwtLQvjxwLDuzk4dUtUwvUYibL2sAHwj2kGaHnfICnF0EXEcE6RIKKwYB" + + "BAGXVQEFAQEHQEL/BiGtq0k84Km1wqQw2DIikVYrQrMttN8d7BPfnr4iAwEIBwAA" + + "/3/xFPG6U17rhTuq+07gmEvaFYKfxRB6sgAYiW6TMTpQEK6IeAQYFggAIBYhBOuF" + + "u1+jOnXhXpROY/IxVQxPR+OOBQJcRwTpAhsMAAoJEPIxVQxPR+OOWdABAMUdSzpM" + + "hzGs1O0RkWNQWbUzQ8nUOeD9wNbjE3zR+yfRAQDbYqvtWQKN4AQLTxVJN5X5AWyb" + + "Pnn+We1aTBhaGa86AQ=="); + + // v6 keys from crypto-refresh + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-secret-key-transf + private static readonly byte[] v6Certificate = Base64.Decode( + "xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf" + + "GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy" + + "KwwfHifBilZwj2Ul7Ce62azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lw" + + "gyU2kCcUmKfvBXbAf6rhRYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaE" + + "QsiPlR4zxP/TP7mhfVEe7XWPxtnMUMtf15OyA51YBM4qBmOHf+MZAAAAIIaTJINn" + + "+eUBXbki+PSAld2nhJh/LVmFsS+60WyvXkQ1wpsGGBsKAAAALAWCY4d/4wKbDCIh" + + "BssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAAQBIKbpGG2dWTX8" + + "j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805" + + "I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg=="); + + private static readonly byte[] v6UnlockedSecretKey = Base64.Decode( + "xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB" + + "exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ" + + "BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6" + + "2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh" + + "RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe" + + "7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/" + + "LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG" + + "GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6" + + "2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE" + + "M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr" + + "k0mXubZvyl4GBg=="); + + // v5 keys from "OpenPGP interoperability test suite" + // https://tests.sequoia-pgp.org/#Inline_Sign-Verify_roundtrip_with_key__Emma_ + private static readonly byte[] v5Certificate = Base64.Decode( + "mDcFXJH05BYAAAAtCSsGAQQB2kcPAQEHQFhZlVcVVtwf+21xNQPX+ecMJJBL0MPd" + + "fj75iux+my8QtBhlbW1hLmdvbGRtYW5AZXhhbXBsZS5uZXSIlgUTFggASCIhBRk0" + + "e8mHJGQCX5nfPsLgAA7ZiEiS4fez6kyUAJFZVptUBQJckfTkAhsDBQsJCAcCAyIC" + + "AQYVCgkICwIEFgIDAQIeBwIXgAAA9cAA/jiR3yMsZMeEQ40u6uzEoXa6UXeV/S3w" + + "wJAXRJy9M8s0AP9vuL/7AyTfFXwwzSjDnYmzS0qAhbLDQ643N+MXGBJ2Bbg8BVyR" + + "9OQSAAAAMgorBgEEAZdVAQUBAQdA+nysrzml2UCweAqtpDuncSPlvrcBWKU0yfU0" + + "YvYWWAoDAQgHiHoFGBYIACwiIQUZNHvJhyRkAl+Z3z7C4AAO2YhIkuH3s+pMlACR" + + "WVabVAUCXJH05AIbDAAAOSQBAP4BOOIR/sGLNMOfeb5fPs/02QMieoiSjIBnijho" + + "b2U5AQC+RtOHCHx7TcIYl5/Uyoi+FOvPLcNw4hOv2nwUzSSVAw=="); + + private static readonly byte[] v5UnlockedSecretKey = Base64.Decode( + "lGEFXJH05BYAAAAtCSsGAQQB2kcPAQEHQFhZlVcVVtwf+21xNQPX+ecMJJBL0MPd" + + "fj75iux+my8QAAAAAAAiAQCHZ1SnSUmWqxEsoI6facIVZQu6mph3cBFzzTvcm5lA" + + "Ng5ctBhlbW1hLmdvbGRtYW5AZXhhbXBsZS5uZXSIlgUTFggASCIhBRk0e8mHJGQC" + + "X5nfPsLgAA7ZiEiS4fez6kyUAJFZVptUBQJckfTkAhsDBQsJCAcCAyICAQYVCgkI" + + "CwIEFgIDAQIeBwIXgAAA9cAA/jiR3yMsZMeEQ40u6uzEoXa6UXeV/S3wwJAXRJy9" + + "M8s0AP9vuL/7AyTfFXwwzSjDnYmzS0qAhbLDQ643N+MXGBJ2BZxmBVyR9OQSAAAA" + + "MgorBgEEAZdVAQUBAQdA+nysrzml2UCweAqtpDuncSPlvrcBWKU0yfU0YvYWWAoD" + + "AQgHAAAAAAAiAP9OdAPppjU1WwpqjIItkxr+VPQRT8Zm/Riw7U3F6v3OiBFHiHoF" + + "GBYIACwiIQUZNHvJhyRkAl+Z3z7C4AAO2YhIkuH3s+pMlACRWVabVAUCXJH05AIb" + + "DAAAOSQBAP4BOOIR/sGLNMOfeb5fPs/02QMieoiSjIBnijhob2U5AQC+RtOHCHx7" + + "TcIYl5/Uyoi+FOvPLcNw4hOv2nwUzSSVAw=="); + + private static readonly char[] emptyPassphrase = Array.Empty(); + + private static PgpSignatureGenerator CreateAndInitPgpSignatureGenerator(PgpSecretKey signingKey, HashAlgorithmTag hashAlgo, char[] passphrase) + { + PgpSignatureGenerator generator = new PgpSignatureGenerator(signingKey.PublicKey.Algorithm, hashAlgo); + PgpPrivateKey privKey = signingKey.ExtractPrivateKey(passphrase); + generator.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); + + return generator; + } + + private static PgpPublicKeyRingBundle CreateBundle(params PgpPublicKeyRing[] keyrings) + { + using (MemoryStream ms = new MemoryStream()) + { + foreach (var keyring in keyrings) + { + keyring.Encode(ms); + } + return new PgpPublicKeyRingBundle(ms.ToArray()); + } + } + + private void VerifyMultipleInlineSignaturesTest(byte[] message, PgpPublicKeyRingBundle bundle, bool shouldFail = false) + { + PgpObjectFactory factory = new PgpObjectFactory(message); + PgpOnePassSignatureList opss = factory.NextPgpObject() as PgpOnePassSignatureList; + for (int i = 0; i < opss.Count; i++) + { + PgpOnePassSignature ops = opss[i]; + ops.InitVerify(bundle.GetPublicKey(ops.KeyId)); + } + + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (Stream dIn = lit.GetInputStream()) + { + + byte[] buffer = new byte[30]; + int bytesRead; + while ((bytesRead = dIn.Read(buffer, 0, buffer.Length)) > 0) + { + for (int i = 0; i < opss.Count; i++) + { + opss[i].Update(buffer, 0, bytesRead); + } + } + } + + PgpSignatureList sigs = factory.NextPgpObject() as PgpSignatureList; + IsEquals(opss.Count, sigs.Count); + int sigCount = sigs.Count - 1; + for (int i = 0; i <= sigCount; i++) + { + IsTrue(shouldFail != opss[i].Verify(sigs[sigCount - i])); + } + } + + [Test] + public void MultipleInlineSignatureTest() + { + // Verify Inline Signature with multiple keys: + // v6 key from crypto-refresh and v4 key "Alice" from "OpenPGP Example Keys and Certificates" + // https://tests.sequoia-pgp.org/#Inline_Sign_with_minimal_key_from_RFC9760_and_key__Alice___verify_with_key_from_RFC9760 + + // inline signed message generated by GopenPGP 3.0.0-alpha + byte[] message = Base64.Decode( + "xEYGAAobIPdza3bN03j7U7LE/Q/46kHCmsfVx2UmTPsNpUk/V/UWyxhsTwYJppfk" + + "1S36bHIrDB8eJ8GKVnCPZSXsJ7rZrMkAxA0DAAoW8jFVDE9H444ByxRiAAAAAABI" + + "ZWxsbyBXb3JsZCA6KcJ1BAAWCgAnBQJl4HbKCRDyMVUMT0fjjhYhBOuFu1+jOnXh" + + "XpROY/IxVQxPR+OOAACKGAEAsQpg3dNdO4C9eMGn1jvVTjP0r2welMFD68dFU5d8" + + "nq8A+gNFdJbX0PP0vNx/kIxpilbdssnF+a04CdVpAkwXmaYPwpgGABsKAAAAKQUC" + + "ZeB2yiKhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAACB3IPdz" + + "a3bN03j7U7LE/Q/46kHCmsfVx2UmTPsNpUk/V/UWJsjxFqBQXqDFAaOjiv8oabeX" + + "qvELkq1bKLb9fJ+ASfZW9FyI1ORHdCrI5zEnpfrFe4Id+xg9N39MTGq+OoPeDA=="); + + PgpPublicKeyRingBundle bundle = CreateBundle( + new PgpPublicKeyRing(alicePubkey), + new PgpPublicKeyRing(v6Certificate)); + + VerifyMultipleInlineSignaturesTest(message, bundle); + + // inline signed message generated by PGPy 0.6.0+dkg-crypto-refresh + message = Base64.Decode( + "xA0DAAoW8jFVDE9H444AxEYGAAobIFWUOmg2wsfVON4qIM1sWUPd9223ANjaMnHT" + + "Mvad9EfVyxhsTwYJppfk1S36bHIrDB8eJ8GKVnCPZSXsJ7rZrMkByxRiAGXgds5I" + + "ZWxsbyBXb3JsZCA6KcKYBgAbCgAAACkFgmXgds4iIQbLGGxPBgmml+TVLfpscisM" + + "Hx4nwYpWcI9lJewnutmsyQAAAACSWCBVlDpoNsLH1TjeKiDNbFlD3fdttwDY2jJx" + + "0zL2nfRH1aouTY4WN/3DFsfP8yFg/BE7Ssaikt7bbXtBSH/AldOtyM1myiFsP+yx" + + "8Img2A7eq9+wKTLjhPHl7zSh7y9KEATCdQQAFgoAHQWCZeB2zhYhBOuFu1+jOnXh" + + "XpROY/IxVQxPR+OOAAoJEPIxVQxPR+OOgDcBAOz0kSpV4/F9Exxdq6oYlHZdsX5U" + + "n9QpjmJVjo7bsMGDAQCd3PA5joXmfoKQhtQT5Qm1dhjfv/c89oPzdjQYmVLnCg=="); + + VerifyMultipleInlineSignaturesTest(message, bundle); + } + + [Test] + public void GenerateAndVerifyMultipleInlineSignatureTest() + { + // Inline Sign-Verify roundtrip test with multiple keys: + // v6 key from crypto-refresh and v4 key "Alice" from "OpenPGP Example Keys and Certificates" + byte[] data = Encoding.UTF8.GetBytes("Hello World :)"); + byte[] message; + + PgpSecretKey[] signingKeys = new PgpSecretKey[] { + new PgpSecretKeyRing(v6UnlockedSecretKey).GetSecretKey(), + new PgpSecretKeyRing(aliceSecretkey).GetSecretKey() + }; + + PgpSignatureGenerator[] generators = new PgpSignatureGenerator[] { + CreateAndInitPgpSignatureGenerator(signingKeys[0], HashAlgorithmTag.Sha384, emptyPassphrase), + CreateAndInitPgpSignatureGenerator(signingKeys[1], HashAlgorithmTag.Sha256, emptyPassphrase) + }; + + using (MemoryStream ms = new MemoryStream()) + { + using (BcpgOutputStream bcOut = new BcpgOutputStream(ms, newFormatOnly: true)) + { + int sigCount = generators.Length; + int count = 1; + foreach (PgpSignatureGenerator generator in generators) + { + generator.GenerateOnePassVersion(count != sigCount).Encode(bcOut); + count++; + } + + PgpLiteralDataGenerator lGen = new PgpLiteralDataGenerator(); + DateTime modificationTime = DateTime.UtcNow; + using (var lOut = lGen.Open( + new UncloseableStream(bcOut), + PgpLiteralData.Utf8, + "_CONSOLE", + data.Length, + modificationTime)) + { + lOut.Write(data, 0, data.Length); + + foreach (PgpSignatureGenerator generator in generators) + { + generator.Update(data); + } + } + + foreach (PgpSignatureGenerator generator in generators.Reverse()) + { + generator.Generate().Encode(bcOut); + } + } + + message = ms.ToArray(); + } + + PgpPublicKeyRingBundle bundle = CreateBundle( + new PgpPublicKeyRing(alicePubkey), + new PgpPublicKeyRing(v6Certificate)); + + VerifyMultipleInlineSignaturesTest(message, bundle); + + //corrupt data; + message[95] = 0x50; + VerifyMultipleInlineSignaturesTest(message, bundle, shouldFail: true); + } + + private void VerifyMultipleDetachedSignaturesTest(byte[] signaturePacket, byte[] data, PgpPublicKeyRingBundle bundle, bool shouldFail = false) + { + PgpObjectFactory factory = new PgpObjectFactory(signaturePacket); + PgpSignatureList sigs = factory.NextPgpObject() as PgpSignatureList; + + IsEquals(sigs.Count, 2); + for (int i = 0; i < sigs.Count; i++) + { + PgpSignature sig = sigs[i]; + sig.InitVerify(bundle.GetPublicKey(sig.KeyId)); + sig.Update(data); + + IsTrue(shouldFail != sig.Verify()); + } + } + + [Test] + public void MultipleDetachedSignatureTest() + { + // Verify Detached Signature with multiple keys: + // v6 key from crypto-refresh and v4 key "Alice" from "OpenPGP Example Keys and Certificates" + // https://tests.sequoia-pgp.org/#Detached_Sign_with_minimal_key_from_RFC9760_and_key__Alice___verify_with_key_from_RFC9760 + + byte[] data = Encoding.UTF8.GetBytes("Hello World :)"); + byte[] corruptedData = Encoding.UTF8.GetBytes("Hello World :("); + + PgpPublicKeyRingBundle bundle = CreateBundle( + new PgpPublicKeyRing(alicePubkey), + new PgpPublicKeyRing(v6Certificate)); + + // Detached Signature generated by GopenPGP 3.0.0-alpha + byte[] signaturePacket = Base64.Decode( + "wpgGABsKAAAAKQUCZeB2zCKhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6" + + "2azJAAAAAEPPIIh5xfXDp5Zmfa7KJ0S+3Z+RBO9j5AC33ZRAwGgWKVuBts2H+I0k" + + "GlIQXoyX+2LnurlGQGxZRqwk/z2d4Tk8oAA62CuJ318aZdo8Z4utdmHvsWlluAWl" + + "lh0XdZ5l/qBNC8J1BAAWCgAnBQJl4HbMCRDyMVUMT0fjjhYhBOuFu1+jOnXhXpRO" + + "Y/IxVQxPR+OOAABPnQEA881lXU6DUMYbXx3rmGa5qSQld9pHxzRYtBT/WCfkzVwA" + + "/0/PN5jncrytAiEjb6YwuZuTVjJdTy6xtzuH+XALdREG"); + + VerifyMultipleDetachedSignaturesTest(signaturePacket, data, bundle); + VerifyMultipleDetachedSignaturesTest(signaturePacket, corruptedData, bundle, shouldFail: true); + + // Detached Signature generated by PGPy 0.6.0+dkg-crypto-refresh + signaturePacket = Base64.Decode( + "wpgGABsKAAAAKQWCZeB20SIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6" + + "2azJAAAAADUkIIqFiPBBvz4Uqsug38k/hVaFdHoHfy82ESRfutwk1ch+TaG8Kk2I" + + "7IMcrzKKSp60I7MEGb5CUCzeeM4v883yXlzZhwiBl+enR8kHxcVZzH+z7aS3OptN" + + "mrcay8CfwzHJD8J1BAAWCgAdBYJl4HbRFiEE64W7X6M6deFelE5j8jFVDE9H444A" + + "CgkQ8jFVDE9H447lbQEAx8hE9sbx1s8kMwuuEUtvoayJyz6R3PyQAIGH72g9XNcA" + + "/32a6SYBHAHl8HOrlkZWzUwaIyhOcI5jN6ppiKRZAL8O"); + + VerifyMultipleDetachedSignaturesTest(signaturePacket, data, bundle); + VerifyMultipleDetachedSignaturesTest(signaturePacket, corruptedData, bundle, shouldFail: true); + } + + + [Test] + public void GenerateAndVerifyMultipleDetachedSignatureTest() + { + // Inline Sign-Verify roundtrip test with multiple keys: + // v6 key from crypto-refresh and v4 key "Alice" from "OpenPGP Example Keys and Certificates" + + byte[] data = Encoding.UTF8.GetBytes("Hello World :)"); + byte[] corruptedData = Encoding.UTF8.GetBytes("Hello World :("); + byte[] signaturePacket; + + PgpSecretKey[] signingKeys = new PgpSecretKey[] { + new PgpSecretKeyRing(v6UnlockedSecretKey).GetSecretKey(), + new PgpSecretKeyRing(aliceSecretkey).GetSecretKey() + }; + + PgpSignatureGenerator[] generators = new PgpSignatureGenerator[] { + CreateAndInitPgpSignatureGenerator(signingKeys[0], HashAlgorithmTag.Sha3_512, emptyPassphrase), + CreateAndInitPgpSignatureGenerator(signingKeys[1], HashAlgorithmTag.Sha224, emptyPassphrase) + }; + + using (MemoryStream ms = new MemoryStream()) + { + using (BcpgOutputStream bcOut = new BcpgOutputStream(ms, newFormatOnly: true)) + { + foreach (PgpSignatureGenerator generator in generators) + { + generator.Update(data); + generator.Generate().Encode(bcOut); + } + } + + signaturePacket = ms.ToArray(); + } + + PgpPublicKeyRingBundle bundle = CreateBundle( + new PgpPublicKeyRing(alicePubkey), + new PgpPublicKeyRing(v6Certificate)); + + VerifyMultipleDetachedSignaturesTest(signaturePacket, data, bundle); + VerifyMultipleDetachedSignaturesTest(signaturePacket, corruptedData, bundle, shouldFail: true); + } + + [Test] + public void Version5KeyParsingTest() + { + string uid = "emma.goldman@example.net"; + PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v5Certificate); + PgpPublicKey[] pubKeys = pubRing.GetPublicKeys().ToArray(); + IsEquals("wrong number of public keys", pubKeys.Length, 2); + + PgpPublicKey masterKey = pubKeys[0]; + PgpPublicKey subKey = pubKeys[1]; + + IsTrue(masterKey.IsMasterKey); + IsTrue(subKey.IsEncryptionKey); + IsEquals(masterKey.Algorithm, PublicKeyAlgorithmTag.EdDsa_Legacy); + IsEquals(subKey.Algorithm, PublicKeyAlgorithmTag.ECDH); + + IsTrue(masterKey.GetUserIds().Contains(uid)); + IsTrue(!masterKey.GetUserIds().Contains("emma.g@example.net")); + + IsEquals(masterKey.KeyId, 0x19347BC987246402); + IsEquals((ulong)subKey.KeyId, 0xE4557C2B02FFBF4B); + IsTrue(AreEqual(masterKey.GetFingerprint(), Hex.Decode("19347BC9872464025F99DF3EC2E0000ED9884892E1F7B3EA4C94009159569B54"))); + IsTrue(AreEqual(subKey.GetFingerprint(), Hex.Decode("E4557C2B02FFBF4B04F87401EC336AF7133D0F85BE7FD09BAEFD9CAEB8C93965"))); + + // verify v5 self sig + PgpSignature signature = masterKey.GetSignaturesForId(uid).ToArray()[0]; + IsEquals(signature.Version, SignaturePacket.Version5); + IsEquals(signature.SignatureType, PgpSignature.PositiveCertification); + signature.InitVerify(masterKey); + IsTrue(signature.VerifyCertification(uid, masterKey)); + + // verify subkey binding sig + signature = subKey.GetSignatures().ToArray()[0]; + IsEquals(signature.Version, SignaturePacket.Version5); + IsEquals(signature.SignatureType, PgpSignature.SubkeyBinding); + signature.InitVerify(masterKey); + IsTrue(signature.VerifyCertification(masterKey, subKey)); + } + + [Test] + public void Version5InlineSignatureTest() + { + // Verify v5 Inline Signature generated by OpenPGP.js 5.5.0 + // https://tests.sequoia-pgp.org/#Inline_Sign-Verify_roundtrip_with_key__Emma_ + byte[] message = Base64.Decode( + "xA0DAQoWGTR7yYckZAIByxR1AGXgdslIZWxsbyBXb3JsZCA6KcJ3BQEWCgAp" + + "BQJl4HbJIiEFGTR7yYckZAJfmd8+wuAADtmISJLh97PqTJQAkVlWm1QAADsI" + + "AQD7aH9a0GKcHdFThMsOQ88xAM5PiqPyDV1A/K23rPN28wD/QoPa1yEE3Y2R" + + "ZtqtH6jAymdyIwtsa5wLvzUjTmP5OQo="); + + PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v5Certificate); + PgpPublicKey signer = pubRing.GetPublicKey(); + + PgpObjectFactory factory = new PgpObjectFactory(message); + + PgpOnePassSignatureList opss = factory.NextPgpObject() as PgpOnePassSignatureList; + IsEquals(opss.Count, 1); + PgpOnePassSignature ops = opss[0]; + IsEquals(ops.Version, OnePassSignaturePacket.Version3); + + ops.InitVerify(signer); + PgpLiteralData literal = factory.NextPgpObject() as PgpLiteralData; + using (Stream dIn = literal.GetInputStream()) + { + byte[] buffer = new byte[30]; + int bytesRead; + while ((bytesRead = dIn.Read(buffer, 0, buffer.Length)) > 0) + { + ops.Update(buffer, 0, bytesRead); + } + } + + PgpSignatureList sigs = factory.NextPgpObject() as PgpSignatureList; + IsEquals(sigs.Count, 1); + byte[] metadata = literal.GetMetadata(sigs[0].Version); + IsTrue(ops.Verify(sigs[0], metadata)); + } + + + public override string Name => "PgpInteroperabilityTestSuite"; + + public override void PerformTest() + { + MultipleInlineSignatureTest(); + GenerateAndVerifyMultipleInlineSignatureTest(); + + MultipleDetachedSignatureTest(); + GenerateAndVerifyMultipleDetachedSignatureTest(); + + Version5KeyParsingTest(); + Version5InlineSignatureTest(); + } + } +} From 6c933f243dc90b24501e962c1e065542c5dd7dee Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Sun, 3 Mar 2024 14:57:43 +0100 Subject: [PATCH 16/37] generation of v6 OpenPGP keypairs --- crypto/src/openpgp/PgpKeyPair.cs | 31 +++- crypto/src/openpgp/PgpKeyRingGenerator.cs | 18 +-- crypto/src/openpgp/PgpSecretKey.cs | 30 +++- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 149 +++++++++++++++++- 4 files changed, 205 insertions(+), 23 deletions(-) diff --git a/crypto/src/openpgp/PgpKeyPair.cs b/crypto/src/openpgp/PgpKeyPair.cs index 65ef5e829..bfa2e44c2 100644 --- a/crypto/src/openpgp/PgpKeyPair.cs +++ b/crypto/src/openpgp/PgpKeyPair.cs @@ -27,19 +27,38 @@ public PgpKeyPair( { } - public PgpKeyPair( + public PgpKeyPair( + int version, + PublicKeyAlgorithmTag algorithm, + AsymmetricCipherKeyPair keyPair, + DateTime time) + : this(version, algorithm, keyPair.Public, keyPair.Private, time) + { + } + + public PgpKeyPair( PublicKeyAlgorithmTag algorithm, AsymmetricKeyParameter pubKey, AsymmetricKeyParameter privKey, DateTime time) + :this(PublicKeyPacket.DefaultVersion, algorithm, pubKey, privKey, time) + { + } + + public PgpKeyPair( + int version, + PublicKeyAlgorithmTag algorithm, + AsymmetricKeyParameter pubKey, + AsymmetricKeyParameter privKey, + DateTime time) { - this.pub = new PgpPublicKey(algorithm, pubKey, time); - this.priv = new PgpPrivateKey(pub, privKey); + this.pub = new PgpPublicKey(version, algorithm, pubKey, time); + this.priv = new PgpPrivateKey(pub, privKey); } - /// Create a key pair from a PgpPrivateKey and a PgpPublicKey. - /// The public key. - /// The private key. + /// Create a key pair from a PgpPrivateKey and a PgpPublicKey. + /// The public key. + /// The private key. public PgpKeyPair( PgpPublicKey pub, PgpPrivateKey priv) diff --git a/crypto/src/openpgp/PgpKeyRingGenerator.cs b/crypto/src/openpgp/PgpKeyRingGenerator.cs index a04ebc7df..9f9bbd6ff 100644 --- a/crypto/src/openpgp/PgpKeyRingGenerator.cs +++ b/crypto/src/openpgp/PgpKeyRingGenerator.cs @@ -1,15 +1,13 @@ +using Org.BouncyCastle.Security; using System; using System.Collections.Generic; -using Org.BouncyCastle.Security; -using Org.BouncyCastle.Utilities; - namespace Org.BouncyCastle.Bcpg.OpenPgp { - /// - /// Generator for a PGP master and subkey ring. - /// This class will generate both the secret and public key rings - /// + /// + /// Generator for a PGP master and subkey ring. + /// This class will generate both the secret and public key rings + /// public class PgpKeyRingGenerator { private IList keys = new List(); @@ -300,7 +298,7 @@ public void AddSubKey( // // Generate the certification // - sGen.InitSign(PgpSignature.SubkeyBinding, masterKey.PrivateKey); + sGen.InitSign(PgpSignature.SubkeyBinding, masterKey.PrivateKey, rand); sGen.SetHashedSubpackets(hashedPackets); sGen.SetUnhashedSubpackets(unhashedPackets); @@ -346,12 +344,12 @@ public void AddSubKey( // // Generate the certification // - sGen.InitSign(PgpSignature.SubkeyBinding, masterKey.PrivateKey); + sGen.InitSign(PgpSignature.SubkeyBinding, masterKey.PrivateKey, rand); // add primary key binding sub packet PgpSignatureGenerator pGen = new PgpSignatureGenerator(keyPair.PublicKey.Algorithm, primaryKeyBindingHashAlgorithm); - pGen.InitSign(PgpSignature.PrimaryKeyBinding, keyPair.PrivateKey); + pGen.InitSign(PgpSignature.PrimaryKeyBinding, keyPair.PrivateKey, rand); PgpSignatureSubpacketGenerator spGen = new PgpSignatureSubpacketGenerator(hashedPackets); diff --git a/crypto/src/openpgp/PgpSecretKey.cs b/crypto/src/openpgp/PgpSecretKey.cs index e93023f33..b2a3d1a93 100644 --- a/crypto/src/openpgp/PgpSecretKey.cs +++ b/crypto/src/openpgp/PgpSecretKey.cs @@ -110,6 +110,22 @@ internal PgpSecretKey( ElGamalPrivateKeyParameters esK = (ElGamalPrivateKeyParameters) privKey.Key; secKey = new ElGamalSecretBcpgKey(esK.X); break; + case PublicKeyAlgorithmTag.Ed25519: + Ed25519PrivateKeyParameters e25519pk = (Ed25519PrivateKeyParameters)privKey.Key; + secKey = new Ed25519SecretBcpgKey(e25519pk.GetEncoded()); + break; + case PublicKeyAlgorithmTag.Ed448: + Ed448PrivateKeyParameters e448pk = (Ed448PrivateKeyParameters)privKey.Key; + secKey = new Ed448SecretBcpgKey(e448pk.GetEncoded()); + break; + case PublicKeyAlgorithmTag.X25519: + X25519PrivateKeyParameters x25519pk = (X25519PrivateKeyParameters)privKey.Key; + secKey = new X25519SecretBcpgKey(x25519pk.GetEncoded()); + break; + case PublicKeyAlgorithmTag.X448: + X448PrivateKeyParameters x448pk = (X448PrivateKeyParameters)privKey.Key; + secKey = new X448SecretBcpgKey(x448pk.GetEncoded()); + break; default: throw new PgpException("unknown key class"); } @@ -243,7 +259,7 @@ internal PgpSecretKey( PgpSignatureSubpacketVector hashedPackets, PgpSignatureSubpacketVector unhashedPackets, SecureRandom rand) - : this(keyPair.PrivateKey, CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets), + : this(keyPair.PrivateKey, CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets, rand), encAlgorithm, rawPassPhrase, clearPassPhrase, useSha1, rand, true) { } @@ -319,7 +335,7 @@ internal PgpSecretKey( PgpSignatureSubpacketVector hashedPackets, PgpSignatureSubpacketVector unhashedPackets, SecureRandom rand) - : this(keyPair.PrivateKey, CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets, hashAlgorithm), + : this(keyPair.PrivateKey, CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets, hashAlgorithm, rand), encAlgorithm, rawPassPhrase, clearPassPhrase, useSha1, rand, true) { } @@ -329,7 +345,8 @@ private static PgpPublicKey CertifiedPublicKey( PgpKeyPair keyPair, string id, PgpSignatureSubpacketVector hashedPackets, - PgpSignatureSubpacketVector unhashedPackets) + PgpSignatureSubpacketVector unhashedPackets, + SecureRandom rand) { PgpSignatureGenerator sGen; try @@ -344,7 +361,7 @@ private static PgpPublicKey CertifiedPublicKey( // // Generate the certification // - sGen.InitSign(certificationLevel, keyPair.PrivateKey); + sGen.InitSign(certificationLevel, keyPair.PrivateKey, rand); sGen.SetHashedSubpackets(hashedPackets); sGen.SetUnhashedSubpackets(unhashedPackets); @@ -367,7 +384,8 @@ private static PgpPublicKey CertifiedPublicKey( string id, PgpSignatureSubpacketVector hashedPackets, PgpSignatureSubpacketVector unhashedPackets, - HashAlgorithmTag hashAlgorithm) + HashAlgorithmTag hashAlgorithm, + SecureRandom rand) { PgpSignatureGenerator sGen; try @@ -382,7 +400,7 @@ private static PgpPublicKey CertifiedPublicKey( // // Generate the certification // - sGen.InitSign(certificationLevel, keyPair.PrivateKey); + sGen.InitSign(certificationLevel, keyPair.PrivateKey, rand); sGen.SetHashedSubpackets(hashedPackets); sGen.SetUnhashedSubpackets(unhashedPackets); diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 5633d9a63..1d6d45684 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -335,6 +335,152 @@ public void Version6UnlockedSecretKeyParsingTest() IsTrue(key.IsRevoked()); } + [Test] + public void Version6Ed25519KeyPairCreationTest() + { + /* + * Create a v6 Ed25519 keypair with the same key material and creation datetime as the test vector + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans + * then check the fingerprint and verify a signature + */ + byte[] keyMaterial = Hex.Decode("1972817b12be707e8d5f586ce61361201d344eb266a2c82fde6835762b65b0b7"); + Ed25519PrivateKeyParameters seckey = new Ed25519PrivateKeyParameters(keyMaterial); + Ed25519PublicKeyParameters pubkey = seckey.GeneratePublicKey(); + PgpKeyPair keypair = new PgpKeyPair(PublicKeyPacket.Version6, PublicKeyAlgorithmTag.Ed25519, pubkey, seckey, DateTime.Parse("2022-11-30 16:08:03Z")); + + IsEquals(keypair.PublicKey.Algorithm, PublicKeyAlgorithmTag.Ed25519); + IsEquals(keypair.PublicKey.CreationTime.ToString("yyyyMMddHHmmss"), "20221130160803"); + byte[] expectedFingerprint = Hex.Decode("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9"); + IsEquals((ulong)keypair.KeyId, 0xCB186C4F0609A697); + IsTrue("wrong master key fingerprint", AreEqual(keypair.PublicKey.GetFingerprint(), expectedFingerprint)); + + + VerifyEncodedSignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes(v6SampleCleartextSignedMessage), + keypair.PublicKey); + + VerifyEncodedSignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes("wrongdata"), + keypair.PublicKey, + shouldFail: true); + + // encode-decode roundtrip + + PgpKeyRingGenerator keyRingGen = new PgpKeyRingGenerator( + PgpSignature.PositiveCertification, + keypair, + "Alice ", + SymmetricKeyAlgorithmTag.Null, + Array.Empty(), + true, + null, + null, + new SecureRandom()); + + PgpSecretKeyRing secring = keyRingGen.GenerateSecretKeyRing(); + byte[] encodedsecring; + using (MemoryStream ms = new MemoryStream()) + { + secring.Encode(ms); + encodedsecring = ms.ToArray(); + } + + PgpSecretKeyRing decodedsecring = new PgpSecretKeyRing(encodedsecring); + + PgpPublicKey pgppubkey = decodedsecring.GetPublicKey(); + PgpSecretKey pgpseckey = decodedsecring.GetSecretKey(); + IsEquals(pgppubkey.Algorithm, PublicKeyAlgorithmTag.Ed25519); + IsEquals(pgppubkey.CreationTime.ToString("yyyyMMddHHmmss"), "20221130160803"); + IsEquals((ulong)pgppubkey.KeyId, 0xCB186C4F0609A697); + IsTrue("wrong master key fingerprint", AreEqual(pgppubkey.GetFingerprint(), expectedFingerprint)); + IsTrue(pgppubkey.GetUserIds().Contains("Alice ")); + + // Sign-Verify roundtrip + byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); + byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); + PgpSignatureGenerator sigGen = new PgpSignatureGenerator(pgppubkey.Algorithm, HashAlgorithmTag.Sha512); + PgpSignatureSubpacketGenerator spkGen = new PgpSignatureSubpacketGenerator(); + PgpPrivateKey privKey = pgpseckey.ExtractPrivateKey(emptyPassphrase); + spkGen.SetIssuerFingerprint(false, pgpseckey); + sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); + sigGen.Update(data); + sigGen.SetHashedSubpackets(spkGen.Generate()); + PgpSignature signature = sigGen.Generate(); + + AreEqual(signature.GetIssuerFingerprint(), expectedFingerprint); + VerifySignature(signature, data, pgppubkey); + VerifySignature(signature, wrongData, pgppubkey, shouldFail: true); + } + + [Test] + public void Version6Ed448KeyPairCreationTest() + { + /* + * Create a v6 Ed448 keypair, then perform encode-decode and sign-verify roundtrips + */ + SecureRandom rand = new SecureRandom(); + DateTime now = DateTime.UtcNow; + + Ed448KeyPairGenerator ed448gen = new Ed448KeyPairGenerator(); + ed448gen.Init(new Ed448KeyGenerationParameters(rand)); + AsymmetricCipherKeyPair kp = ed448gen.GenerateKeyPair(); + + PgpKeyPair keypair = new PgpKeyPair(PublicKeyPacket.Version6, PublicKeyAlgorithmTag.Ed448, kp, now); + IsEquals(keypair.PublicKey.Algorithm, PublicKeyAlgorithmTag.Ed448); + IsEquals(keypair.PublicKey.CreationTime.ToString("yyyyMMddHHmmss"), now.ToString("yyyyMMddHHmmss")); + long keyId = keypair.PublicKey.KeyId; + byte[] fpr = keypair.PublicKey.GetFingerprint(); + IsEquals(fpr.Length, 32); + + // encode-decode roundtrip + PgpKeyRingGenerator keyRingGen = new PgpKeyRingGenerator( + PgpSignature.PositiveCertification, + keypair, + "Alice ", + SymmetricKeyAlgorithmTag.Null, + Array.Empty(), + true, + null, + null, + rand); + + PgpSecretKeyRing secring = keyRingGen.GenerateSecretKeyRing(); + byte[] encodedsecring; + using (MemoryStream ms = new MemoryStream()) + { + secring.Encode(ms); + encodedsecring = ms.ToArray(); + } + + PgpSecretKeyRing decodedsecring = new PgpSecretKeyRing(encodedsecring); + PgpPublicKey pgppubkey = decodedsecring.GetPublicKey(); + PgpSecretKey pgpseckey = decodedsecring.GetSecretKey(); + IsEquals(pgppubkey.Algorithm, PublicKeyAlgorithmTag.Ed448); + IsEquals(pgppubkey.CreationTime.ToString("yyyyMMddHHmmss"), now.ToString("yyyyMMddHHmmss")); + IsEquals(pgppubkey.KeyId, keyId); + IsTrue("wrong master key fingerprint", AreEqual(pgppubkey.GetFingerprint(), fpr)); + IsTrue(pgppubkey.GetUserIds().Contains("Alice ")); + + // Sign-Verify roundtrip + byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); + byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); + PgpSignatureGenerator sigGen = new PgpSignatureGenerator(pgppubkey.Algorithm, HashAlgorithmTag.Sha512); + PgpSignatureSubpacketGenerator spkGen = new PgpSignatureSubpacketGenerator(); + PgpPrivateKey privKey = pgpseckey.ExtractPrivateKey(emptyPassphrase); + spkGen.SetIssuerFingerprint(false, pgpseckey); + sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, rand); + sigGen.Update(data); + sigGen.SetHashedSubpackets(spkGen.Generate()); + PgpSignature signature = sigGen.Generate(); + + AreEqual(signature.GetIssuerFingerprint(), fpr); + + VerifySignature(signature, data, pgppubkey); + VerifySignature(signature, wrongData, pgppubkey, shouldFail: true); + } + [Test] public void Version6LockedSecretKeyParsingTest() { @@ -539,9 +685,10 @@ public override void PerformTest() Version4Ed25519LegacyPubkeySampleTest(); Version4Ed25519LegacySignatureSampleTest(); Version4Ed25519LegacyCreateTest(); - Version6CertificateParsingTest(); Version6PublicKeyCreationTest(); + Version6Ed25519KeyPairCreationTest(); + Version6Ed448KeyPairCreationTest(); Version6UnlockedSecretKeyParsingTest(); Version6LockedSecretKeyParsingTest(); Version6SampleCleartextSignedMessageVerifySignatureTest(); From fd0da1397ca5ef110124ea45f17cfef328ef41e2 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Sun, 3 Mar 2024 17:57:38 +0100 Subject: [PATCH 17/37] test adding encryption subkeys in v6 keyrings --- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 122 ++++++++++++++---- 1 file changed, 96 insertions(+), 26 deletions(-) diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 1d6d45684..ab1ddf0d0 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -285,7 +285,7 @@ public void Version6UnlockedSecretKeyParsingTest() kpGen.Init(new X25519KeyGenerationParameters(new SecureRandom())); AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); - IsTrue("X25519 agreement failed", EncryptThenDecryptX25519Test(alice, bob)); + IsTrue("X25519 agreement failed", EncryptThenDecryptTest(alice, bob, encryptionKey.PublicKey.Algorithm)); // Encode test using (MemoryStream ms = new MemoryStream()) @@ -367,26 +367,28 @@ public void Version6Ed25519KeyPairCreationTest() shouldFail: true); // encode-decode roundtrip - + SecureRandom rand = new SecureRandom(); + string uid = "Alice "; PgpKeyRingGenerator keyRingGen = new PgpKeyRingGenerator( PgpSignature.PositiveCertification, keypair, - "Alice ", + uid, SymmetricKeyAlgorithmTag.Null, Array.Empty(), true, null, null, - new SecureRandom()); + rand); - PgpSecretKeyRing secring = keyRingGen.GenerateSecretKeyRing(); - byte[] encodedsecring; - using (MemoryStream ms = new MemoryStream()) - { - secring.Encode(ms); - encodedsecring = ms.ToArray(); - } + // add an encryption subkey + X25519KeyPairGenerator x25519gen = new X25519KeyPairGenerator(); + x25519gen.Init(new X25519KeyGenerationParameters(rand)); + AsymmetricCipherKeyPair x25519kp = x25519gen.GenerateKeyPair(); + keypair = new PgpKeyPair(PublicKeyPacket.Version6, PublicKeyAlgorithmTag.X25519, x25519kp, DateTime.Parse("2022-11-30 16:08:03Z")); + keyRingGen.AddSubKey(keypair); + PgpSecretKeyRing secring = keyRingGen.GenerateSecretKeyRing(); + byte[] encodedsecring = secring.GetEncoded(); PgpSecretKeyRing decodedsecring = new PgpSecretKeyRing(encodedsecring); PgpPublicKey pgppubkey = decodedsecring.GetPublicKey(); @@ -395,7 +397,33 @@ public void Version6Ed25519KeyPairCreationTest() IsEquals(pgppubkey.CreationTime.ToString("yyyyMMddHHmmss"), "20221130160803"); IsEquals((ulong)pgppubkey.KeyId, 0xCB186C4F0609A697); IsTrue("wrong master key fingerprint", AreEqual(pgppubkey.GetFingerprint(), expectedFingerprint)); - IsTrue(pgppubkey.GetUserIds().Contains("Alice ")); + IsTrue(pgppubkey.GetUserIds().Contains(uid)); + + // verify selfsig + PgpSignature signature = pgppubkey.GetSignaturesForId(uid).ToArray()[0]; + IsEquals(signature.Version, SignaturePacket.Version6); + IsEquals(signature.SignatureType, PgpSignature.PositiveCertification); + signature.InitVerify(pgppubkey); + IsTrue(signature.VerifyCertification(uid, pgppubkey)); + IsTrue(!signature.VerifyCertification("Bob ", pgppubkey)); + + // verify subkey + PgpSecretKey subKey = decodedsecring.GetSecretKeys().ToArray()[1]; + IsEquals(subKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X25519); + + // Verify subkey binding signature + PgpSignature bindingSig = subKey.PublicKey.GetSignatures().First(); + IsTrue(bindingSig.SignatureType == PgpSignature.SubkeyBinding); + bindingSig.InitVerify(pgppubkey); + IsTrue("subkey binding signature verification failed", bindingSig.VerifyCertification(pgppubkey, subKey.PublicKey)); + + // encrypt-decrypt test + AsymmetricCipherKeyPair alice = GetKeyPair(subKey); + IAsymmetricCipherKeyPairGenerator kpGen = new X25519KeyPairGenerator(); + kpGen.Init(new X25519KeyGenerationParameters(rand)); + AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); + IsTrue("X25519 agreement failed", EncryptThenDecryptTest(alice, bob, subKey.PublicKey.Algorithm)); + // Sign-Verify roundtrip byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); @@ -407,7 +435,7 @@ public void Version6Ed25519KeyPairCreationTest() sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); sigGen.Update(data); sigGen.SetHashedSubpackets(spkGen.Generate()); - PgpSignature signature = sigGen.Generate(); + signature = sigGen.Generate(); AreEqual(signature.GetIssuerFingerprint(), expectedFingerprint); VerifySignature(signature, data, pgppubkey); @@ -435,10 +463,11 @@ public void Version6Ed448KeyPairCreationTest() IsEquals(fpr.Length, 32); // encode-decode roundtrip + string uid = "Alice "; PgpKeyRingGenerator keyRingGen = new PgpKeyRingGenerator( PgpSignature.PositiveCertification, keypair, - "Alice ", + uid, SymmetricKeyAlgorithmTag.Null, Array.Empty(), true, @@ -446,22 +475,49 @@ public void Version6Ed448KeyPairCreationTest() null, rand); - PgpSecretKeyRing secring = keyRingGen.GenerateSecretKeyRing(); - byte[] encodedsecring; - using (MemoryStream ms = new MemoryStream()) - { - secring.Encode(ms); - encodedsecring = ms.ToArray(); - } + // add an encryption subkey + X448KeyPairGenerator x448gen = new X448KeyPairGenerator(); + x448gen.Init(new X448KeyGenerationParameters(rand)); + AsymmetricCipherKeyPair x448kp = x448gen.GenerateKeyPair(); + keypair = new PgpKeyPair(PublicKeyPacket.Version6, PublicKeyAlgorithmTag.X448, x448kp, now); + keyRingGen.AddSubKey(keypair); + PgpSecretKeyRing secring = keyRingGen.GenerateSecretKeyRing(); + byte[] encodedsecring = secring.GetEncoded(); PgpSecretKeyRing decodedsecring = new PgpSecretKeyRing(encodedsecring); + PgpPublicKey pgppubkey = decodedsecring.GetPublicKey(); PgpSecretKey pgpseckey = decodedsecring.GetSecretKey(); IsEquals(pgppubkey.Algorithm, PublicKeyAlgorithmTag.Ed448); IsEquals(pgppubkey.CreationTime.ToString("yyyyMMddHHmmss"), now.ToString("yyyyMMddHHmmss")); IsEquals(pgppubkey.KeyId, keyId); IsTrue("wrong master key fingerprint", AreEqual(pgppubkey.GetFingerprint(), fpr)); - IsTrue(pgppubkey.GetUserIds().Contains("Alice ")); + IsTrue(pgppubkey.GetUserIds().Contains(uid)); + + // verify selfsig + PgpSignature signature = pgppubkey.GetSignaturesForId(uid).ToArray()[0]; + IsEquals(signature.Version, SignaturePacket.Version6); + IsEquals(signature.SignatureType, PgpSignature.PositiveCertification); + signature.InitVerify(pgppubkey); + IsTrue(signature.VerifyCertification(uid, pgppubkey)); + IsTrue(!signature.VerifyCertification("Bob ", pgppubkey)); + + // verify subkey + PgpSecretKey subKey = decodedsecring.GetSecretKeys().ToArray()[1]; + IsEquals(subKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X448); + + // Verify subkey binding signature + PgpSignature bindingSig = subKey.PublicKey.GetSignatures().First(); + IsTrue(bindingSig.SignatureType == PgpSignature.SubkeyBinding); + bindingSig.InitVerify(pgppubkey); + IsTrue("subkey binding signature verification failed", bindingSig.VerifyCertification(pgppubkey, subKey.PublicKey)); + + // encrypt-decrypt test + AsymmetricCipherKeyPair alice = GetKeyPair(subKey); + IAsymmetricCipherKeyPairGenerator kpGen = new X448KeyPairGenerator(); + kpGen.Init(new X448KeyGenerationParameters(rand)); + AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); + IsTrue("X448 agreement failed", EncryptThenDecryptTest(alice, bob, subKey.PublicKey.Algorithm)); // Sign-Verify roundtrip byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); @@ -473,7 +529,7 @@ public void Version6Ed448KeyPairCreationTest() sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, rand); sigGen.Update(data); sigGen.SetHashedSubpackets(spkGen.Generate()); - PgpSignature signature = sigGen.Generate(); + signature = sigGen.Generate(); AreEqual(signature.GetIssuerFingerprint(), fpr); @@ -665,14 +721,28 @@ private static AsymmetricCipherKeyPair GetKeyPair(PgpSecretKey secretKey, string secretKey.ExtractPrivateKey(password.ToCharArray()).Key); } - private static bool EncryptThenDecryptX25519Test(AsymmetricCipherKeyPair alice, AsymmetricCipherKeyPair bob) + private static bool EncryptThenDecryptTest(AsymmetricCipherKeyPair alice, AsymmetricCipherKeyPair bob, PublicKeyAlgorithmTag algo) { - X25519Agreement agreeA = new X25519Agreement(); + IRawAgreement agreeA, agreeB; + + switch (algo) + { + case PublicKeyAlgorithmTag.X25519: + agreeA = new X25519Agreement(); + agreeB = new X25519Agreement(); + break; + case PublicKeyAlgorithmTag.X448: + agreeA = new X448Agreement(); + agreeB = new X448Agreement(); + break; + default: + throw new ArgumentException($"Unsupported algo {algo}"); + } + agreeA.Init(alice.Private); byte[] secretA = new byte[agreeA.AgreementSize]; agreeA.CalculateAgreement(bob.Public, secretA, 0); - X25519Agreement agreeB = new X25519Agreement(); agreeB.Init(bob.Private); byte[] secretB = new byte[agreeB.AgreementSize]; agreeB.CalculateAgreement(alice.Public, secretB, 0); From cf27aea36fa9191e050fd22d8590290727d045fe Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Mon, 4 Mar 2024 19:29:06 +0100 Subject: [PATCH 18/37] small modifications in tests --- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index ab1ddf0d0..14ffc2721 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -18,9 +18,6 @@ namespace Org.BouncyCastle.Bcpg.OpenPgp.Tests public class PgpCryptoRefreshTest : SimpleTest { - public override string Name => "PgpCryptoRefreshTest"; - - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-key private readonly byte[] v4Ed25519LegacyPubkeySample = Base64.Decode( "xjMEU/NfCxYJKwYBBAHaRw8BAQdAPwmJlL3ZFu1AUxl5NOSofIBzOhKA1i+AEJku" + @@ -280,14 +277,7 @@ public void Version6UnlockedSecretKeyParsingTest() IsEquals(encryptionKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X25519); IsEquals(encryptionKey.PublicKey.KeyId, 0x12C83F1E706F6308); - AsymmetricCipherKeyPair alice = GetKeyPair(encryptionKey); - IAsymmetricCipherKeyPairGenerator kpGen = new X25519KeyPairGenerator(); - kpGen.Init(new X25519KeyGenerationParameters(new SecureRandom())); - AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); - - IsTrue("X25519 agreement failed", EncryptThenDecryptTest(alice, bob, encryptionKey.PublicKey.Algorithm)); - - // Encode test + // Encode-Decode roundtrip using (MemoryStream ms = new MemoryStream()) { using (BcpgOutputStream bs = new BcpgOutputStream(ms, newFormatOnly: true)) @@ -366,7 +356,7 @@ public void Version6Ed25519KeyPairCreationTest() keypair.PublicKey, shouldFail: true); - // encode-decode roundtrip + // Encode-Decode roundtrip SecureRandom rand = new SecureRandom(); string uid = "Alice "; PgpKeyRingGenerator keyRingGen = new PgpKeyRingGenerator( @@ -417,14 +407,6 @@ public void Version6Ed25519KeyPairCreationTest() bindingSig.InitVerify(pgppubkey); IsTrue("subkey binding signature verification failed", bindingSig.VerifyCertification(pgppubkey, subKey.PublicKey)); - // encrypt-decrypt test - AsymmetricCipherKeyPair alice = GetKeyPair(subKey); - IAsymmetricCipherKeyPairGenerator kpGen = new X25519KeyPairGenerator(); - kpGen.Init(new X25519KeyGenerationParameters(rand)); - AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); - IsTrue("X25519 agreement failed", EncryptThenDecryptTest(alice, bob, subKey.PublicKey.Algorithm)); - - // Sign-Verify roundtrip byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); @@ -440,6 +422,15 @@ public void Version6Ed25519KeyPairCreationTest() AreEqual(signature.GetIssuerFingerprint(), expectedFingerprint); VerifySignature(signature, data, pgppubkey); VerifySignature(signature, wrongData, pgppubkey, shouldFail: true); + + // encrypt-decrypt test + AsymmetricCipherKeyPair alice = GetKeyPair(subKey); + + PgpSecretKeyRing bobSecring = new PgpSecretKeyRing(v6UnlockedSecretKey); + PgpSecretKey bobEncryptionKey = bobSecring.GetSecretKeys().ToArray()[1]; + AsymmetricCipherKeyPair bob = GetKeyPair(bobEncryptionKey); + + IsTrue("X25519 agreement failed", EncryptThenDecryptTest(alice, bob, subKey.PublicKey.Algorithm)); } [Test] @@ -512,13 +503,6 @@ public void Version6Ed448KeyPairCreationTest() bindingSig.InitVerify(pgppubkey); IsTrue("subkey binding signature verification failed", bindingSig.VerifyCertification(pgppubkey, subKey.PublicKey)); - // encrypt-decrypt test - AsymmetricCipherKeyPair alice = GetKeyPair(subKey); - IAsymmetricCipherKeyPairGenerator kpGen = new X448KeyPairGenerator(); - kpGen.Init(new X448KeyGenerationParameters(rand)); - AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); - IsTrue("X448 agreement failed", EncryptThenDecryptTest(alice, bob, subKey.PublicKey.Algorithm)); - // Sign-Verify roundtrip byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); @@ -532,9 +516,15 @@ public void Version6Ed448KeyPairCreationTest() signature = sigGen.Generate(); AreEqual(signature.GetIssuerFingerprint(), fpr); - VerifySignature(signature, data, pgppubkey); VerifySignature(signature, wrongData, pgppubkey, shouldFail: true); + + // Encrypt-Decrypt test + AsymmetricCipherKeyPair alice = GetKeyPair(subKey); + IAsymmetricCipherKeyPairGenerator kpGen = new X448KeyPairGenerator(); + kpGen.Init(new X448KeyGenerationParameters(rand)); + AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); + IsTrue("X448 agreement failed", EncryptThenDecryptTest(alice, bob, subKey.PublicKey.Algorithm)); } [Test] @@ -563,7 +553,7 @@ public void Version6LockedSecretKeyParsingTest() IsEquals(encryptionKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X25519); IsEquals(encryptionKey.PublicKey.KeyId, 0x12C83F1E706F6308); - // Encode test + // Encode-Decode roundtrip using (MemoryStream ms = new MemoryStream()) { using (BcpgOutputStream bs = new BcpgOutputStream(ms, newFormatOnly: true)) @@ -580,7 +570,6 @@ public void Version6LockedSecretKeyParsingTest() public void Version6SampleCleartextSignedMessageVerifySignatureTest() { // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-cleartext-signed-mes - PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v6Certificate); PgpPublicKey pubKey = pubRing.GetPublicKey(); @@ -750,6 +739,8 @@ private static bool EncryptThenDecryptTest(AsymmetricCipherKeyPair alice, Asymme return Arrays.AreEqual(secretA, secretB); } + public override string Name => "PgpCryptoRefreshTest"; + public override void PerformTest() { Version4Ed25519LegacyPubkeySampleTest(); From a9c2208b5ee6a6bfa098b60f33419292076034f7 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Tue, 5 Mar 2024 18:10:24 +0100 Subject: [PATCH 19/37] "Version" and "Hash" Armor Header only on explicit request --- crypto/src/bcpg/ArmoredOutputStream.cs | 58 ++++++++++++++++--- .../test/src/openpgp/test/PGPArmoredTest.cs | 47 +++++++++++++++ .../test/PGPClearSignedSignatureTest.cs | 43 +++++++++----- 3 files changed, 125 insertions(+), 23 deletions(-) diff --git a/crypto/src/bcpg/ArmoredOutputStream.cs b/crypto/src/bcpg/ArmoredOutputStream.cs index 03d4a1a91..f39e5095b 100644 --- a/crypto/src/bcpg/ArmoredOutputStream.cs +++ b/crypto/src/bcpg/ArmoredOutputStream.cs @@ -18,6 +18,7 @@ public class ArmoredOutputStream : BaseOutputStream { public static readonly string HeaderVersion = "Version"; + private readonly bool showVersion; private static readonly byte[] encodingTable = { @@ -148,14 +149,29 @@ private static string CreateVersion() private readonly IDictionary> m_headers; public ArmoredOutputStream(Stream outStream) + : this(outStream, false) { + } + + public ArmoredOutputStream(Stream outStream, bool showVersion) + { + this.showVersion = showVersion; this.outStream = outStream; this.m_headers = new Dictionary>(1); - SetHeader(HeaderVersion, Version); + + if (showVersion) + { + SetHeader(HeaderVersion, Version); + } } public ArmoredOutputStream(Stream outStream, IDictionary headers) - : this(outStream) + :this(outStream, headers, false) + { + } + + public ArmoredOutputStream(Stream outStream, IDictionary headers, bool showVersion) + : this(outStream, showVersion) { foreach (var header in headers) { @@ -229,21 +245,42 @@ public void ResetHeaders() } } - /** - * Start a clear text signed message. - * @param hashAlgorithm - */ + /// + /// Start a clear text signed message. + /// + public void BeginClearText() + { + DoWrite("-----BEGIN PGP SIGNED MESSAGE-----" + NewLine + NewLine); + + clearText = true; + newLine = true; + lastb = 0; + + } + + /// + /// Start a clear text signed message with a "Hash" Armor Header. + /// This header is deprecated and SHOULD NOT be emitted unless: + /// * The cleartext signed message contains a v4 signature made using a SHA2-based digest(SHA224, SHA256, SHA384, or SHA512), and + /// * The cleartext signed message might be verified by a legacy OpenPGP implementation that requires this header. + /// + /// + /// + /// public void BeginClearText( HashAlgorithmTag hashAlgorithm) { - string hash; + string hash; switch (hashAlgorithm) { case HashAlgorithmTag.Sha1: hash = "SHA1"; break; - case HashAlgorithmTag.Sha256: + case HashAlgorithmTag.Sha224: + hash = "SHA224"; + break; + case HashAlgorithmTag.Sha256: hash = "SHA256"; break; case HashAlgorithmTag.Sha384: @@ -336,7 +373,10 @@ public override void WriteByte(byte value) DoWrite(headerStart + type + headerTail + NewLine); - if (m_headers.TryGetValue(HeaderVersion, out var versionHeaders)) + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-armor-header + // To minimize metadata, implementations SHOULD NOT emit this key and its corresponding value except + // for debugging purposes with explicit user consent. + if (showVersion && m_headers.TryGetValue(HeaderVersion, out var versionHeaders)) { WriteHeaderEntry(HeaderVersion, versionHeaders[0]); } diff --git a/crypto/test/src/openpgp/test/PGPArmoredTest.cs b/crypto/test/src/openpgp/test/PGPArmoredTest.cs index 9fe5da6ed..195c1ca24 100644 --- a/crypto/test/src/openpgp/test/PGPArmoredTest.cs +++ b/crypto/test/src/openpgp/test/PGPArmoredTest.cs @@ -124,6 +124,52 @@ private void blankLineTest() } } + private void VersionIsOptionalTest() + { + using (MemoryStream bOut = new MemoryStream()) + { + using (ArmoredOutputStream aOut = new ArmoredOutputStream(bOut)) + { + aOut.Write(sample, 0, sample.Length); + } + + bOut.Position = 0; + using (var reader = new StreamReader(bOut)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + FailIf("Unexpected version armor header", line.StartsWith("Version:")); + } + } + } + + using (MemoryStream bOut = new MemoryStream()) + { + using (ArmoredOutputStream aOut = new ArmoredOutputStream(bOut, showVersion: true)) + { + aOut.Write(sample, 0, sample.Length); + } + + bOut.Position = 0; + using (var reader = new StreamReader(bOut)) + { + bool versionIsPresent = false; + string line; + while ((line = reader.ReadLine()) != null) + { + if (line.StartsWith("Version:")) + { + versionIsPresent = true; + break; + } + } + + IsTrue("Version armored header expected", versionIsPresent); + } + } + } + private void repeatHeaderTest() { MemoryStream bOut = new MemoryStream(); @@ -288,6 +334,7 @@ public override void PerformTest() blankLineTest(); pgpUtilTest(); repeatHeaderTest(); + VersionIsOptionalTest(); } public override string Name diff --git a/crypto/test/src/openpgp/test/PGPClearSignedSignatureTest.cs b/crypto/test/src/openpgp/test/PGPClearSignedSignatureTest.cs index a6f03c06f..e41c1f19c 100644 --- a/crypto/test/src/openpgp/test/PGPClearSignedSignatureTest.cs +++ b/crypto/test/src/openpgp/test/PGPClearSignedSignatureTest.cs @@ -164,23 +164,26 @@ public override string Name private void messageTest( string message, - string type) + string type, + bool ignoreHeaders = false) { ArmoredInputStream aIn = new ArmoredInputStream( new MemoryStream(Encoding.ASCII.GetBytes(message))); - string[] headers = aIn.GetArmorHeaders(); - - if (headers == null || headers.Length != 1) + if (!ignoreHeaders) { - Fail("wrong number of headers found"); - } + string[] headers = aIn.GetArmorHeaders(); - if (!"Hash: SHA256".Equals(headers[0])) - { - Fail("header value wrong: " + headers[0]); - } + if (headers == null || headers.Length != 1) + { + Fail("wrong number of headers found"); + } + if (!"Hash: SHA256".Equals(headers[0])) + { + Fail("header value wrong: " + headers[0]); + } + } // // read the input, making sure we ingore the last newline. // @@ -254,7 +257,8 @@ private PgpSecretKey ReadSecretKey( private void generateTest( string message, - string type) + string type, + bool includeHashHeader = true) { PgpSecretKey pgpSecKey = ReadSecretKey(new MemoryStream(secretKey)); PgpPrivateKey pgpPrivKey = pgpSecKey.ExtractPrivateKey("".ToCharArray()); @@ -273,7 +277,14 @@ private void generateTest( ArmoredOutputStream aOut = new ArmoredOutputStream(bOut); MemoryStream bIn = new MemoryStream(Encoding.ASCII.GetBytes(message), false); - aOut.BeginClearText(HashAlgorithmTag.Sha256); + if (includeHashHeader) + { + aOut.BeginClearText(HashAlgorithmTag.Sha256); + } + else + { + aOut.BeginClearText(); + } // // note the last \n m_in the file is ignored @@ -306,7 +317,7 @@ private void generateTest( aOut.Close(); byte[] bs = bOut.ToArray(); - messageTest(Encoding.ASCII.GetString(bs, 0, bs.Length), type); + messageTest(Encoding.ASCII.GetString(bs, 0, bs.Length), type, !includeHashHeader); } private static int ReadInputLine( @@ -424,7 +435,11 @@ public override void PerformTest() generateTest(nlOnlyMessage, "\\r"); generateTest(crOnlyMessage, "\\n"); generateTest(crNlMessage, "\\r\\n"); - } + + generateTest(nlOnlyMessage, "\\r", includeHashHeader: false); + generateTest(crOnlyMessage, "\\n", includeHashHeader: false); + generateTest(crNlMessage, "\\r\\n", includeHashHeader: false); + } [Test] public void TestFunction() From f939a076cfd1918a949f67baf0be457707973858 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Tue, 12 Mar 2024 18:34:39 +0100 Subject: [PATCH 20/37] Review PgpSecretKey class * Unprotected v6 keys does not use the two-octets checksum https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#table-2 * In DoCopyWithNewPassword rewrite Checksum using SHA-1 instead of the deprecated UsageChecksum (MalleableCFB) * Some code deduplication and reorganization --- crypto/src/bcpg/SecretKeyPacket.cs | 4 +- crypto/src/openpgp/PgpSecretKey.cs | 312 +++++++++++------- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 24 +- .../test/src/openpgp/test/PgpKeyRingTest.cs | 2 +- 4 files changed, 209 insertions(+), 133 deletions(-) diff --git a/crypto/src/bcpg/SecretKeyPacket.cs b/crypto/src/bcpg/SecretKeyPacket.cs index 985fc28cf..001229647 100644 --- a/crypto/src/bcpg/SecretKeyPacket.cs +++ b/crypto/src/bcpg/SecretKeyPacket.cs @@ -263,9 +263,9 @@ public SymmetricKeyAlgorithmTag EncAlgorithm get { return encAlgorithm; } } - public AeadAlgorithmTag GetAeadAlgorithm() + public AeadAlgorithmTag AeadAlgorithm { - return aeadAlgorithm; + get { return aeadAlgorithm; } } public int S2kUsage diff --git a/crypto/src/openpgp/PgpSecretKey.cs b/crypto/src/openpgp/PgpSecretKey.cs index b2a3d1a93..24e717385 100644 --- a/crypto/src/openpgp/PgpSecretKey.cs +++ b/crypto/src/openpgp/PgpSecretKey.cs @@ -25,6 +25,8 @@ public class PgpSecretKey private readonly SecretKeyPacket secret; private readonly PgpPublicKey pub; + #region "Internal constructors" + internal PgpSecretKey( SecretKeyPacket secret, PgpPublicKey pub) @@ -33,13 +35,29 @@ internal PgpSecretKey( this.pub = pub; } + internal PgpSecretKey( + PgpPrivateKey privKey, + PgpPublicKey pubKey, + SymmetricKeyAlgorithmTag encAlgorithm, + byte[] rawPassPhrase, + bool clearPassPhrase, + bool useSha1, + SecureRandom rand, + bool isMasterKey) + :this(privKey, pubKey, encAlgorithm, 0, rawPassPhrase, clearPassPhrase, + useSha1 ? SecretKeyPacket.UsageSha1 : SecretKeyPacket.UsageChecksum, + rand, isMasterKey) + { + } + internal PgpSecretKey( PgpPrivateKey privKey, PgpPublicKey pubKey, SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, byte[] rawPassPhrase, bool clearPassPhrase, - bool useSha1, + int s2kUsage, SecureRandom rand, bool isMasterKey) { @@ -132,25 +150,53 @@ internal PgpSecretKey( try { - MemoryStream bOut = new MemoryStream(); - BcpgOutputStream pOut = new BcpgOutputStream(bOut); + byte[] keyData = secKey.GetEncoded(); - pOut.WriteObject(secKey); + if (encAlgorithm == SymmetricKeyAlgorithmTag.Null) + { + s2kUsage = SecretKeyPacket.UsageNone; + } - byte[] keyData = bOut.ToArray(); - byte[] checksumData = Checksum(useSha1, keyData, keyData.Length); + // RFC 4880 § 5.5.3 + "RFC 4880bis" § 5.5.3 + Crypto-refresh § 5.5.3. + // v6 keys with UsageNone: No checksum + // v5 v6 keys with MalleableCFB: Not allowed + // v3 v4 v5 keys with UsageNone: two-octet checksum + // v3 v4 keys with MalleableCFB: two-octet checksum + // all keys with UsageSha1: Sha1 checksum + // all keys with UsageAead: No checksum (use the auth tag of the AEAD algo) + if (pub.Version > PublicKeyPacket.Version4 && s2kUsage == SecretKeyPacket.UsageChecksum) + { + throw new PgpException($"A version {pub.Version} key MUST NOT use MalleableCFB S2K Usage"); + } + else + { + byte[] checksumData = Array.Empty(); + + if (s2kUsage == SecretKeyPacket.UsageSha1) + { + checksumData = Checksum(true, keyData, keyData.Length); + } + else if (s2kUsage == SecretKeyPacket.UsageNone && pub.Version != PublicKeyPacket.Version6) + { + checksumData = Checksum(false, keyData, keyData.Length); + } + else if (s2kUsage == SecretKeyPacket.UsageChecksum) + { + checksumData = Checksum(false, keyData, keyData.Length); + } - keyData = Arrays.Concatenate(keyData, checksumData); + keyData = Arrays.Concatenate(keyData, checksumData); + } if (encAlgorithm == SymmetricKeyAlgorithmTag.Null) { if (isMasterKey) { - this.secret = new SecretKeyPacket(pub.publicPk, encAlgorithm, null, null, keyData); + this.secret = new SecretKeyPacket(pub.publicPk, encAlgorithm, aeadAlgorithm, s2kUsage, null, null, keyData); } else { - this.secret = new SecretSubkeyPacket(pub.publicPk, encAlgorithm, null, null, keyData); + this.secret = new SecretSubkeyPacket(pub.publicPk, encAlgorithm, aeadAlgorithm, s2kUsage, null, null, keyData); } } else @@ -168,17 +214,13 @@ internal PgpSecretKey( encData = EncryptKeyDataV3(keyData, encAlgorithm, rawPassPhrase, clearPassPhrase, rand, out s2k, out iv); } - int s2kUsage = useSha1 - ? SecretKeyPacket.UsageSha1 - : SecretKeyPacket.UsageChecksum; - if (isMasterKey) { - this.secret = new SecretKeyPacket(pub.publicPk, encAlgorithm, s2kUsage, s2k, iv, encData); + this.secret = new SecretKeyPacket(pub.publicPk, encAlgorithm, aeadAlgorithm, s2kUsage, s2k, iv, encData); } else { - this.secret = new SecretSubkeyPacket(pub.publicPk, encAlgorithm, s2kUsage, s2k, iv, encData); + this.secret = new SecretSubkeyPacket(pub.publicPk, encAlgorithm, aeadAlgorithm, s2kUsage, s2k, iv, encData); } } } @@ -192,6 +234,97 @@ internal PgpSecretKey( } } + + internal PgpSecretKey( + int certificationLevel, + PgpKeyPair keyPair, + string id, + SymmetricKeyAlgorithmTag encAlgorithm, + byte[] rawPassPhrase, + bool clearPassPhrase, + bool useSha1, + PgpSignatureSubpacketVector hashedPackets, + PgpSignatureSubpacketVector unhashedPackets, + SecureRandom rand) + : this(keyPair.PrivateKey, CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets, rand), + encAlgorithm, rawPassPhrase, clearPassPhrase, useSha1, rand, true) + { + } + + internal PgpSecretKey( + int certificationLevel, + PgpKeyPair keyPair, + string id, + SymmetricKeyAlgorithmTag encAlgorithm, + HashAlgorithmTag hashAlgorithm, + byte[] rawPassPhrase, + bool clearPassPhrase, + bool useSha1, + PgpSignatureSubpacketVector hashedPackets, + PgpSignatureSubpacketVector unhashedPackets, + SecureRandom rand) + : this(keyPair.PrivateKey, CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets, hashAlgorithm, rand), + encAlgorithm, rawPassPhrase, clearPassPhrase, useSha1, rand, true) + { + } + + private static PgpPublicKey CertifiedPublicKey( + int certificationLevel, + PgpKeyPair keyPair, + string id, + PgpSignatureSubpacketVector hashedPackets, + PgpSignatureSubpacketVector unhashedPackets, + SecureRandom rand) + { + return CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets, HashAlgorithmTag.Sha1, rand); + } + + + private static PgpPublicKey CertifiedPublicKey( + int certificationLevel, + PgpKeyPair keyPair, + string id, + PgpSignatureSubpacketVector hashedPackets, + PgpSignatureSubpacketVector unhashedPackets, + HashAlgorithmTag hashAlgorithm, + SecureRandom rand) + { + PgpSignatureGenerator sGen; + try + { + sGen = new PgpSignatureGenerator(keyPair.PublicKey.Algorithm, hashAlgorithm); + } + catch (Exception e) + { + throw new PgpException("Creating signature generator: " + e.Message, e); + } + + // TODO For v6 keys, the User Id should be optional and information about + // the primary Public-Key (key flags, key expiration, features, algorithm + // preferences, etc.) should be stored in a direct key signature over the + // Public-Key instead of in a User ID self-signature. + + // + // Generate the certification + // + sGen.InitSign(certificationLevel, keyPair.PrivateKey, rand); + + sGen.SetHashedSubpackets(hashedPackets); + sGen.SetUnhashedSubpackets(unhashedPackets); + + try + { + PgpSignature certification = sGen.GenerateCertification(id, keyPair.PublicKey); + return PgpPublicKey.AddCertification(keyPair.PublicKey, id, certification); + } + catch (Exception e) + { + throw new PgpException("Exception doing certification: " + e.Message, e); + } + } + + #endregion + /// /// Conversion of the passphrase characters to bytes is performed using Convert.ToByte(), which is /// the historical behaviour of the library (1.7 and earlier). @@ -248,22 +381,6 @@ public PgpSecretKey( { } - internal PgpSecretKey( - int certificationLevel, - PgpKeyPair keyPair, - string id, - SymmetricKeyAlgorithmTag encAlgorithm, - byte[] rawPassPhrase, - bool clearPassPhrase, - bool useSha1, - PgpSignatureSubpacketVector hashedPackets, - PgpSignatureSubpacketVector unhashedPackets, - SecureRandom rand) - : this(keyPair.PrivateKey, CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets, rand), - encAlgorithm, rawPassPhrase, clearPassPhrase, useSha1, rand, true) - { - } - /// /// Conversion of the passphrase characters to bytes is performed using Convert.ToByte(), which is /// the historical behaviour of the library (1.7 and earlier). @@ -323,99 +440,6 @@ public PgpSecretKey( { } - internal PgpSecretKey( - int certificationLevel, - PgpKeyPair keyPair, - string id, - SymmetricKeyAlgorithmTag encAlgorithm, - HashAlgorithmTag hashAlgorithm, - byte[] rawPassPhrase, - bool clearPassPhrase, - bool useSha1, - PgpSignatureSubpacketVector hashedPackets, - PgpSignatureSubpacketVector unhashedPackets, - SecureRandom rand) - : this(keyPair.PrivateKey, CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets, hashAlgorithm, rand), - encAlgorithm, rawPassPhrase, clearPassPhrase, useSha1, rand, true) - { - } - - private static PgpPublicKey CertifiedPublicKey( - int certificationLevel, - PgpKeyPair keyPair, - string id, - PgpSignatureSubpacketVector hashedPackets, - PgpSignatureSubpacketVector unhashedPackets, - SecureRandom rand) - { - PgpSignatureGenerator sGen; - try - { - sGen = new PgpSignatureGenerator(keyPair.PublicKey.Algorithm, HashAlgorithmTag.Sha1); - } - catch (Exception e) - { - throw new PgpException("Creating signature generator: " + e.Message, e); - } - - // - // Generate the certification - // - sGen.InitSign(certificationLevel, keyPair.PrivateKey, rand); - - sGen.SetHashedSubpackets(hashedPackets); - sGen.SetUnhashedSubpackets(unhashedPackets); - - try - { - PgpSignature certification = sGen.GenerateCertification(id, keyPair.PublicKey); - return PgpPublicKey.AddCertification(keyPair.PublicKey, id, certification); - } - catch (Exception e) - { - throw new PgpException("Exception doing certification: " + e.Message, e); - } - } - - - private static PgpPublicKey CertifiedPublicKey( - int certificationLevel, - PgpKeyPair keyPair, - string id, - PgpSignatureSubpacketVector hashedPackets, - PgpSignatureSubpacketVector unhashedPackets, - HashAlgorithmTag hashAlgorithm, - SecureRandom rand) - { - PgpSignatureGenerator sGen; - try - { - sGen = new PgpSignatureGenerator(keyPair.PublicKey.Algorithm, hashAlgorithm); - } - catch (Exception e) - { - throw new PgpException("Creating signature generator: " + e.Message, e); - } - - // - // Generate the certification - // - sGen.InitSign(certificationLevel, keyPair.PrivateKey, rand); - - sGen.SetHashedSubpackets(hashedPackets); - sGen.SetUnhashedSubpackets(unhashedPackets); - - try - { - PgpSignature certification = sGen.GenerateCertification(id, keyPair.PublicKey); - return PgpPublicKey.AddCertification(keyPair.PublicKey, id, certification); - } - catch (Exception e) - { - throw new PgpException("Exception doing certification: " + e.Message, e); - } - } - public PgpSecretKey( int certificationLevel, PublicKeyAlgorithmTag algorithm, @@ -505,6 +529,12 @@ public SymmetricKeyAlgorithmTag KeyEncryptionAlgorithm get { return secret.EncAlgorithm; } } + /// The AEAD algorithm the key is encrypted with. + public AeadAlgorithmTag KeyEncryptionAeadAlgorithm + { + get { return secret.AeadAlgorithm; } + } + /// The key ID of the public key associated with this key. public long KeyId { @@ -1020,7 +1050,14 @@ internal static PgpSecretKey DoCopyWithNewPassword( if (newEncAlgorithm == SymmetricKeyAlgorithmTag.Null) { s2kUsage = SecretKeyPacket.UsageNone; - if (key.secret.S2kUsage == SecretKeyPacket.UsageSha1) // SHA-1 hash, need to rewrite Checksum + + // v6 keys with UsageNone don't use checksums + if (key.PublicKey.Version == PublicKeyPacket.Version6) + { + keyData = new byte[rawKeyData.Length - 2]; + Array.Copy(rawKeyData, 0, keyData, 0, keyData.Length - 2); + } + else if (key.secret.S2kUsage == SecretKeyPacket.UsageSha1) // SHA-1 hash, need to rewrite Checksum { keyData = new byte[rawKeyData.Length - 18]; @@ -1040,13 +1077,34 @@ internal static PgpSecretKey DoCopyWithNewPassword( { if (s2kUsage == SecretKeyPacket.UsageNone) { - s2kUsage = SecretKeyPacket.UsageChecksum; + if (key.PublicKey.Version == PublicKeyPacket.Version6) + { + // v6 keys with UsageNone don't use checksums + } + else if (key.PublicKey.Version == PublicKeyPacket.Version3) + { + // for backward compatibility with legacy v3 keys use the deprecated UsageChecksum (MalleableCFB) + s2kUsage = SecretKeyPacket.UsageChecksum; + } + else + { + // rewrite Checksum using SHA-1 instead of the deprecated UsageChecksum (MalleableCFB) + s2kUsage = SecretKeyPacket.UsageSha1; + byte[] check = Checksum(true, rawKeyData, rawKeyData.Length - 2); + byte[] newKeyData = new byte[rawKeyData.Length - 2 + 20]; + Array.Copy(rawKeyData, 0, newKeyData, 0, rawKeyData.Length - 2); + Array.Copy(check, 0, newKeyData, rawKeyData.Length - 2, check.Length); + + rawKeyData = newKeyData; + } + } try { - if (pubKeyPacket.Version >= 4) + if (pubKeyPacket.Version >= PublicKeyPacket.Version4) { + // TODO for v6 use AEAD when implemented keyData = EncryptKeyDataV4(rawKeyData, newEncAlgorithm, HashAlgorithmTag.Sha1, rawNewPassPhrase, clearPassPhrase, rand, out s2k, out iv); } else diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 14ffc2721..fa25de35b 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -358,6 +358,12 @@ public void Version6Ed25519KeyPairCreationTest() // Encode-Decode roundtrip SecureRandom rand = new SecureRandom(); + + PgpSignatureSubpacketGenerator spgen = new PgpSignatureSubpacketGenerator(); + spgen.SetPreferredHashAlgorithms(true, new int[] { (int)HashAlgorithmTag.Sha512, (int)HashAlgorithmTag.Sha256 }); + spgen.SetPreferredSymmetricAlgorithms(true, new int[] { (int)SymmetricKeyAlgorithmTag.Aes256, (int)SymmetricKeyAlgorithmTag.Aes128 }); + PgpSignatureSubpacketVector hashed = spgen.Generate(); + string uid = "Alice "; PgpKeyRingGenerator keyRingGen = new PgpKeyRingGenerator( PgpSignature.PositiveCertification, @@ -365,8 +371,8 @@ public void Version6Ed25519KeyPairCreationTest() uid, SymmetricKeyAlgorithmTag.Null, Array.Empty(), - true, - null, + false, + hashed, null, rand); @@ -378,7 +384,11 @@ public void Version6Ed25519KeyPairCreationTest() keyRingGen.AddSubKey(keypair); PgpSecretKeyRing secring = keyRingGen.GenerateSecretKeyRing(); + byte[] encodedsecring = secring.GetEncoded(); + // expected length of v6 unencrypted Ed25519 secret key packet: 75 octets + FailIf("unexpected packet length", encodedsecring[1] != 75); + PgpSecretKeyRing decodedsecring = new PgpSecretKeyRing(encodedsecring); PgpPublicKey pgppubkey = decodedsecring.GetPublicKey(); @@ -461,7 +471,7 @@ public void Version6Ed448KeyPairCreationTest() uid, SymmetricKeyAlgorithmTag.Null, Array.Empty(), - true, + false, null, null, rand); @@ -474,7 +484,11 @@ public void Version6Ed448KeyPairCreationTest() keyRingGen.AddSubKey(keypair); PgpSecretKeyRing secring = keyRingGen.GenerateSecretKeyRing(); + byte[] encodedsecring = secring.GetEncoded(); + // expected length of v6 unencrypted Ed448 secret key packet: 125 octets + FailIf("unexpected packet length", encodedsecring[1] != 125); + PgpSecretKeyRing decodedsecring = new PgpSecretKeyRing(encodedsecring); PgpPublicKey pgppubkey = decodedsecring.GetPublicKey(); @@ -545,11 +559,15 @@ public void Version6LockedSecretKeyParsingTest() // signing key PgpSecretKey signingKey = secretKeys[0]; + IsEquals(signingKey.KeyEncryptionAlgorithm, SymmetricKeyAlgorithmTag.Aes256); + IsEquals(signingKey.KeyEncryptionAeadAlgorithm, AeadAlgorithmTag.Ocb); IsEquals(signingKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.Ed25519); IsEquals((ulong)signingKey.PublicKey.KeyId, 0xCB186C4F0609A697); // encryption key PgpSecretKey encryptionKey = secretKeys[1]; + IsEquals(encryptionKey.KeyEncryptionAlgorithm, SymmetricKeyAlgorithmTag.Aes256); + IsEquals(encryptionKey.KeyEncryptionAeadAlgorithm, AeadAlgorithmTag.Ocb); IsEquals(encryptionKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X25519); IsEquals(encryptionKey.PublicKey.KeyId, 0x12C83F1E706F6308); diff --git a/crypto/test/src/openpgp/test/PgpKeyRingTest.cs b/crypto/test/src/openpgp/test/PgpKeyRingTest.cs index 821a7f295..6b2cc3e70 100644 --- a/crypto/test/src/openpgp/test/PgpKeyRingTest.cs +++ b/crypto/test/src/openpgp/test/PgpKeyRingTest.cs @@ -2440,7 +2440,7 @@ public void RewrapTest() // this should succeed PgpPrivateKey privTmp = pgpKey.ExtractPrivateKey(newPass); - if (pgpKey.KeyId != oldKeyID || pgpKey.S2kUsage != SecretKeyPacket.UsageChecksum) + if (pgpKey.KeyId != oldKeyID || pgpKey.S2kUsage != SecretKeyPacket.UsageSha1) { Fail("usage/key ID mismatch"); } From e0ce83b74dca1e442863b731631e1c5c7d7cc5ea Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Sat, 13 Apr 2024 15:44:05 +0200 Subject: [PATCH 21/37] SKESK v6 decryption --- crypto/src/bcpg/AeadInputStream.cs | 182 ++++++++++++++++++ crypto/src/bcpg/AeadUtils.cs | 35 +++- .../src/bcpg/SymmetricEncIntegrityPacket.cs | 73 ++++++- .../src/bcpg/SymmetricKeyEncSessionPacket.cs | 140 ++++++++++++-- crypto/src/openpgp/PgpPbeEncryptedData.cs | 167 ++++++++++++++-- crypto/src/openpgp/PgpUtilities.cs | 31 +++ .../src/openpgp/test/PgpCryptoRefreshTest.cs | 134 +++++++++++++ 7 files changed, 725 insertions(+), 37 deletions(-) create mode 100644 crypto/src/bcpg/AeadInputStream.cs diff --git a/crypto/src/bcpg/AeadInputStream.cs b/crypto/src/bcpg/AeadInputStream.cs new file mode 100644 index 000000000..39a591834 --- /dev/null +++ b/crypto/src/bcpg/AeadInputStream.cs @@ -0,0 +1,182 @@ +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Utilities; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.IO; +using System; +using System.IO; + +namespace Org.BouncyCastle.Bcpg +{ + internal class AeadInputStream + : BaseInputStream + { + // ported from bc-java + + private readonly Stream inputStream; + private readonly byte[] buf; + private readonly BufferedAeadBlockCipher cipher; + private readonly KeyParameter secretKey; + private readonly byte[] aaData; + private readonly byte[] iv; + private readonly int chunkLength; + private readonly int tagLen; + + private byte[] data; + private int dataOff; + private long chunkIndex = 0; + private long totalBytes = 0; + + private static int GetChunkLength(int chunkSize) + { + return 1 << (chunkSize + 6); + } + + public AeadInputStream( + Stream inputStream, + BufferedAeadBlockCipher cipher, + KeyParameter secretKey, + byte[] iv, + AeadAlgorithmTag aeadAlgorithm, + int chunkSize, + byte[] aaData) + { + this.inputStream = inputStream; + this.cipher = cipher; + this.secretKey = secretKey; + this.aaData = aaData; + this.iv = iv; + + chunkLength = GetChunkLength(chunkSize); + tagLen = AeadUtils.GetAuthTagLength(aeadAlgorithm); + + buf = new byte[chunkLength + tagLen + tagLen]; // allow room for chunk tag and message tag + + Streams.ReadFully(inputStream, buf, 0, tagLen + tagLen); + + // load the first block + data = ReadBlock(); + dataOff = 0; + } + + public override int Read(byte[] buffer, int offset, int count) + { + Streams.ValidateBufferArguments(buffer, offset, count); + + if (count == 0) + { + return 0; + } + + if (data != null && dataOff == data.Length) + { + data = ReadBlock(); + dataOff = 0; + } + + if (data == null) + { + return 0; + } + + int available = data.Length - dataOff; + int supplyLen = count < available ? count : available; + Array.Copy(data, dataOff, buffer, offset, supplyLen); + dataOff += supplyLen; + + return supplyLen; + } + + private static byte[] GetNonce(byte[] iv, long chunkIndex) + { + byte[] nonce = Arrays.Clone(iv); + byte[] chunkid = Pack.UInt64_To_BE((ulong)chunkIndex); + Array.Copy(chunkid, 0, nonce, nonce.Length - 8, 8); + + return nonce; + } + + private static byte[] GetAdata(bool isV5StyleAead, byte[] aaData, long chunkIndex, long totalBytes) + { + byte[] adata; + if (isV5StyleAead) + { + adata = new byte[13]; + Array.Copy(aaData, 0, adata, 0, aaData.Length); + Array.Copy(Pack.UInt64_To_BE((ulong)chunkIndex), 0, adata, aaData.Length, 8); + } + else + { + adata = new byte[aaData.Length + 8]; + Array.Copy(aaData, 0, adata, 0, aaData.Length); + Array.Copy(Pack.UInt64_To_BE((ulong)totalBytes), 0, adata, aaData.Length, 8); + } + return adata; + } + + private byte[] ReadBlock() + { + int dataLen = Streams.ReadFully(inputStream, buf, tagLen + tagLen, chunkLength); + if (dataLen == 0) + { + return null; + } + + byte[] adata = Arrays.Clone(aaData); + byte[] decData = new byte[dataLen]; + + try + { + AeadParameters aeadParams = new AeadParameters( + secretKey, + 8 * tagLen, + GetNonce(iv, chunkIndex), + adata); + + cipher.Init(false, aeadParams); + + int len = cipher.ProcessBytes(buf, 0, dataLen + tagLen, decData, 0); + + cipher.DoFinal(decData, len); + } + catch (InvalidCipherTextException e) + { + throw new IOException($"exception processing chunk {chunkIndex}: {e.Message}"); + } + + totalBytes += decData.Length; + chunkIndex++; + Array.Copy(buf, dataLen + tagLen, buf, 0, tagLen); // copy back the "tag" + + if (dataLen != chunkLength) + { + // last block + try + { + adata = GetAdata(false, aaData, chunkIndex, totalBytes); + AeadParameters aeadParams = new AeadParameters( + secretKey, + 8 * tagLen, + GetNonce(iv, chunkIndex), + adata); + + cipher.Init(false, aeadParams); + + cipher.ProcessBytes(buf, 0, tagLen, buf, 0); + + cipher.DoFinal(buf, 0); // check final tag + } + catch (InvalidCipherTextException e) + { + throw new IOException($"exception processing final tag: {e.Message}"); + } + } + else + { + Streams.ReadFully(inputStream, buf, tagLen, tagLen); // read the next tag bytes + } + + return decData; + } + } +} \ No newline at end of file diff --git a/crypto/src/bcpg/AeadUtils.cs b/crypto/src/bcpg/AeadUtils.cs index e7e63c74e..d2bda51e8 100644 --- a/crypto/src/bcpg/AeadUtils.cs +++ b/crypto/src/bcpg/AeadUtils.cs @@ -28,6 +28,28 @@ public static int GetIVLength(AeadAlgorithmTag aeadAlgorithmTag) } } + /// + /// Returns the name of the AEAD Algorithm + /// + /// AEAD algorithm identifier + /// name of the AEAD Algorithm + /// + + public static string GetAeadAlgorithmName(AeadAlgorithmTag aeadAlgorithmTag) + { + switch (aeadAlgorithmTag) + { + case AeadAlgorithmTag.Eax: + return "EAX"; + case AeadAlgorithmTag.Ocb: + return "OCB"; + case AeadAlgorithmTag.Gcm: + return "GCM"; + default: + throw new ArgumentException($"Invalid AEAD algorithm tag: {aeadAlgorithmTag}"); + } + } + /// /// Return the length of the authentication tag used by the given AEAD algorithm in octets. /// @@ -52,23 +74,22 @@ public static int GetAuthTagLength(AeadAlgorithmTag aeadAlgorithmTag) /// two separate byte arrays. /// m is the key length of the cipher algorithm, while n is the IV length of the AEAD algorithm. /// Note, that the IV is filled with
n-8
bytes only, the remainder is left as 0s. - /// Return an array of both arrays with the key and index 0 and the IV at index 1. ///
/// m+n-8 bytes of concatenated message key and IV /// symmetric cipher algorithm /// AEAD algorithm - /// array of arrays containing message key and IV - public static byte[][] SplitMessageKeyAndIv(byte[] messageKeyAndIv, SymmetricKeyAlgorithmTag cipherAlgo, AeadAlgorithmTag aeadAlgo) + /// Message key + /// IV + public static void SplitMessageKeyAndIv(byte[] messageKeyAndIv, SymmetricKeyAlgorithmTag cipherAlgo, AeadAlgorithmTag aeadAlgo, out byte[] messageKey, out byte[] iv) { int keyLen = PgpUtilities.GetKeySizeInOctets(cipherAlgo); int ivLen = GetIVLength(aeadAlgo); - byte[] messageKey = new byte[keyLen]; - byte[] iv = new byte[ivLen]; + + messageKey = new byte[keyLen]; + iv = new byte[ivLen]; Array.Copy(messageKeyAndIv, messageKey, messageKey.Length); Array.Copy(messageKeyAndIv, messageKey.Length, iv, 0, ivLen-8); - - return new byte[][] { messageKey, iv }; } } } \ No newline at end of file diff --git a/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs b/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs index 1de72a4a0..e30da6f1e 100644 --- a/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs +++ b/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs @@ -1,3 +1,4 @@ +using Org.BouncyCastle.Utilities; using System; using System.IO; @@ -6,13 +7,81 @@ namespace Org.BouncyCastle.Bcpg public class SymmetricEncIntegrityPacket : InputStreamPacket { - internal readonly int version; + /// + /// Version 3 SEIPD packet. + /// + /// + public const int Version1 = 1; + /// + /// Version 2 SEIPD packet. + /// + /// + public const int Version2 = 2; - internal SymmetricEncIntegrityPacket( + private readonly int version; // V1, V2 + private readonly SymmetricKeyAlgorithmTag cipherAlgorithm; // V2 Only + private readonly AeadAlgorithmTag aeadAlgorithm; // V2 Only + private readonly int chunkSize; // V2 Only + private readonly byte[] salt; // V2 Only + + internal SymmetricEncIntegrityPacket( BcpgInputStream bcpgIn) : base(bcpgIn, PacketTag.SymmetricEncryptedIntegrityProtected) { version = bcpgIn.ReadByte(); + if (version == Version2) + { + cipherAlgorithm = (SymmetricKeyAlgorithmTag)bcpgIn.ReadByte(); + aeadAlgorithm = (AeadAlgorithmTag)bcpgIn.ReadByte(); + chunkSize = bcpgIn.ReadByte(); + + salt = new byte[32]; + if (bcpgIn.Read(salt, 0, 32) != salt.Length) + { + throw new IOException("Premature end of stream."); + } + } + } + + public int Version + { + get { return version; } + } + + public SymmetricKeyAlgorithmTag CipherAlgorithm + { + get { return cipherAlgorithm; } + } + + public AeadAlgorithmTag AeadAlgorithm + { + get { return aeadAlgorithm; } + } + + public int ChunkSize + { + get { return chunkSize; } + } + + public byte[] GetSalt() + { + return Arrays.Clone(salt); + } + + internal byte[] GetAAData() + { + return CreateAAData(Tag, Version, cipherAlgorithm, aeadAlgorithm, chunkSize); + } + + internal static byte[] CreateAAData(PacketTag tag, int version, SymmetricKeyAlgorithmTag cipherAlgorithm, AeadAlgorithmTag aeadAlgorithm, int chunkSize) + { + return new byte[]{ + (byte)(0xC0 | (byte)tag), + (byte)version, + (byte)cipherAlgorithm, + (byte)aeadAlgorithm, + (byte)chunkSize + }; } } } diff --git a/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs b/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs index f053b431a..215ff98d0 100644 --- a/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs @@ -1,4 +1,6 @@ +using Org.BouncyCastle.Utilities; using System; +using System.Data.SqlTypes; using System.IO; namespace Org.BouncyCastle.Bcpg @@ -9,30 +11,85 @@ namespace Org.BouncyCastle.Bcpg public class SymmetricKeyEncSessionPacket : ContainedPacket { + /// + /// Version 4 SKESK packet. + /// Used only with V1 SEIPD or SED packets. + /// + public const int Version4 = 4; + + /// + /// Version 5 SKESK packet. + /// Used only with AEADEncDataPacket AED packets. + /// Defined in retired "RFC4880-bis" draft + /// + /// + public const int Version5 = 5; + + /// + /// Version 6 SKESK packet. + /// Used only with V2 SEIPD packets. + /// + public const int Version6 = 6; + private readonly int version; private readonly SymmetricKeyAlgorithmTag encAlgorithm; private readonly S2k s2k; private readonly byte[] secKeyData; + private readonly byte[] s2kBytes; + private readonly AeadAlgorithmTag aeadAlgorithm; + private readonly byte[] iv; + public SymmetricKeyEncSessionPacket( BcpgInputStream bcpgIn) :base(PacketTag.SymmetricKeyEncryptedSessionKey) { version = bcpgIn.ReadByte(); - encAlgorithm = (SymmetricKeyAlgorithmTag) bcpgIn.ReadByte(); - s2k = new S2k(bcpgIn); + switch (version) + { + case Version4: + encAlgorithm = (SymmetricKeyAlgorithmTag)bcpgIn.ReadByte(); + s2k = new S2k(bcpgIn); + secKeyData = bcpgIn.ReadAll(); + break; + + case Version5: + case Version6: + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-6-symmetric-key-enc + // SymmAlgo + AEADAlgo + S2KCount + S2K + IV + int next5Fields5Count = bcpgIn.ReadByte(); + encAlgorithm = (SymmetricKeyAlgorithmTag)bcpgIn.ReadByte(); + aeadAlgorithm = (AeadAlgorithmTag)bcpgIn.ReadByte(); + + int s2kOctetCount = bcpgIn.ReadByte(); + s2kBytes = new byte[s2kOctetCount]; + bcpgIn.ReadFully(s2kBytes); + s2k = new S2k(new MemoryStream(s2kBytes)); + + int ivsize = AeadUtils.GetIVLength(aeadAlgorithm); + iv = new byte[ivsize]; + bcpgIn.ReadFully(iv); - secKeyData = bcpgIn.ReadAll(); + // contains both the encrypted session key and the AEAD authentication tag + secKeyData = bcpgIn.ReadAll(); + break; + } } - public SymmetricKeyEncSessionPacket( - SymmetricKeyAlgorithmTag encAlgorithm, + /// + /// Create a v4 SKESK packet. + /// + /// symmetric encryption algorithm + /// s2k specifier + /// encrypted session key + public SymmetricKeyEncSessionPacket( + SymmetricKeyAlgorithmTag encAlgorithm, S2k s2k, byte[] secKeyData) : base(PacketTag.SymmetricKeyEncryptedSessionKey) { - this.version = 4; + this.version = Version4; this.encAlgorithm = encAlgorithm; this.s2k = s2k; this.secKeyData = secKeyData; @@ -46,6 +103,11 @@ public SymmetricKeyAlgorithmTag EncAlgorithm get { return encAlgorithm; } } + public AeadAlgorithmTag AeadAlgorithm + { + get { return aeadAlgorithm; } + } + /** * @return S2k */ @@ -70,21 +132,69 @@ public int Version get { return version; } } - public override void Encode(BcpgOutputStream bcpgOut) + internal byte[] GetAAData() { - MemoryStream bOut = new MemoryStream(); - using (var pOut = new BcpgOutputStream(bOut)) + return CreateAAData(Tag, Version, EncAlgorithm, AeadAlgorithm); + } + + internal static byte[] CreateAAData(PacketTag tag, int version, SymmetricKeyAlgorithmTag encAlgorithm, AeadAlgorithmTag aeadAlgorithm) + { + return new byte[] { - pOut.Write((byte)version, (byte)encAlgorithm); - pOut.WriteObject(s2k); + (byte)(0xC0 | (byte)tag), + (byte)version, + (byte)encAlgorithm, + (byte)aeadAlgorithm + }; + } + + internal byte[] GetAeadIV() + { + return Arrays.Clone(iv); + } - if (secKeyData != null && secKeyData.Length > 0) + public override void Encode(BcpgOutputStream bcpgOut) + { + using (MemoryStream bOut = new MemoryStream()) + { + using (var pOut = new BcpgOutputStream(bOut)) { - pOut.Write(secKeyData); + pOut.WriteByte((byte)version); + + if (version == Version4) + { + pOut.WriteByte((byte)encAlgorithm); + pOut.WriteObject(s2k); + + if (secKeyData != null && secKeyData.Length > 0) + { + pOut.Write(secKeyData); + } + } + else if (version == Version5 || version == Version6) + { + var s2kenc = s2k.GetEncoded(); + int s2kLen = s2kenc.Length; + + // len of 5 following fields + int count = 1 + 1 + 1 + s2kLen + iv.Length; + pOut.WriteByte((byte)count); + + pOut.WriteByte((byte)encAlgorithm); + pOut.WriteByte((byte)aeadAlgorithm); + pOut.WriteByte((byte)s2kLen); + pOut.Write(s2kenc); + pOut.Write(iv); + + if (secKeyData != null && secKeyData.Length > 0) + { + pOut.Write(secKeyData); + } + } } - } - bcpgOut.WritePacket(PacketTag.SymmetricKeyEncryptedSessionKey, bOut.ToArray()); + bcpgOut.WritePacket(PacketTag.SymmetricKeyEncryptedSessionKey, bOut.ToArray()); + } } } } diff --git a/crypto/src/openpgp/PgpPbeEncryptedData.cs b/crypto/src/openpgp/PgpPbeEncryptedData.cs index 1c3432d89..91bec3fd0 100644 --- a/crypto/src/openpgp/PgpPbeEncryptedData.cs +++ b/crypto/src/openpgp/PgpPbeEncryptedData.cs @@ -1,15 +1,15 @@ -using System; -using System.IO; - using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.IO; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities.IO; +using System; +using System.IO; namespace Org.BouncyCastle.Bcpg.OpenPgp { - /// A password based encryption object. + /// A password based encryption object. public class PgpPbeEncryptedData : PgpEncryptedData { @@ -21,10 +21,63 @@ internal PgpPbeEncryptedData( : base(encData) { this.keyData = keyData; - } + EnforceConstraints(); + } - /// Return the raw input stream for the data stream. - public override Stream GetInputStream() + private void EnforceConstraints() + { + switch (keyData.Version) + { + case SymmetricKeyEncSessionPacket.Version4: + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-4-symmetric-key-enc + // A version 4 SKESK packet precedes a version 1 SEIPD packet. In historic data, it is sometimes found + // preceding a deprecated SED packet. A v4 SKESK packet MUST NOT precede a v2 SEIPD packet. + if (encData is SymmetricEncDataPacket) + { + return; + } + + if (encData is SymmetricEncIntegrityPacket seipd1) + { + if (seipd1.Version == SymmetricEncIntegrityPacket.Version1) + { + return; + } + + // V2 SEIPD cannot be preceded by V4 SKESK + throw new PgpException($"Version 4 SKESK cannot precede SEIPD of version {seipd1.Version}"); + } + break; + + case SymmetricKeyEncSessionPacket.Version5: + // https://www.ietf.org/archive/id/draft-koch-openpgp-2015-rfc4880bis-01.html does not state any constraints + break; + + case SymmetricKeyEncSessionPacket.Version6: + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-6-symmetric-key-enc + // A version 6 SKESK packet precedes a version 2 SEIPD packet. A v6 SKESK packet MUST NOT precede a v1 SEIPD + // packet or a deprecated Symmetrically Encrypted Data. + if (encData is SymmetricEncDataPacket) + { + throw new PgpException("Version 6 SKESK MUST NOT precede a deprecated SED packet."); + } + + if (encData is SymmetricEncIntegrityPacket seipd2) + { + if (seipd2.Version == SymmetricEncIntegrityPacket.Version2) + { + return; + } + throw new PgpException($"Version 6 PKESK cannot precede SEIPD of version {seipd2.Version}"); + } + break; + default: + throw new UnsupportedPacketVersionException($"Unsupported PGP secret key encrypted session key packet version encountered: {keyData.Version}"); + } + } + + /// Return the raw input stream for the data stream. + public override Stream GetInputStream() { return encData.GetInputStream(); } @@ -57,7 +110,7 @@ public Stream GetDataStreamRaw(byte[] rawPassPhrase) return DoGetDataStream(rawPassPhrase, false); } - internal Stream DoGetDataStream(byte[] rawPassPhrase, bool clearPassPhrase) + private Stream DoGetDataStreamVersion1(byte[] rawPassPhrase, bool clearPassPhrase) { try { @@ -130,7 +183,6 @@ internal Stream DoGetDataStream(byte[] rawPassPhrase, bool clearPassPhrase) throw new PgpDataValidationException("quick check failed."); } - return encStream; } catch (PgpException) @@ -143,17 +195,106 @@ internal Stream DoGetDataStream(byte[] rawPassPhrase, bool clearPassPhrase) } } - private IBufferedCipher CreateStreamCipher( + private static KeyParameter DeriveVersion6SessionKey(SymmetricKeyEncSessionPacket keyData, byte[] rawPassPhrase, bool clearPassPhrase) + { + var keyAlgorithm = keyData.EncAlgorithm; + var aeadAlgo = keyData.AeadAlgorithm; + var aeadIV = keyData.GetAeadIV(); + var hkdfInfo = keyData.GetAAData(); + var secKeyData = keyData.GetSecKeyData(); + + var keyAlgoName = PgpUtilities.GetSymmetricCipherName(keyAlgorithm); + var aeadAlgoName = AeadUtils.GetAeadAlgorithmName(aeadAlgo); + var keyCipher = CipherUtilities.GetCipher($"{keyAlgoName}/{aeadAlgoName}/NoPadding"); + + var key = PgpUtilities.DoMakeKeyFromPassPhrase(keyAlgorithm, keyData.S2k, rawPassPhrase, clearPassPhrase); + + var hkdfParams = new HkdfParameters(key.GetKey(), Array.Empty(), hkdfInfo); + var hkdfGen = new HkdfBytesGenerator(PgpUtilities.CreateDigest(HashAlgorithmTag.Sha256)); + hkdfGen.Init(hkdfParams); + var hkdfOutput = new byte[PgpUtilities.GetKeySizeInOctets(keyAlgorithm)]; + hkdfGen.GenerateBytes(hkdfOutput, 0, hkdfOutput.Length); + + var aeadParams = new AeadParameters( + new KeyParameter(hkdfOutput), + 8 * AeadUtils.GetAuthTagLength(aeadAlgo), + aeadIV, + hkdfInfo); + + keyCipher.Init(false, aeadParams); + byte[] keyBytes = keyCipher.DoFinal(secKeyData); + + return ParameterUtilities.CreateKeyParameter( + PgpUtilities.GetSymmetricCipherName(keyAlgorithm), + keyBytes, 0, keyBytes.Length); + } + + private Stream DoGetDataStreamVersion2(SymmetricEncIntegrityPacket seipd, byte[] rawPassPhrase, bool clearPassPhrase) + { + try + { + KeyParameter sessionKey = DeriveVersion6SessionKey(keyData, rawPassPhrase, clearPassPhrase); + + var aadata = seipd.GetAAData(); + var salt = seipd.GetSalt(); + PgpUtilities.DeriveAeadMessageKeyAndIv(sessionKey, seipd.CipherAlgorithm, seipd.AeadAlgorithm, salt, aadata, out var messageKey, out var iv); + var cipher = CreateAeadCipher(seipd.CipherAlgorithm, seipd.AeadAlgorithm); + + var aeadStream = new AeadInputStream( + encData.GetInputStream(), + cipher, + messageKey, + iv, + seipd.AeadAlgorithm, + seipd.ChunkSize, + aadata); + + encStream = BcpgInputStream.Wrap(aeadStream); + return encStream; + } + catch (PgpException) + { + throw; + } + catch (Exception e) + { + throw new PgpException("Exception creating cipher", e); + } + } + + internal Stream DoGetDataStream(byte[] rawPassPhrase, bool clearPassPhrase) + { + if (encData is SymmetricEncIntegrityPacket seipd && seipd.Version == SymmetricEncIntegrityPacket.Version2) + { + return DoGetDataStreamVersion2(seipd, rawPassPhrase, clearPassPhrase); + } + else + { + return DoGetDataStreamVersion1(rawPassPhrase, clearPassPhrase); + } + } + + private IBufferedCipher CreateStreamCipher( SymmetricKeyAlgorithmTag keyAlgorithm) { string mode = (encData is SymmetricEncIntegrityPacket) ? "CFB" : "OpenPGPCFB"; - string cName = PgpUtilities.GetSymmetricCipherName(keyAlgorithm) - + "/" + mode + "/NoPadding"; + string cName = $"{PgpUtilities.GetSymmetricCipherName(keyAlgorithm)}/{mode}/NoPadding"; return CipherUtilities.GetCipher(cName); } - } + + private static BufferedAeadBlockCipher CreateAeadCipher( + SymmetricKeyAlgorithmTag keyAlgorithm, AeadAlgorithmTag aeadAlgorithm) + { + string algo = PgpUtilities.GetSymmetricCipherName(keyAlgorithm); + string mode = AeadUtils.GetAeadAlgorithmName(aeadAlgorithm); + string cName = $"{algo}/{mode}/NoPadding"; + + return CipherUtilities.GetCipher(cName) as BufferedAeadBlockCipher; + } + + } } diff --git a/crypto/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index 08418d126..dab25543e 100644 --- a/crypto/src/openpgp/PgpUtilities.cs +++ b/crypto/src/openpgp/PgpUtilities.cs @@ -8,6 +8,7 @@ using Org.BouncyCastle.Asn1.Sec; using Org.BouncyCastle.Asn1.X9; using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Signers; using Org.BouncyCastle.Math; @@ -277,6 +278,36 @@ public static int GetKeySizeInOctets(SymmetricKeyAlgorithmTag algorithm) return (GetKeySize(algorithm) + 7) / 8; } + /// + /// Derive a message key and IV from the given session key. + /// + /// session key + /// symmetric cipher algorithm tag + /// AEAD algorithm tag + /// salt + /// HKDF info + /// + /// + public static void DeriveAeadMessageKeyAndIv( + KeyParameter sessionKey, + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, + byte[] salt, + byte[] hkdfInfo, + out KeyParameter messageKey, + out byte[] iv) + { + var hkdfGen = new HkdfBytesGenerator(CreateDigest(HashAlgorithmTag.Sha256)); + var hkdfParams = new HkdfParameters(sessionKey.GetKey(), salt, hkdfInfo); + hkdfGen.Init(hkdfParams); + var hkdfOutput = new byte[GetKeySizeInOctets(encAlgorithm) + AeadUtils.GetIVLength(aeadAlgorithm) - 8]; + hkdfGen.GenerateBytes(hkdfOutput, 0, hkdfOutput.Length); + + AeadUtils.SplitMessageKeyAndIv(hkdfOutput, encAlgorithm, aeadAlgorithm, out var messageKeyBytes, out iv); + + messageKey = new KeyParameter(messageKeyBytes); + } + public static KeyParameter MakeKey( SymmetricKeyAlgorithmTag algorithm, byte[] keyBytes) diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index fa25de35b..88917149a 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -88,6 +88,31 @@ public class PgpCryptoRefreshTest "b7gKqPxbyxbhljGygHQPnqau1eBzrQD5QVplPEDnemrnfmkrpx0GmhCfokxYz9jj" + "FtCgazStmsuOXF9SFQE="); + // Sample AEAD encryption and decryption + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-aead-eax-encryption- + // encrypts the cleartext string Hello, world! with the passphrase password, using AES-128 with AEAD-EAX encryption. + private readonly byte[] v6skesk_aes128_eax = Base64.Decode( + "w0AGHgcBCwMIpa5XnR/F2Cv/aSJPkZmTs1Bvo7WaanPP+MXvxfQcV/tU4cImgV14" + + "KPX5LEVOtl6+AKtZhsaObnxV0mkCBwEGn/kOOzIZZPOkKRPI3MZhkyUBUifvt+rq" + + "pJ8EwuZ0F11KPSJu1q/LnKmsEiwUcOEcY9TAqyQcapOK1Iv5mlqZuQu6gyXeYQR1" + + "QCWKt5Wala0FHdqW6xVDHf719eIlXKeCYVRuM5o="); + + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-aead-ocb-encryption- + // encrypts the cleartext string Hello, world! with the passphrase password, using AES-128 with AEAD-OCB encryption. + private readonly byte[] v6skesk_aes128_ocb = Base64.Decode( + "wz8GHQcCCwMIVqKY0vXjZFP/z8xcEWZO2520JZDX3EawckG2EsOBLP/76gDyNHsl" + + "ZBEj+IeuYNT9YU4IN9gZ02zSaQIHAgYgpmH3MfyaMDK1YjMmAn46XY21dI6+/wsM" + + "WRDQns3WQf+f04VidYA1vEl1TOG/P/+n2tCjuBBPUTPPQqQQCoPu9MobSAGohGv0" + + "K82nyM6dZeIS8wHLzZj9yt5pSod61CRzI/boVw=="); + + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-aead-gcm-encryption- + // encrypts the cleartext string Hello, world! with the passphrase password, using AES-128 with AEAD-GCM encryption. + private readonly byte[] v6skesk_aes128_gcm = Base64.Decode( + "wzwGGgcDCwMI6dOXhbIHAAj/tC58SD70iERXyzcmubPbn/d25fTZpAlS4kRymIUa" + + "v/91Jt8t1VRBdXmneZ/SaQIHAwb8uUSQvLmLvcnRBsYJAmaUD3LontwhtVlrFXax" + + "Ae0Pn/xvxtZbv9JNzQeQlm5tHoWjAFN4TLHYtqBpnvEhVaeyrWJYUxtXZR/Xd3kS" + + "+pXjXZtAIW9ppMJI2yj/QzHxYykHOZ5v+Q=="); + private readonly char[] emptyPassphrase = Array.Empty(); [Test] @@ -757,6 +782,114 @@ private static bool EncryptThenDecryptTest(AsymmetricCipherKeyPair alice, Asymme return Arrays.AreEqual(secretA, secretB); } + [Test] + public void Version6SkeskVersion2SeipdTest() + { + byte[][] messages = new byte[][] + { + v6skesk_aes128_eax, + v6skesk_aes128_ocb, + v6skesk_aes128_gcm + }; + + byte[] plaintext = Encoding.UTF8.GetBytes("Hello, world!"); + byte[] password = Encoding.UTF8.GetBytes("password"); + + for (int i = 0; i < messages.Length; i++) + { + PgpObjectFactory factory = new PgpObjectFactory(messages[i]); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encData is null); + + var encData0 = encData[0] as PgpPbeEncryptedData; + FailIf("invalid PgpPbeEncryptedData", encData0 is null); + + using (var stream = encData0.GetDataStreamRaw(password)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (var ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + var decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + } + + for (int i = 0; i < messages.Length; i++) + { + // corrupt AEAD nonce + var message = Arrays.Clone(messages[i]); + message[0x18]--; + PgpObjectFactory factory = new PgpObjectFactory(message); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + var encData0 = encData[0] as PgpPbeEncryptedData; + var err = Assert.Throws(() => + { + var stream = encData0.GetDataStreamRaw(password); + }); + } + + for (int i = 0; i < messages.Length; i++) + { + // corrupt encrypted session key + var message = Arrays.Clone(messages[i]); + message[0x28]--; + PgpObjectFactory factory = new PgpObjectFactory(message); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + var encData0 = encData[0] as PgpPbeEncryptedData; + var err = Assert.Throws(() => + { + var stream = encData0.GetDataStreamRaw(password); + }); + } + + for (int i = 0; i < messages.Length; i++) + { + // corrupt chunk #0 encrypted data + var message = Arrays.Clone(messages[i]); + message[message.Length - 35]--; + PgpObjectFactory factory = new PgpObjectFactory(message); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + var encData0 = encData[0] as PgpPbeEncryptedData; + var err = Assert.Throws(() => + { + var stream = encData0.GetDataStreamRaw(password); + }); + } + + for (int i = 0; i < messages.Length; i++) + { + // corrupt chunk #0 authtag + var message = Arrays.Clone(messages[i]); + message[message.Length - 20]--; + PgpObjectFactory factory = new PgpObjectFactory(message); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + var encData0 = encData[0] as PgpPbeEncryptedData; + var err = Assert.Throws(() => + { + var stream = encData0.GetDataStreamRaw(password); + }); + } + + for (int i = 0; i < messages.Length; i++) + { + // corrupt final authtag + var message = Arrays.Clone(messages[i]); + message[message.Length-2]--; + PgpObjectFactory factory = new PgpObjectFactory(message); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + var encData0 = encData[0] as PgpPbeEncryptedData; + var err = Assert.Throws(() => + { + var stream = encData0.GetDataStreamRaw(password); + }); + } + + + } + public override string Name => "PgpCryptoRefreshTest"; public override void PerformTest() @@ -773,6 +906,7 @@ public override void PerformTest() Version6SampleCleartextSignedMessageVerifySignatureTest(); Version6SampleInlineSignedMessageVerifySignatureTest(); Version6GenerateAndVerifyInlineSignatureTest(); + Version6SkeskVersion2SeipdTest(); } } } \ No newline at end of file From 16e73293f4b56dc22c0d018cf0631fce674d3e0c Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Sun, 14 Apr 2024 17:41:20 +0200 Subject: [PATCH 22/37] Support for Argon2 s2k in SKESK and PgpSecretKey decryption --- crypto/src/bcpg/SecretKeyPacket.cs | 12 +- crypto/src/openpgp/PgpSecretKey.cs | 31 ++- crypto/src/openpgp/PgpUtilities.cs | 19 +- crypto/test/data/openpgp/big-aead-msg.asc | 57 ++++ .../src/openpgp/test/PgpCryptoRefreshTest.cs | 246 ++++++++++++++---- 5 files changed, 305 insertions(+), 60 deletions(-) create mode 100644 crypto/test/data/openpgp/big-aead-msg.asc diff --git a/crypto/src/bcpg/SecretKeyPacket.cs b/crypto/src/bcpg/SecretKeyPacket.cs index 001229647..45f05d505 100644 --- a/crypto/src/bcpg/SecretKeyPacket.cs +++ b/crypto/src/bcpg/SecretKeyPacket.cs @@ -293,7 +293,17 @@ public byte[] GetSecretKeyData() return secKeyData; } - + internal byte[] GetAAData() + { + // HKDF Info used for key derivation in UsageAead + return new byte[] + { + (byte)(0xC0 | (byte)Tag), + (byte)pubKeyPacket.Version, + (byte)encAlgorithm, + (byte)aeadAlgorithm + }; + } private byte[] EncodeConditionalParameters() { diff --git a/crypto/src/openpgp/PgpSecretKey.cs b/crypto/src/openpgp/PgpSecretKey.cs index 24e717385..7ecad0b99 100644 --- a/crypto/src/openpgp/PgpSecretKey.cs +++ b/crypto/src/openpgp/PgpSecretKey.cs @@ -10,6 +10,7 @@ using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Asn1.X9; using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Math; using Org.BouncyCastle.Math.EC.Rfc8032; @@ -595,7 +596,35 @@ private byte[] ExtractKeyData(byte[] rawPassPhrase, bool clearPassPhrase) byte[] iv = secret.GetIV(); byte[] data; - if (secret.PublicKeyPacket.Version >= 4) + if (secret.S2kUsage == SecretKeyPacket.UsageAead) + { + var aeadAlgorithm = secret.AeadAlgorithm; + var hkdfInfo = secret.GetAAData(); + var hkdfParams = new HkdfParameters(key.GetKey(), Array.Empty(), hkdfInfo); + var hkdfGen = new HkdfBytesGenerator(PgpUtilities.CreateDigest(HashAlgorithmTag.Sha256)); + hkdfGen.Init(hkdfParams); + var hkdfOutput = new byte[PgpUtilities.GetKeySizeInOctets(encAlgorithm)]; + hkdfGen.GenerateBytes(hkdfOutput, 0, hkdfOutput.Length); + + var encodedPubkeyContents = secret.PublicKeyPacket.GetEncodedContents(); + byte[] aadata = new byte[encodedPubkeyContents.Length + 1]; + aadata[0] = (byte)(0xC0 | (byte)secret.Tag); + Array.Copy(encodedPubkeyContents, 0, aadata, 1, encodedPubkeyContents.Length); + + var encAlgoName = PgpUtilities.GetSymmetricCipherName(encAlgorithm); + var aeadAlgoName = AeadUtils.GetAeadAlgorithmName(aeadAlgorithm); + var cipher = CipherUtilities.GetCipher($"{encAlgoName}/{aeadAlgoName}/NoPadding"); + + var aeadParams = new AeadParameters( + new KeyParameter(hkdfOutput), + 8 * AeadUtils.GetAuthTagLength(aeadAlgorithm), + iv, + aadata); + + cipher.Init(false, aeadParams); + data = cipher.DoFinal(encData); + } + else if (secret.PublicKeyPacket.Version >= PublicKeyPacket.Version4) { data = RecoverKeyData(encAlgorithm, "/CFB/NoPadding", key, iv, encData, 0, encData.Length); diff --git a/crypto/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index dab25543e..5210f5d1e 100644 --- a/crypto/src/openpgp/PgpUtilities.cs +++ b/crypto/src/openpgp/PgpUtilities.cs @@ -370,7 +370,24 @@ internal static KeyParameter DoMakeKeyFromPassPhrase(SymmetricKeyAlgorithmTag al int generatedBytes = 0; int loopCount = 0; - while (generatedBytes < keyBytes.Length) + if (s2k != null && s2k.Type == S2k.Argon2) + { + Argon2Parameters.Builder builder = + new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id) + .WithVersion(Argon2Parameters.ARGON2_VERSION_13) + .WithIterations(s2k.Passes) + .WithMemoryPowOfTwo(s2k.MemorySizeExponent) + .WithParallelism(s2k.Parallelism) + .WithSalt(s2k.GetIV()); + + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + + gen.Init(builder.Build()); + gen.GenerateBytes(rawPassPhrase, keyBytes, 0, keyBytes.Length); + return MakeKey(algorithm, keyBytes); + } + + while (generatedBytes < keyBytes.Length) { IDigest digest; if (s2k != null) diff --git a/crypto/test/data/openpgp/big-aead-msg.asc b/crypto/test/data/openpgp/big-aead-msg.asc new file mode 100644 index 000000000..5a00b879c --- /dev/null +++ b/crypto/test/data/openpgp/big-aead-msg.asc @@ -0,0 +1,57 @@ +-----BEGIN PGP MESSAGE----- +Comment: V6 SKESK + V2 SEIPD AEAD encrypted message that spans +Comment: over 4 chunks (chunk size 512 octets) +Comment: 2000 octets of /dev/zero encrypted with password +Comment: "password" using Argon2 and AES-256 in OCB mode +Comment: generated with gosop 2.0.0-alpha +Comment: Session key A96F671431CEB0F859CFC653976417CCC41... +Comment: Session key ...26BC0F93C30C6E5F0073E0B91E65A + +w1gGJgkCFAS8v5Skp/SlGwgm963nzHEoAwQQ0jy/KQBcQi0ZlxKX7FQpCBWuNH9D +ahN+JQDx/58tEnRr6CL5Guu36/OXRIxXIO+ni7IZYjL5rHobCk0NBO7b0ukCCQID +gCNsalrnzMkJZTzVPg5XmGahfHmgHuLM3pSARdRdL5N9xhhBj6/Huj0xgtAi2KKR +a+vzfnoFq0Zq+R053tiAxXGTPyxwdwb74UjEN58fd78jC4dSgV6OQeuA2E+waFTS +VCOFr1wTmKTp99byPUHkWkEepm+iKAUX43MR1WHPseJakTX9r7AwOU3GZtfK9T15 +EzzOYS2USDws1uFylWai9giPnkN3cV3cQJWQwwCjt2l14p+cIkpJWHvG9372uB3P +VQ0xovDyKjFBWAQ3OEzYieVuW1PQ5VVyWnFOxY2FIScAQkTZulJQ+E3XozAgowkV +q0qOd8PLq6GPPFnsm4A6Cepvaq63EJAvoHsa/4ku524kmVe+nOrhlzRU2DrV66hT +2bn3ywtu3B9Ji1CcGHFTBVvragFFbmBHjBdSwMNg3cT1oGOOGIiK8HqSjGeSfK+g +/Vn5z8iIeR9QQTW3Q/SpnVrQUMt353fTo42bN5okEbOj3X6kUQfS5/3lgse/7xIR +cRzBKknA0x7PtEHDb+KY/XSMUfnAWln6Huj7JdEiwG6DsxYlpSOckd1M7UyGLo/0 +sa/Rp7MhHPFUW6NwSM3YHAYv4jrLnPSYDUp6PGeaJl9fG7MXLqiWi9Jb45QvXs1B +krI+Bnxg6uhdBlL7hJjsH4nOV0/34QYcAlPgJ+lPL5arxv+ouJpGCZC3Zm9hd5Cf +Tlcp5IXa484/Qzrx4CKZGsTugV6tmQd15H6fGfyPlxK40Egvt7W4FO6p+OMX7peJ +mTKWSm9bJIF/bCv4CBch29kyfW7yb1n1Y7mL+5Y60y4HHFKDl3ftrPbqGskp9zqe +lDP21DWaCCfGZUXYX9TpnJZJl6o+YTghRiuobe+HM081Bk7PRQ82ZxqvSYDCCHMB +n8xAmLQijunT4aVFQlh/rQNRy5bz+gvWb/zBjIQ0+sR1Fx8jlFxU0nYp8ft2elfv +xXs7+87m5zPPkY4NBKPweV4vOslGSBdQASMnwLNb1Zp5IVQMuGSDrJwgiWQJcsuy +vjhTwO1FUcnPcAO7kV8TmX07ig4X0MLxQGZVlLX5CnFNKC7QZZ0KVSx17ZpDrrOj +VXXpbtHyuOoK1z+AOhvgPwvEvcJXpQLTjT3jbbLUL4wjMGhfKVMmOHdZD51gZGDl +8ziObW4+4hYegHigPlWCOLnbIYj6RGyuoccdiEImrt9Syi/SKida2y5KduEwDQEw +qAIFYPq/xE5+HZi6t7bsrSH94OpK+tzEGYoDDOBEKoOtwxqFV1Rb0ehwPPFafIfX +GZRYDqE30Sh9L1AN9LNSsEY/F8RYaEb60aQ2tvYipSHpEgSrBDY0oQ5YJXf2Rd2W +AH1eAEHkOP+gL66CIOlgLQsmp+U4/Kay5OVH34QwMaZUr2Wy4+dOCdl87sGarVCL +v1ygk4v2kpjLpj+E6aqkA+OI1AAIindUKzbxIk4kHdV0YFloF/0Z450LXw/A0Pda +hl9yiTJhEGvI5/jzFyhqP4YOFi0SZ+E9V66lFaqJSXBWLBZ1Xp8Hl/Yie8ArZh/i +BRakHuL2FMcXRnYv8B6b7P62N5YG9WrTs4AxERJMZmFdiKNnirta5vHSblj2Mn+3 +dAWRJ/N2fZBJOhXNrVYSAQc7W3odQfbtncS24ZeAd4TGLJ90//zyF1deo3AVIAqQ +BZEqTqDIORWzjy+2T2RF1ug6vyLHro5IQ66pHvEtXHXjU7YAE5+ZuFwHx5jp4est +L0BxjZCYZApa90Lwaql1FeFUnUAQplpFhGiZfXRSgtkCxkdQ+kW1U2dlo1J+oJQ7 +4ZLjgrFCL7t7m5m6seM7bD4Sbh/OfFcSNe8hEvJF6QuLUmw3UL56V3UfBR50AKAv +v5ajAAlCXA+KQp86/N4H0vZHkT6kZ8Hakmcbmz14nIp0QLkwI00XofX1oXipLrea +yt7/uXxH36+/Pz3orVNHPnSn8n++5KKhlm+OMQfr+NbJHBgJ6kz2k51yA1vuYDF9 +fX0Kx9QlnSpq2YNs/eeUp+VHYQT3td3r1Xq3XrQEhbwBzr1IUJlyIRkNu2RziekH +eCOmqS6QPHdx8S1MdA7XI9pIAT/oEaNxdMWa4u3QjP6F+TcaElAZhsQoDi/b4/uM +l5RLW6STHJ5Acmg1+9QBM3QjR+N/HKmkE0TL3BP9mI6t/pXLfMAGT8EMNmqdHVV4 +55N8UvwqACGYQPz6Gv8PloO27p5V4ggNuCoNbwZ0l/Npi+AXDUfzrVCwhLZYnPeV ++pqSaK+6IKH8xNV+HBsZZVs8mDRCJg4rm4iAjtv2ylnzUgbcDm012WuuyjANi2II +L+WbhSmMF+PM41K0DcHl6RFv2R83iPp7WELYIJnGHajPuXu+WV97WCIlnA1NdA2a +vRbfLs4ztvh3+VtLQwISRXtOPq9XpIAZ34uc330SRzaWlnQXejHtos1eG26Xq6Kq +/MiBEbz1RyhDDpTOKlSGsa8n5CveOWrufAjLjmLUQAekSy6uFxx9KDoh9SEiFh5o +MP6Em9OR64px1Cpoaye79lLs/PEHKMDZUp8E4GGOL/pQKmKK71n3PRpSipzNrUrs +rkf9GddnAJwf9WcpUEvwbPKAY392MyG0JGK8t+UpHnEn/q8/TlephIXl9BatamXh +OtqEB4m/GedoZYKZzzcRyyLu16l0k6+fj8tgT0G4Cm13shw1MlvEvfgRIjrmtKyb +38STUVxgHJzsSejMkGLXMPDx+InVcL/ESvmbbjBaSU31QphnD2xyPz6f0ZP/mCQP +aqnkxOuGhK8VlBy4dTlW3sKvL1dFMSiz0HXJEIkMBwIDkFdqD5bj+PfR6M1FR3Aw +sKZt+8/TOYGzbCH/2g== +-----END PGP MESSAGE----- \ No newline at end of file diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 88917149a..09b4dbc5b 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -88,9 +88,10 @@ public class PgpCryptoRefreshTest "b7gKqPxbyxbhljGygHQPnqau1eBzrQD5QVplPEDnemrnfmkrpx0GmhCfokxYz9jj" + "FtCgazStmsuOXF9SFQE="); - // Sample AEAD encryption and decryption + // Sample AEAD encryption and decryption - V6 SKESK + V2 SEIPD // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-aead-eax-encryption- - // encrypts the cleartext string Hello, world! with the passphrase password, using AES-128 with AEAD-EAX encryption. + // encrypts the cleartext string Hello, world! with the passphrase password, S2K type iterated+salted, + // using AES-128 with AEAD-EAX encryption. private readonly byte[] v6skesk_aes128_eax = Base64.Decode( "w0AGHgcBCwMIpa5XnR/F2Cv/aSJPkZmTs1Bvo7WaanPP+MXvxfQcV/tU4cImgV14" + "KPX5LEVOtl6+AKtZhsaObnxV0mkCBwEGn/kOOzIZZPOkKRPI3MZhkyUBUifvt+rq" + @@ -98,7 +99,8 @@ public class PgpCryptoRefreshTest "QCWKt5Wala0FHdqW6xVDHf719eIlXKeCYVRuM5o="); // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-aead-ocb-encryption- - // encrypts the cleartext string Hello, world! with the passphrase password, using AES-128 with AEAD-OCB encryption. + // encrypts the cleartext string Hello, world! with the passphrase password, S2K type iterated+salted, + // using AES-128 with AEAD-OCB encryption. private readonly byte[] v6skesk_aes128_ocb = Base64.Decode( "wz8GHQcCCwMIVqKY0vXjZFP/z8xcEWZO2520JZDX3EawckG2EsOBLP/76gDyNHsl" + "ZBEj+IeuYNT9YU4IN9gZ02zSaQIHAgYgpmH3MfyaMDK1YjMmAn46XY21dI6+/wsM" + @@ -106,13 +108,42 @@ public class PgpCryptoRefreshTest "K82nyM6dZeIS8wHLzZj9yt5pSod61CRzI/boVw=="); // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-aead-gcm-encryption- - // encrypts the cleartext string Hello, world! with the passphrase password, using AES-128 with AEAD-GCM encryption. + // encrypts the cleartext string Hello, world! with the passphrase password, S2K type iterated+salted, + // using AES-128 with AEAD-GCM encryption. private readonly byte[] v6skesk_aes128_gcm = Base64.Decode( "wzwGGgcDCwMI6dOXhbIHAAj/tC58SD70iERXyzcmubPbn/d25fTZpAlS4kRymIUa" + "v/91Jt8t1VRBdXmneZ/SaQIHAwb8uUSQvLmLvcnRBsYJAmaUD3LontwhtVlrFXax" + "Ae0Pn/xvxtZbv9JNzQeQlm5tHoWjAFN4TLHYtqBpnvEhVaeyrWJYUxtXZR/Xd3kS" + "+pXjXZtAIW9ppMJI2yj/QzHxYykHOZ5v+Q=="); + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-messages-encrypted-u + // V4 SKESK + V1 SEIPD using Argon2 (t=1, p=4, m=21) with AES-128/192/256, + // cleartext string "Hello, world!", passphrase "password" + private readonly byte[] v4skesk_argon2_aes128 = Base64.Decode( + "wycEBwScUvg8J/leUNU1RA7N/zE2AQQVnlL8rSLPP5VlQsunlO+ECxHSPgGYGKY+" + + "YJz4u6F+DDlDBOr5NRQXt/KJIf4m4mOlKyC/uqLbpnLJZMnTq3o79GxBTdIdOzhH" + + "XfA3pqV4mTzF"); + + private readonly byte[] v4skesk_argon2_aes192 = Base64.Decode( + "wy8ECAThTKxHFTRZGKli3KNH4UP4AQQVhzLJ2va3FG8/pmpIPd/H/mdoVS5VBLLw" + + "F9I+AdJ1Sw56PRYiKZjCvHg+2bnq02s33AJJoyBexBI4QKATFRkyez2gldJldRys" + + "LVg77Mwwfgl2n/d572WciAM="); + + private readonly byte[] v4skesk_argon2_aes256 = Base64.Decode( + "wzcECQS4eJUgIG/3mcaILEJFpmJ8AQQVnZ9l7KtagdClm9UaQ/Z6M/5roklSGpGu" + + "623YmaXezGj80j4B+Ku1sgTdJo87X1Wrup7l0wJypZls21Uwd67m9koF60eefH/K" + + "95D1usliXOEm8ayQJQmZrjf6K6v9PWwqMQ=="); + + // V6 SKESK + V2 SEIPD using Argon2 with AES-256 in OCB mode + // cleartext string "Hello, world!", passphrase "password" + // Session key 9DC22B5D8DFCED080C881885335E5A1A7E1215F17BBEC0B485655A308BE3D934 + // generated with gosop 2.0.0-alpha + private readonly byte[] v6skesk_argon2_aes256_ocb = Base64.Decode( + "w1gGJgkCFARXue/MBMPDOPspqjeXOAwCAwQQdUzaSpJVUWXsrfYfYX6Bu+PWWSv5" + + "v6yNbe7XcntA8BuivOCuH6FU3Mt0UJPZRO9/fRjiEGTuwg6Q7ar/gZ/N0lkCCQIM" + + "Zjd4SG7Tv4RJHeycolKmqSHDoK5XlOsA7vlw50nKuRjDyRfsPOFDfHz8hR/z7D1i" + + "HST68tjRCRmwqeqVgusCmBlXrXzYTkPXGtmZl2+EYazSACQFVg=="); + private readonly char[] emptyPassphrase = Array.Empty(); [Test] @@ -257,6 +288,31 @@ public void Version6PublicKeyCreationTest() shouldFail: true); } + + private void SignVerifyRoundtrip(PgpSecretKey signingKey, char[] passphrase) + { + byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); + byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); + + PgpSignatureGenerator sigGen = new PgpSignatureGenerator(signingKey.PublicKey.Algorithm, HashAlgorithmTag.Sha512); + PgpSignatureSubpacketGenerator spkGen = new PgpSignatureSubpacketGenerator(); + PgpPrivateKey privKey = signingKey.ExtractPrivateKey(passphrase); + spkGen.SetIssuerFingerprint(false, signingKey); + sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); + sigGen.Update(data); + sigGen.SetHashedSubpackets(spkGen.Generate()); + PgpSignature signature = sigGen.Generate(); + + AreEqual(signature.GetIssuerFingerprint(), signingKey.GetFingerprint()); + + VerifySignature(signature, data, signingKey.PublicKey); + VerifySignature(signature, wrongData, signingKey.PublicKey, shouldFail: true); + + byte[] encodedSignature = signature.GetEncoded(); + VerifyEncodedSignature(encodedSignature, data, signingKey.PublicKey); + VerifyEncodedSignature(encodedSignature, wrongData, signingKey.PublicKey, shouldFail: true); + } + [Test] public void Version6UnlockedSecretKeyParsingTest() { @@ -278,24 +334,7 @@ public void Version6UnlockedSecretKeyParsingTest() IsEquals(signingKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.Ed25519); IsEquals((ulong)signingKey.PublicKey.KeyId, 0xCB186C4F0609A697); - // generate and verify a v6 signature - byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); - byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); - PgpSignatureGenerator sigGen = new PgpSignatureGenerator(signingKey.PublicKey.Algorithm, HashAlgorithmTag.Sha512); - PgpSignatureSubpacketGenerator spkGen = new PgpSignatureSubpacketGenerator(); - PgpPrivateKey privKey = signingKey.ExtractPrivateKey(emptyPassphrase); - spkGen.SetIssuerFingerprint(false, signingKey); - sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); - sigGen.Update(data); - sigGen.SetHashedSubpackets(spkGen.Generate()); - PgpSignature signature = sigGen.Generate(); - - VerifySignature(signature, data, signingKey.PublicKey); - VerifySignature(signature, wrongData, signingKey.PublicKey, shouldFail: true); - - byte[] encodedSignature = signature.GetEncoded(); - VerifyEncodedSignature(encodedSignature, data, signingKey.PublicKey); - VerifyEncodedSignature(encodedSignature, wrongData, signingKey.PublicKey, shouldFail: true); + SignVerifyRoundtrip(signingKey, emptyPassphrase); // encryption key PgpSecretKey encryptionKey = secretKeys[1]; @@ -317,8 +356,10 @@ public void Version6UnlockedSecretKeyParsingTest() // generate and verify a v6 userid self-cert string userId = "Alice "; string wrongUserId = "Bob "; + PgpSignatureGenerator sigGen = new PgpSignatureGenerator(signingKey.PublicKey.Algorithm, HashAlgorithmTag.Sha512); + PgpPrivateKey privKey = signingKey.ExtractPrivateKey(emptyPassphrase); sigGen.InitSign(PgpSignature.PositiveCertification, privKey, new SecureRandom()); - signature = sigGen.GenerateCertification(userId, signingKey.PublicKey); + PgpSignature signature = sigGen.GenerateCertification(userId, signingKey.PublicKey); signature.InitVerify(signingKey.PublicKey); if (!signature.VerifyCertification(userId, signingKey.PublicKey)) { @@ -443,20 +484,7 @@ public void Version6Ed25519KeyPairCreationTest() IsTrue("subkey binding signature verification failed", bindingSig.VerifyCertification(pgppubkey, subKey.PublicKey)); // Sign-Verify roundtrip - byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); - byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); - PgpSignatureGenerator sigGen = new PgpSignatureGenerator(pgppubkey.Algorithm, HashAlgorithmTag.Sha512); - PgpSignatureSubpacketGenerator spkGen = new PgpSignatureSubpacketGenerator(); - PgpPrivateKey privKey = pgpseckey.ExtractPrivateKey(emptyPassphrase); - spkGen.SetIssuerFingerprint(false, pgpseckey); - sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); - sigGen.Update(data); - sigGen.SetHashedSubpackets(spkGen.Generate()); - signature = sigGen.Generate(); - - AreEqual(signature.GetIssuerFingerprint(), expectedFingerprint); - VerifySignature(signature, data, pgppubkey); - VerifySignature(signature, wrongData, pgppubkey, shouldFail: true); + SignVerifyRoundtrip(pgpseckey, emptyPassphrase); // encrypt-decrypt test AsymmetricCipherKeyPair alice = GetKeyPair(subKey); @@ -543,20 +571,7 @@ public void Version6Ed448KeyPairCreationTest() IsTrue("subkey binding signature verification failed", bindingSig.VerifyCertification(pgppubkey, subKey.PublicKey)); // Sign-Verify roundtrip - byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); - byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); - PgpSignatureGenerator sigGen = new PgpSignatureGenerator(pgppubkey.Algorithm, HashAlgorithmTag.Sha512); - PgpSignatureSubpacketGenerator spkGen = new PgpSignatureSubpacketGenerator(); - PgpPrivateKey privKey = pgpseckey.ExtractPrivateKey(emptyPassphrase); - spkGen.SetIssuerFingerprint(false, pgpseckey); - sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, rand); - sigGen.Update(data); - sigGen.SetHashedSubpackets(spkGen.Generate()); - signature = sigGen.Generate(); - - AreEqual(signature.GetIssuerFingerprint(), fpr); - VerifySignature(signature, data, pgppubkey); - VerifySignature(signature, wrongData, pgppubkey, shouldFail: true); + SignVerifyRoundtrip(pgpseckey, emptyPassphrase); // Encrypt-Decrypt test AsymmetricCipherKeyPair alice = GetKeyPair(subKey); @@ -573,9 +588,6 @@ public void Version6LockedSecretKeyParsingTest() * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-locked-v6-secret-key * The same secret key as in Version6UnlockedSecretKeyParsingTest, but the secret key * material is locked with a passphrase using AEAD and Argon2. - * - * AEAD/Argon passphrase decryption is not implemented yet, so we just test - * parsing and encoding */ PgpSecretKeyRing secretKeyRing = new PgpSecretKeyRing(v6LockedSecretKey); @@ -589,6 +601,19 @@ public void Version6LockedSecretKeyParsingTest() IsEquals(signingKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.Ed25519); IsEquals((ulong)signingKey.PublicKey.KeyId, 0xCB186C4F0609A697); + // try to decrypt with wrong passphrases + Assert.Throws(() => + { + PgpPrivateKey pk = signingKey.ExtractPrivateKey(emptyPassphrase); + }); + Assert.Throws(() => + { + PgpPrivateKey pk = signingKey.ExtractPrivateKey("wrong".ToCharArray()); + }); + + string passphrase = "correct horse battery staple"; + SignVerifyRoundtrip(signingKey, passphrase.ToCharArray()); + // encryption key PgpSecretKey encryptionKey = secretKeys[1]; IsEquals(encryptionKey.KeyEncryptionAlgorithm, SymmetricKeyAlgorithmTag.Aes256); @@ -596,6 +621,14 @@ public void Version6LockedSecretKeyParsingTest() IsEquals(encryptionKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X25519); IsEquals(encryptionKey.PublicKey.KeyId, 0x12C83F1E706F6308); + // decrypt test + AsymmetricCipherKeyPair alice = GetKeyPair(encryptionKey, passphrase); + IAsymmetricCipherKeyPairGenerator kpGen = new X25519KeyPairGenerator(); + kpGen.Init(new X25519KeyGenerationParameters(new SecureRandom())); + AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); + IsTrue("X25519 agreement failed", EncryptThenDecryptTest(alice, bob, encryptionKey.PublicKey.Algorithm)); + + // Encode-Decode roundtrip using (MemoryStream ms = new MemoryStream()) { @@ -785,11 +818,13 @@ private static bool EncryptThenDecryptTest(AsymmetricCipherKeyPair alice, Asymme [Test] public void Version6SkeskVersion2SeipdTest() { + // encrypts the cleartext string "Hello, world!" with the passphrase "password", + // S2K type iterated+salted, using AES-128 with AEAD encryption. byte[][] messages = new byte[][] { - v6skesk_aes128_eax, - v6skesk_aes128_ocb, - v6skesk_aes128_gcm + v6skesk_aes128_eax, // from crypto-refresh A.9 + v6skesk_aes128_ocb, // from crypto-refresh A.10 + v6skesk_aes128_gcm // from crypto-refresh A.11 }; byte[] plaintext = Encoding.UTF8.GetBytes("Hello, world!"); @@ -817,6 +852,19 @@ public void Version6SkeskVersion2SeipdTest() } } + // wrong passsword + byte[] wrongpassword = Encoding.UTF8.GetBytes("wrongpassword"); + for (int i = 0; i < messages.Length; i++) + { + PgpObjectFactory factory = new PgpObjectFactory(messages[i]); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + var encData0 = encData[0] as PgpPbeEncryptedData; + var err = Assert.Throws(() => + { + var stream = encData0.GetDataStreamRaw(wrongpassword); + }); + } + for (int i = 0; i < messages.Length; i++) { // corrupt AEAD nonce @@ -887,6 +935,89 @@ public void Version6SkeskVersion2SeipdTest() }); } + /* + * V6 SKESK + V2 SEIPD AEAD encrypted message that spans over 4 chunks + * (chunk size 512 octets) + * 2000 octets of /dev/zero encrypted with password "password" using Argon2 + * and AES-256 in OCB mode. Generated with gosop 2.0.0-alpha + * Session key A96F671431CEB0F859CFC653976417CCC4126BC0F93C30C6E5F0073E0B91E65A + */ + { + plaintext = new byte[2000]; + Arrays.Fill(plaintext, 0); + + Stream message = PgpUtilities.GetDecoderStream( + SimpleTest.GetTestDataAsStream("openpgp.big-aead-msg.asc")); + + PgpObjectFactory factory = new PgpObjectFactory(message); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encData is null); + + var encData0 = encData[0] as PgpPbeEncryptedData; + FailIf("invalid PgpPbeEncryptedData", encData0 is null); + + using (var stream = encData0.GetDataStreamRaw(password)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (var ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + var decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + } + } + + [Test] + public void SkeskWithArgon2Test() + { + byte[][] messages = new byte[][] + { + v4skesk_argon2_aes128, // from crypto-refresh A.12.1 + v4skesk_argon2_aes192, // from crypto-refresh A.12.2 + v4skesk_argon2_aes256, // from crypto-refresh A.12.3 + v6skesk_argon2_aes256_ocb // generated with gosop 2.0.0-alpha + }; + + byte[] plaintext = Encoding.UTF8.GetBytes("Hello, world!"); + byte[] password = Encoding.UTF8.GetBytes("password"); + + for (int i = 0; i < messages.Length; i++) + { + PgpObjectFactory factory = new PgpObjectFactory(messages[i]); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encData is null); + + var encData0 = encData[0] as PgpPbeEncryptedData; + FailIf("invalid PgpPbeEncryptedData", encData0 is null); + + using (var stream = encData0.GetDataStreamRaw(password)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (var ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + var decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + } + + // wrong passsword + byte[] wrongpassword = Encoding.UTF8.GetBytes("wrongpassword"); + for (int i = 0; i < messages.Length; i++) + { + PgpObjectFactory factory = new PgpObjectFactory(messages[i]); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + var encData0 = encData[0] as PgpPbeEncryptedData; + var err = Assert.Throws(() => + { + var stream = encData0.GetDataStreamRaw(wrongpassword); + }); + } } @@ -907,6 +1038,7 @@ public override void PerformTest() Version6SampleInlineSignedMessageVerifySignatureTest(); Version6GenerateAndVerifyInlineSignatureTest(); Version6SkeskVersion2SeipdTest(); + SkeskWithArgon2Test(); } } } \ No newline at end of file From cb265792870e0508f587dd0828ef1f55aefe42f5 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Tue, 16 Apr 2024 19:13:47 +0200 Subject: [PATCH 23/37] V6 PKESK decryption --- crypto/src/bcpg/AeadUtils.cs | 15 +- crypto/src/bcpg/PublicKeyEncSessionPacket.cs | 196 +++++++++++-- crypto/src/openpgp/PgpPbeEncryptedData.cs | 12 +- .../src/openpgp/PgpPublicKeyEncryptedData.cs | 268 +++++++++++++++--- .../test/data/openpgp/big-pkesk-aead-msg.asc | 57 ++++ ...ig-aead-msg.asc => big-skesk-aead-msg.asc} | 0 .../src/openpgp/test/PgpCryptoRefreshTest.cs | 122 +++++++- .../test/PgpInteroperabilityTestSuite.cs | 78 +++++ 8 files changed, 668 insertions(+), 80 deletions(-) create mode 100644 crypto/test/data/openpgp/big-pkesk-aead-msg.asc rename crypto/test/data/openpgp/{big-aead-msg.asc => big-skesk-aead-msg.asc} (100%) diff --git a/crypto/src/bcpg/AeadUtils.cs b/crypto/src/bcpg/AeadUtils.cs index d2bda51e8..e828f735a 100644 --- a/crypto/src/bcpg/AeadUtils.cs +++ b/crypto/src/bcpg/AeadUtils.cs @@ -1,7 +1,8 @@ using System; -using System.IO; using Org.BouncyCastle.Bcpg.OpenPgp; -using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Security; + namespace Org.BouncyCastle.Bcpg { @@ -91,5 +92,15 @@ public static void SplitMessageKeyAndIv(byte[] messageKeyAndIv, SymmetricKeyAlgo Array.Copy(messageKeyAndIv, messageKey, messageKey.Length); Array.Copy(messageKeyAndIv, messageKey.Length, iv, 0, ivLen-8); } + + public static BufferedAeadBlockCipher CreateAeadCipher( + SymmetricKeyAlgorithmTag encAlgorithm, AeadAlgorithmTag aeadAlgorithm) + { + string algo = PgpUtilities.GetSymmetricCipherName(encAlgorithm); + string mode = GetAeadAlgorithmName(aeadAlgorithm); + string cName = $"{algo}/{mode}/NoPadding"; + + return CipherUtilities.GetCipher(cName) as BufferedAeadBlockCipher; + } } } \ No newline at end of file diff --git a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs index b38b19bd5..dd7e1d7f0 100644 --- a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs @@ -4,6 +4,7 @@ using Org.BouncyCastle.Math; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.IO; +using Org.BouncyCastle.Crypto.Utilities; namespace Org.BouncyCastle.Bcpg { @@ -11,29 +12,78 @@ namespace Org.BouncyCastle.Bcpg public class PublicKeyEncSessionPacket : ContainedPacket //, PublicKeyAlgorithmTag { - private readonly int version; - private readonly long keyId; - private readonly PublicKeyAlgorithmTag algorithm; - private readonly byte[][] data; + private readonly int version; // V3, V6 + private readonly long keyId; // V3 only + private readonly PublicKeyAlgorithmTag algorithm; // V3, V6 + private readonly byte[][] data; // V3, V6 + private readonly int keyVersion; // V6 only + private readonly byte[] keyFingerprint; // V6 only - internal PublicKeyEncSessionPacket( + /// + /// Version 3 PKESK packet. + /// + /// + public const int Version3 = 3; + + /// + /// Version 6 PKESK packet. + /// + /// + public const int Version6 = 6; + + internal PublicKeyEncSessionPacket( BcpgInputStream bcpgIn) :base(PacketTag.PublicKeyEncryptedSession) { version = bcpgIn.ReadByte(); + switch (version) + { + case Version3: + keyId |= (long)bcpgIn.ReadByte() << 56; + keyId |= (long)bcpgIn.ReadByte() << 48; + keyId |= (long)bcpgIn.ReadByte() << 40; + keyId |= (long)bcpgIn.ReadByte() << 32; + keyId |= (long)bcpgIn.ReadByte() << 24; + keyId |= (long)bcpgIn.ReadByte() << 16; + keyId |= (long)bcpgIn.ReadByte() << 8; + keyId |= (uint)bcpgIn.ReadByte(); + break; + case Version6: + int keyInfoLength = bcpgIn.ReadByte(); + if (keyInfoLength == 0) + { + // anonymous recipient + keyVersion = 0; + keyFingerprint = Array.Empty(); + keyId = 0; + } + else + { + keyVersion = bcpgIn.ReadByte(); + keyFingerprint = new byte[keyInfoLength - 1]; + bcpgIn.ReadFully(keyFingerprint); - keyId |= (long)bcpgIn.ReadByte() << 56; - keyId |= (long)bcpgIn.ReadByte() << 48; - keyId |= (long)bcpgIn.ReadByte() << 40; - keyId |= (long)bcpgIn.ReadByte() << 32; - keyId |= (long)bcpgIn.ReadByte() << 24; - keyId |= (long)bcpgIn.ReadByte() << 16; - keyId |= (long)bcpgIn.ReadByte() << 8; - keyId |= (uint)bcpgIn.ReadByte(); - + switch (keyVersion) + { + case PublicKeyPacket.Version4: + keyId = (long)Pack.BE_To_UInt64(keyFingerprint, keyFingerprint.Length - 8); + break; + case PublicKeyPacket.Version5: + case PublicKeyPacket.Version6: + keyId = (long)Pack.BE_To_UInt64(keyFingerprint); + break; + default: + throw new InvalidOperationException($"unsupported OpenPGP key packet version: {keyVersion}"); + } + } + break; + default: + throw new UnsupportedPacketVersionException($"Unsupported PGP public key encrypted session key packet version encountered: {version}"); + } + algorithm = (PublicKeyAlgorithmTag) bcpgIn.ReadByte(); - switch ((PublicKeyAlgorithmTag) algorithm) + switch (algorithm) { case PublicKeyAlgorithmTag.RsaEncrypt: case PublicKeyAlgorithmTag.RsaGeneral: @@ -51,18 +101,49 @@ internal PublicKeyEncSessionPacket( case PublicKeyAlgorithmTag.ECDH: data = new byte[][]{ Streams.ReadAll(bcpgIn) }; break; - default: + case PublicKeyAlgorithmTag.X25519: + case PublicKeyAlgorithmTag.X448: + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for- + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for-x + + // 32 (for X25519) or 56 (for X448) octets representing an ephemeral public key. + int keylen = algorithm == PublicKeyAlgorithmTag.X25519 ? 32 : 56; + byte[] ephemeralPubKey = new byte[keylen]; + bcpgIn.ReadFully(ephemeralPubKey); + + // A one-octet size of the following fields. + int esklen = bcpgIn.ReadByte(); + + // The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet). + // The encrypted session key. + byte[] encryptedSessionKey = new byte[esklen]; + bcpgIn.ReadFully(encryptedSessionKey); + + data = new byte[][]{ + ephemeralPubKey, + encryptedSessionKey, + }; + break; + + default: throw new IOException("unknown PGP public key algorithm encountered"); } } + + /// + /// Create a new V3 PKESK packet. + /// + /// ID of the recipient key, 0 for anonymo + /// public key algorithm + /// session data public PublicKeyEncSessionPacket( long keyId, PublicKeyAlgorithmTag algorithm, byte[][] data) : base(PacketTag.PublicKeyEncryptedSession) { - this.version = 3; + this.version = Version3; this.keyId = keyId; this.algorithm = algorithm; this.data = new byte[data.Length][]; @@ -82,7 +163,22 @@ public long KeyId get { return keyId; } } - public PublicKeyAlgorithmTag Algorithm + public bool IsRecipientAnonymous + { + get { return keyId == 0; } + } + + public byte[] GetKeyFingerprint() + { + return Arrays.Clone(keyFingerprint); + } + + public int KeyVersion + { + get { return keyVersion; } + } + + public PublicKeyAlgorithmTag Algorithm { get { return algorithm; } } @@ -94,20 +190,58 @@ public byte[][] GetEncSessionKey() public override void Encode(BcpgOutputStream bcpgOut) { - MemoryStream bOut = new MemoryStream(); - using (var pOut = new BcpgOutputStream(bOut)) - { - pOut.WriteByte((byte)version); - pOut.WriteLong(keyId); - pOut.WriteByte((byte)algorithm); - - for (int i = 0; i < data.Length; ++i) - { - pOut.Write(data[i]); - } - } + using (MemoryStream bOut = new MemoryStream()) + { + using (var pOut = new BcpgOutputStream(bOut)) + { + pOut.WriteByte((byte)version); + + switch (version) + { + case Version3: + pOut.WriteLong(keyId); + break; + + case Version6: + if (keyVersion == 0) + { + // anonymous recipient + pOut.WriteByte(0); + } + else + { + pOut.WriteByte((byte)(keyFingerprint.Length + 1)); + pOut.WriteByte((byte)keyVersion); + pOut.Write(keyFingerprint); + } + break; + default: + throw new UnsupportedPacketVersionException($"Unsupported PGP public key encrypted session key packet version encountered: {version}"); + } - bcpgOut.WritePacket(PacketTag.PublicKeyEncryptedSession, bOut.ToArray()); + pOut.WriteByte((byte)algorithm); + + if (algorithm == PublicKeyAlgorithmTag.X25519 || algorithm == PublicKeyAlgorithmTag.X448) + { + // ephemeral public key. + pOut.Write(data[0]); + // One-octet size of the encrypted session key (prefixed by the one-octet + // algorithm identifier, in the case of a v3 PKESK packet). + pOut.WriteByte((byte)(data[1].Length)); + // encrypted session key + pOut.Write(data[1]); + } + else + { + for (int i = 0; i < data.Length; ++i) + { + pOut.Write(data[i]); + } + } + } + + bcpgOut.WritePacket(PacketTag.PublicKeyEncryptedSession, bOut.ToArray()); + } } } } diff --git a/crypto/src/openpgp/PgpPbeEncryptedData.cs b/crypto/src/openpgp/PgpPbeEncryptedData.cs index 91bec3fd0..e007b6a6d 100644 --- a/crypto/src/openpgp/PgpPbeEncryptedData.cs +++ b/crypto/src/openpgp/PgpPbeEncryptedData.cs @@ -238,7 +238,7 @@ private Stream DoGetDataStreamVersion2(SymmetricEncIntegrityPacket seipd, byte[] var aadata = seipd.GetAAData(); var salt = seipd.GetSalt(); PgpUtilities.DeriveAeadMessageKeyAndIv(sessionKey, seipd.CipherAlgorithm, seipd.AeadAlgorithm, salt, aadata, out var messageKey, out var iv); - var cipher = CreateAeadCipher(seipd.CipherAlgorithm, seipd.AeadAlgorithm); + var cipher = AeadUtils.CreateAeadCipher(seipd.CipherAlgorithm, seipd.AeadAlgorithm); var aeadStream = new AeadInputStream( encData.GetInputStream(), @@ -286,15 +286,5 @@ private IBufferedCipher CreateStreamCipher( return CipherUtilities.GetCipher(cName); } - private static BufferedAeadBlockCipher CreateAeadCipher( - SymmetricKeyAlgorithmTag keyAlgorithm, AeadAlgorithmTag aeadAlgorithm) - { - string algo = PgpUtilities.GetSymmetricCipherName(keyAlgorithm); - string mode = AeadUtils.GetAeadAlgorithmName(aeadAlgorithm); - string cName = $"{algo}/{mode}/NoPadding"; - - return CipherUtilities.GetCipher(cName) as BufferedAeadBlockCipher; - } - } } diff --git a/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs b/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs index 645973215..36d23a764 100644 --- a/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs +++ b/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs @@ -1,8 +1,10 @@ using System; using System.IO; +using System.Text; using Org.BouncyCastle.Asn1.Cryptlib; using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.IO; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Math; @@ -26,9 +28,56 @@ internal PgpPublicKeyEncryptedData( : base(encData) { this.keyData = keyData; + EnforceConstraints(); } - private static IBufferedCipher GetKeyCipher( + private void EnforceConstraints() + { + switch (keyData.Version) + { + case PublicKeyEncSessionPacket.Version3: + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-3-public-key-encryp + // A version 3 PKESK packet precedes a version 1 SEIPD packet. In historic data, it is sometimes + // found preceding a deprecated SED packet. + // A V3 PKESK packet MUST NOT precede a V2 SEIPD packet. + if (encData is SymmetricEncDataPacket) + { + return; + } + if (encData is SymmetricEncIntegrityPacket seipd1) + { + if (seipd1.Version == SymmetricEncIntegrityPacket.Version1) + { + return; + } + throw new ArgumentException($"Version 3 PKESK cannot precede SEIPD of version {seipd1.Version}"); + } + break; + + case PublicKeyEncSessionPacket.Version6: + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-6-public-key-encryp + //A version 6 PKESK packet precedes a version 2 SEIPD packet. + //A V6 PKESK packet MUST NOT precede a V1 SEIPD packet or a deprecated SED packet. + if (encData is SymmetricEncDataPacket) + { + throw new ArgumentException("Version 6 PKESK MUST NOT precede a deprecated SED packet."); + } + + if (encData is SymmetricEncIntegrityPacket seipd2) + { + if (seipd2.Version == SymmetricEncIntegrityPacket.Version2) + { + return; + } + throw new ArgumentException($"Version 6 PKESK cannot precede SEIPD of version {seipd2.Version}"); + } + break; + default: + throw new UnsupportedPacketVersionException($"Unsupported PGP public key encrypted session key packet version encountered: {keyData.Version}"); + } + } + + private static IBufferedCipher GetKeyCipher( PublicKeyAlgorithmTag algorithm) { try @@ -58,9 +107,14 @@ private static IBufferedCipher GetKeyCipher( private bool ConfirmCheckSum( byte[] sessionInfo) { - int check = 0; + // for X25519 and X448 no checksum or padding are appended to the session key before key wrapping + if (keyData.Algorithm == PublicKeyAlgorithmTag.X25519 || keyData.Algorithm == PublicKeyAlgorithmTag.X448) + { + return true; + } - for (int i = 1; i != sessionInfo.Length - 2; i++) + int check = 0; + for (int i = 1; i != sessionInfo.Length - 2; i++) { check += sessionInfo[i] & 0xff; } @@ -75,16 +129,75 @@ public long KeyId get { return keyData.KeyId; } } - /// - /// Return the algorithm code for the symmetric algorithm used to encrypt the data. - /// - public SymmetricKeyAlgorithmTag GetSymmetricAlgorithm( - PgpPrivateKey privKey) - { - byte[] sessionData = RecoverSessionData(privKey); + /// The key fingerprint for the key used to encrypt the data (v6 only). + public byte[] GetKeyFingerprint() + { + return keyData.GetKeyFingerprint(); + } - return (SymmetricKeyAlgorithmTag)sessionData[0]; - } + /// + /// Return the algorithm code for the symmetric algorithm used to encrypt the data. + /// + public SymmetricKeyAlgorithmTag GetSymmetricAlgorithm( + PgpPrivateKey privKey) + { + if (keyData.Version == PublicKeyEncSessionPacket.Version3) + { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for- + // In V3 PKESK, the symmetric algorithm Id + // * with X25519 and X448 is not encrypted, it's prepended in plaintext + // to the encrypted session key. + // * with other algorithms, it's is encrypted with the session key + + if (keyData.Algorithm == PublicKeyAlgorithmTag.X25519 || keyData.Algorithm == PublicKeyAlgorithmTag.X448) + { + byte[][] secKeyData = keyData.GetEncSessionKey(); + return (SymmetricKeyAlgorithmTag)secKeyData[1][0]; + } + else + { + byte[] sessionData = RecoverSessionData(privKey); + + return (SymmetricKeyAlgorithmTag)sessionData[0]; + } + } + else if (keyData.Version == PublicKeyEncSessionPacket.Version6) + { + // V6 PKESK stores the cipher algorithm in the V2 SEIPD packet fields. + return ((SymmetricEncIntegrityPacket)encData).CipherAlgorithm; + } + else + { + throw new UnsupportedPacketVersionException($"Unsupported PGP public key encrypted session key packet version encountered: {keyData.Version}"); + } + } + + private Stream GetDataStreamSeipdVersion2(byte[] sessionData, SymmetricEncIntegrityPacket seipd) + { + var encAlgo = seipd.CipherAlgorithm; + var aeadAlgo = seipd.AeadAlgorithm; + var aadata = seipd.GetAAData(); + var salt = seipd.GetSalt(); + + var sessionKey = ParameterUtilities.CreateKeyParameter( + PgpUtilities.GetSymmetricCipherName(encAlgo), + sessionData, 0, sessionData.Length); + + PgpUtilities.DeriveAeadMessageKeyAndIv(sessionKey, encAlgo, aeadAlgo, salt, aadata, out var messageKey, out var iv); + var cipher = AeadUtils.CreateAeadCipher(seipd.CipherAlgorithm, seipd.AeadAlgorithm); + + var aeadStream = new AeadInputStream( + encData.GetInputStream(), + cipher, + messageKey, + iv, + aeadAlgo, + seipd.ChunkSize, + aadata); + + encStream = BcpgInputStream.Wrap(aeadStream); + return encStream; + } /// Return the decrypted data stream for the packet. public Stream GetDataStream( @@ -92,10 +205,27 @@ public Stream GetDataStream( { byte[] sessionData = RecoverSessionData(privKey); + if (keyData.Version == PublicKeyEncSessionPacket.Version6) + { + // V6 PKESK + V2 SEIPD + return GetDataStreamSeipdVersion2(sessionData, (SymmetricEncIntegrityPacket)encData); + } + if (!ConfirmCheckSum(sessionData)) throw new PgpKeyValidationException("key checksum failed"); - SymmetricKeyAlgorithmTag symmAlg = (SymmetricKeyAlgorithmTag)sessionData[0]; + SymmetricKeyAlgorithmTag symmAlg; + if (keyData.Algorithm == PublicKeyAlgorithmTag.X25519 || keyData.Algorithm == PublicKeyAlgorithmTag.X448) + { + // with X25519 and X448 is not encrypted, the symmetric algorithm Id is + // prepended in plaintext to the encrypted session key. + byte[][] secKeyData = keyData.GetEncSessionKey(); + symmAlg = (SymmetricKeyAlgorithmTag)secKeyData[1][0]; + } + else + { + symmAlg = (SymmetricKeyAlgorithmTag)sessionData[0]; + } if (symmAlg == SymmetricKeyAlgorithmTag.Null) return encData.GetInputStream(); @@ -127,8 +257,17 @@ public Stream GetDataStream( try { - KeyParameter key = ParameterUtilities.CreateKeyParameter( - cipherName, sessionData, 1, sessionData.Length - 3); + // no checksum and padding for X25519 and X448 + int offset = 0; + int length = sessionData.Length; + if (keyData.Algorithm != PublicKeyAlgorithmTag.X25519 && keyData.Algorithm != PublicKeyAlgorithmTag.X448) + { + offset = 1; + length -= 3; + } + + KeyParameter key = ParameterUtilities.CreateKeyParameter( + cipherName, sessionData, offset, length); byte[] iv = new byte[cipher.GetBlockSize()]; @@ -189,43 +328,102 @@ private byte[] RecoverSessionData(PgpPrivateKey privKey) { byte[][] secKeyData = keyData.GetEncSessionKey(); + if (keyData.Algorithm == PublicKeyAlgorithmTag.X25519 || keyData.Algorithm == PublicKeyAlgorithmTag.X448) + { + // See sect. 5.1.6. and 5.1.7 of crypto-refresh for the description of + // the key derivation algorithm for X25519 and X448 + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for- + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for-x + byte[] eph = secKeyData[0]; + byte[] esk = secKeyData[1]; + + IRawAgreement agreement; + IDigest digestForHkdf; + byte[] hkdfInfo; + AsymmetricKeyParameter ephPubkey; + SymmetricKeyAlgorithmTag wrappingAlgo; + + if (keyData.Algorithm == PublicKeyAlgorithmTag.X25519) + { + agreement = new X25519Agreement(); + ephPubkey = new X25519PublicKeyParameters(eph); + digestForHkdf = PgpUtilities.CreateDigest(HashAlgorithmTag.Sha256); + hkdfInfo = Encoding.ASCII.GetBytes("OpenPGP X25519"); + wrappingAlgo = SymmetricKeyAlgorithmTag.Aes128; + } + else + { + agreement = new X448Agreement(); + ephPubkey = new X448PublicKeyParameters(eph); + digestForHkdf = PgpUtilities.CreateDigest(HashAlgorithmTag.Sha512); + hkdfInfo = Encoding.ASCII.GetBytes("OpenPGP X448"); + wrappingAlgo = SymmetricKeyAlgorithmTag.Aes256; + } + + agreement.Init(privKey.Key); + byte[] sharedSecret = new byte[agreement.AgreementSize]; + agreement.CalculateAgreement(ephPubkey, sharedSecret, 0); + + byte[] pubKeyMaterial = ((OctetArrayBcpgKey)privKey.PublicKeyPacket.Key).GetKey(); + byte[] ikm = Arrays.ConcatenateAll(eph, pubKeyMaterial, sharedSecret); + byte[] hkdfSalt = Array.Empty(); + var hkdfParams = new HkdfParameters(ikm, hkdfSalt, hkdfInfo); + var hkdfGen = new HkdfBytesGenerator(digestForHkdf); + hkdfGen.Init(hkdfParams); + var hkdfOutput = new byte[PgpUtilities.GetKeySizeInOctets(wrappingAlgo)]; + hkdfGen.GenerateBytes(hkdfOutput, 0, hkdfOutput.Length); + + KeyParameter kek = ParameterUtilities.CreateKeyParameter("AES", hkdfOutput); + var wrapper = PgpUtilities.CreateWrapper(wrappingAlgo); + wrapper.Init(false, kek); + int offset = 0; + int length = esk.Length; + if (keyData.Version == PublicKeyEncSessionPacket.Version3) + { + offset = 1; + length--; + } + var keyBytes = wrapper.Unwrap(esk, offset, length); + return keyBytes; + } + if (keyData.Algorithm != PublicKeyAlgorithmTag.ECDH) { IBufferedCipher cipher = GetKeyCipher(keyData.Algorithm); try - { + { cipher.Init(false, privKey.Key); - } - catch (InvalidKeyException e) - { - throw new PgpException("error setting asymmetric cipher", e); - } + } + catch (InvalidKeyException e) + { + throw new PgpException("error setting asymmetric cipher", e); + } if (keyData.Algorithm == PublicKeyAlgorithmTag.RsaEncrypt - || keyData.Algorithm == PublicKeyAlgorithmTag.RsaGeneral) - { + || keyData.Algorithm == PublicKeyAlgorithmTag.RsaGeneral) + { byte[] bi = secKeyData[0]; cipher.ProcessBytes(bi, 2, bi.Length - 2); - } - else - { - ElGamalPrivateKeyParameters k = (ElGamalPrivateKeyParameters)privKey.Key; - int size = (k.Parameters.P.BitLength + 7) / 8; + } + else + { + ElGamalPrivateKeyParameters k = (ElGamalPrivateKeyParameters)privKey.Key; + int size = (k.Parameters.P.BitLength + 7) / 8; ProcessEncodedMpi(cipher, size, secKeyData[0]); ProcessEncodedMpi(cipher, size, secKeyData[1]); - } + } try - { + { return cipher.DoFinal(); - } - catch (Exception e) - { - throw new PgpException("exception decrypting secret key", e); - } + } + catch (Exception e) + { + throw new PgpException("exception decrypting secret key", e); + } } ECDHPublicBcpgKey ecPubKey = (ECDHPublicBcpgKey)privKey.PublicKeyPacket.Key; diff --git a/crypto/test/data/openpgp/big-pkesk-aead-msg.asc b/crypto/test/data/openpgp/big-pkesk-aead-msg.asc new file mode 100644 index 000000000..c481452ad --- /dev/null +++ b/crypto/test/data/openpgp/big-pkesk-aead-msg.asc @@ -0,0 +1,57 @@ +-----BEGIN PGP MESSAGE----- +Comment: V6 PKESK + V2 SEIPD AEAD encrypted message that spans +Comment: over 4 chunks (chunk size 512 octets) +Comment: 2000 octets of /dev/zero encrypted with sample V6 +Comment: certificate from crypto-refresh Appendix A.3 +Comment: generated with gosop 2.0.0-alpha +Comment: Session key CFB73D46CF7C13B7535227BEDB5B2D8B4023C5B... +Comment: Session key ...58289D19CF2C33B0DB388B0B6 + +wW0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRkZAF3uCv64JHab +dMihIp+6i/QylLjksSURIA+Z1HHoSChcgayIgr6j/Q4A8BYSTdpcMdlXUakz8dpw +yxEWODQA2lQ5UMnl4+ZT0ukCCQIDOl2tRUXUTtbMzZx+EiDOerM0byHPZaEBHDhk +t9kB+i4cYJRnBKylw8pgS7Xp0727plIARoGamB004i2eO2BEabdfstHli3dfsCpq +esQeDKMVdUCPPaOb4FwcNUrSJPNWPQeDdQLHAUUXCFEalN5OZglmzLBDZNyb0LFR +k/oVvPyNlmX5Rt+ZW3L8cEoHzfJdLNJWEC2TRKFg3GVFrwucEjEpJtQsha1/x7cd +83Cvz9zEX/A3clkWj855KMx/AK4hkn9oKgQuWgFEIb5H4hyNLkpUPGlZg4o+fTwj +vD6pcJIZBx/rxS7QHg9jsYQVLQUk2t4K9B3cq4bAKavWxVfISGMrs3VgCcG1ePKH +FFfek6U98sL39mu+dklPGWP9rLTCk3dlAc0mMvadMapcH/ypk1YZtK0iEDEMODqL +rxXFmE4Mi0FPemW+63/o0GGCFz5YRNOf0cqKwysLpQr7Sa4uGnZv3ykuXbtb9l65 +i6i0I+F5IojQfUaR6gjfOQS2DHXEOey+xP+ZbQ8+M61wr97IbEXXo5d3Z5Ytbqot +s6YYHMWL1XkX0F4L+wTRWuDhIGTFCPqY5v1ejccT5SMmG1qcNZM1yaKXuaNWXWVa +KAcGw5Zh2HRn3kkXAZTM/v/va5MZaYd+MbTfzrQ1Wym1YNl2L80uJFg6AK5aRc4s +YukmxdeszVowz9LF+7tzr+TAY14+VJmiOB5yFue5dj6Rz0Z4veerutVz23T2izLQ +UYdY48sQDYSipckg1vUbvTx2+3/B+AN/xFv9hb6whINLE1Ifg2zExCOxtZytmHN9 +kYNmHMTDkXjioxFepdHMv95DTZgwx5T3imOwvfNsYlGP1g74pjiG4AM0BSiOVUse +QmhyTvm3bniSwcnkBwPrnUw4yiCVX796QL8IvZBfY2t0n5P/LUHnYQ3HTnfRwMC9 +Qj3U0lRQwAlkKLHcCapsq6XLwbiL6gozq0G13WVe3hdwi6Do1kYHpl66lmnPbW09 +8Nos1iEoptMdiesFmPqQ2d6peqY1QPfrG/MKGM/nH8+GiVo7qCDjdv1ZPW9ebHln +hKOrA+5c0BpPozBUULSUYo1nIXv8/zGVuoQl9T/c1Be4BS/Ub8D4l1/T5EQ2qb5c +vSrQ33Rx9uwSxWMZwZ41w53/JbgCVgZSpiAIXQdtdPBwBPn4YKis7DW1aiTqJJRH +jvJaEveBzlZzPpNCZkL/vLKJC4b9UmPfzbUo9xoAFmDZo1c3hUGHDhNt/qEba2VW +XaCYg5eJtYYtcH1dNg5O4Olc54a6mBpoc0KS3nvDzM/UhLub0AZfUM8vH/hXAEW+ +Efcb0fJzx5Yl3MNMvhAgxwN0u3icaBS03Yo7VBCmQ4ti+ela3h2zP5MxihBI4iXH +gh1WSKrk0GMTK6uDC8TSolkclOdGx7+V2ftSrMk5lJ+qaGQatu6LJhjirjifYlGY +cHRDOqGAbMvAhdaARcIxpi5Cu2HCk5myflAXN1t+O+s+N8VSyKPkIOMU1y4AVQ27 +kR7uPFNgd9F1AzKZzOVgZiP3Kqcjoo5sGJiqfVEbnsKidB4ddqgrmz40AgqkC9ZU +WtKjMByUPvJo0S1ZvBlawgi038KiEh2vmbPSjgbLMrKPK9ZY45Y6kiW5Safu7N2d +4TRIBByuF6dhKTMC0KoZqv7/CQ8XhZlWhbphNO7f0IeY5qGd/mhOPRbNRQEt0CWL +N+NBELK9xB5nSEpUxejWq8XbBz+wtn0SE/mNev/ebVYNcWBVJdQJyBotfSCjeJh0 +dXAbxE/5X3x7no5S9MFmTn/BIBFTnK2RrNiaSPZUjEOVL8qDTnPSq5VLUVRK0QNl +WEX7/Y5BCYlxFpc6inK1RvYIcU4ZR15ZyVsrcMvdapyU5XNL+wyucR8Qb09itulC +VuWyjJoPyPUuL5ofaquAhHny9Owpwnwfatp/9Fobcklb8FcFQfonv/Laf8aOmv91 +g+C1HpSFRK6fUY97BokMIiovyVXRRSNki4wUoOacHwwU1WBi73qDNkJHaI9l9IpW +CLgl5IAv1z52W/RXhGFo14D8Q+niQL6QlBYXSgBu+3B4PDrbr0ZTk86nL6N9GwIM +yhuKAyZUbNrCZyPO1klkpxC06/7wQX85uq5a+myb+GANXT3bQ7wrhF3lcz3HSexH +v36KG5s6TJObJde+f329eBM5u1skUOXqCREKgM/lZQNC4BxXtIGKlwJ5LXCsL42F +bfo/5aUY/iKlfr940WnOHeh7uxYCqGjTDmv5uRn4sE/r8ZpZg4KLpKVU3TaXeOki +TcoCKaBtDxq/6FqxWGN98BEVTd/2grQRU0kHg3a1UvOvW2ExUvS4Kvo2Izt4QINj +pDFRCrpKwKH7kTeFt2RmmfQHiqDbAH78O6nnr49rGg9XcN+vKxtXRJJnvYwbHIAz +5MT//uP6XYImianWVOyUpM1gCaUVdRqw9zKMvuttddLYxjSNCUOu0+hlUrQ00mvt +QJU2PNqmFq+po7p5ZLk/4wcyJz/MxZ664mJLaecX2WknG6UZJ8vBGupxOTVGarn0 +U2RFe4OGIOx7aFFGDoi2JgOseUgTdMhUXiWIIZCy3tq992BaGG8mpMXoMWkdUYz9 +kC9dXONb8yYNwyL3WKEKfgWAJyuOWC428mvUYX9VpHSUHl7maEWNkZASm6coyrXN +MbA5bioA+BdTTtPn/DzlvkfagxrWa1EAQhSN7QY9U/AaCkkIQxOJqMqD6FDjTzAD +mAH5vE0dNlGZLWw5zMgI6nml71UrKwAUiL6oMhCH6OG/jSp9sQL9k9DhSYDPyGKm +LKtOZ3eSQE/ltd8aA5iEXyfG3QSTOwmPFuMrI4LKvITlFA== +-----END PGP MESSAGE----- \ No newline at end of file diff --git a/crypto/test/data/openpgp/big-aead-msg.asc b/crypto/test/data/openpgp/big-skesk-aead-msg.asc similarity index 100% rename from crypto/test/data/openpgp/big-aead-msg.asc rename to crypto/test/data/openpgp/big-skesk-aead-msg.asc diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 09b4dbc5b..fab7c485e 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -144,6 +144,26 @@ public class PgpCryptoRefreshTest "Zjd4SG7Tv4RJHeycolKmqSHDoK5XlOsA7vlw50nKuRjDyRfsPOFDfHz8hR/z7D1i" + "HST68tjRCRmwqeqVgusCmBlXrXzYTkPXGtmZl2+EYazSACQFVg=="); + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-x25519-aead-ocb-encr + // encrypts the cleartext string "Hello, world!" for the sample certificate v6Certificate + // V6 PKESK + V2 SEIPD X25519 AES-128 OCB + private readonly byte[] v6pkesk_v2seipd_aes128_ocb = Base64.Decode( + "wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO" + + "WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS" + + "aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l" + + "yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo" + + "bhF30A+IitsxxA=="); + + // from the "OpenPGP interoperability test suite" + // https://tests.sequoia-pgp.org/#Encrypt-Decrypt_roundtrip_with_minimal_key_from_RFC9760 + // encrypts the cleartext string "Hello World :)" for the sample certificate v6Certificate + // V3 PKESK + V1 SEIPD X25519 AES-256 + private readonly byte[] v3pkesk_v1seipd_aes256 = Base64.Decode( + "wVQDEsg/HnBvYwgZEFZQspuRTLEGqQ4oEX+0ap/cDogTvDbh+Fu5K6O7ZCkpCZu4" + + "g6JfGwkmmqn6Ekff2LPS+jcsgz4S3y+90y7zg+bw6jgy81vJYZLSPwHPmq3ld0oV" + + "codBvOUSOAvARPpDCHvAOyMT+ZmYEbbQK/ahc3P6HGDArsfcAETvsIHBE8U45o4g" + + "poZLYyxi0A=="); + private readonly char[] emptyPassphrase = Array.Empty(); [Test] @@ -947,7 +967,7 @@ public void Version6SkeskVersion2SeipdTest() Arrays.Fill(plaintext, 0); Stream message = PgpUtilities.GetDecoderStream( - SimpleTest.GetTestDataAsStream("openpgp.big-aead-msg.asc")); + SimpleTest.GetTestDataAsStream("openpgp.big-skesk-aead-msg.asc")); PgpObjectFactory factory = new PgpObjectFactory(message); PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; @@ -1018,7 +1038,106 @@ public void SkeskWithArgon2Test() var stream = encData0.GetDataStreamRaw(wrongpassword); }); } + } + + [Test] + public void PkeskTest() + { + PgpSecretKeyRing secretKeyRing = new PgpSecretKeyRing(v6UnlockedSecretKey); + PgpSecretKey[] secretKeys = secretKeyRing.GetSecretKeys().ToArray(); + PgpSecretKey encryptionSubkey = secretKeys[1]; + PgpPrivateKey privKey = encryptionSubkey.ExtractPrivateKey(emptyPassphrase); + + // V6 PKESK + V2 SEIPD X25519 AES-128 OCB + { + byte[] plaintext = Encoding.UTF8.GetBytes("Hello, world!"); + PgpObjectFactory factory = new PgpObjectFactory(v6pkesk_v2seipd_aes128_ocb); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encData is null); + + var encData0 = encData[0] as PgpPublicKeyEncryptedData; + FailIf("invalid PgpPublicKeyEncryptedData", encData0 is null); + + IsEquals(encryptionSubkey.KeyId, encData0.KeyId); + IsTrue(Arrays.AreEqual(encryptionSubkey.GetFingerprint(), encData0.GetKeyFingerprint())); + IsEquals(SymmetricKeyAlgorithmTag.Aes128, encData0.GetSymmetricAlgorithm(privKey)); + + using (var stream = encData0.GetDataStream(privKey)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (var ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + var decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + } + + /* + * V6 PKESK + V2 SEIPD AEAD encrypted message that spans over 4 chunks + * (chunk size 512 octets) + * 2000 octets of /dev/zero encrypted with sample V6 certificate from + * crypto-refresh Appendix A.3 and AES-256 in OCB mode. + * Generated with gosop 2.0.0-alpha + * Session key CFB73D46CF7C13B7535227BEDB5B2D8B4023C5B58289D19CF2C33B0DB388B0B6 + */ + { + var plaintext = new byte[2000]; + Arrays.Fill(plaintext, 0); + + Stream message = PgpUtilities.GetDecoderStream( + SimpleTest.GetTestDataAsStream("openpgp.big-pkesk-aead-msg.asc")); + + PgpObjectFactory factory = new PgpObjectFactory(message); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encData is null); + + var encData0 = encData[0] as PgpPublicKeyEncryptedData; + FailIf("invalid PgpPublicKeyEncryptedData", encData0 is null); + + IsEquals(encryptionSubkey.KeyId, encData0.KeyId); + IsEquals(SymmetricKeyAlgorithmTag.Aes256, encData0.GetSymmetricAlgorithm(privKey)); + + using (var stream = encData0.GetDataStream(privKey)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (var ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + var decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + } + // V3 PKESK + V1 SEIPD X25519 AES-256 + { + byte[] plaintext = Encoding.UTF8.GetBytes("Hello World :)"); + PgpObjectFactory factory = new PgpObjectFactory(v3pkesk_v1seipd_aes256); + PgpEncryptedDataList encData = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encData is null); + + var encData0 = encData[0] as PgpPublicKeyEncryptedData; + FailIf("invalid PgpPublicKeyEncryptedData", encData0 is null); + + IsEquals(encryptionSubkey.KeyId, encData0.KeyId); + IsEquals(SymmetricKeyAlgorithmTag.Aes256, encData0.GetSymmetricAlgorithm(privKey)); + + using (var stream = encData0.GetDataStream(privKey)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (var ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + var decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + } } public override string Name => "PgpCryptoRefreshTest"; @@ -1039,6 +1158,7 @@ public override void PerformTest() Version6GenerateAndVerifyInlineSignatureTest(); Version6SkeskVersion2SeipdTest(); SkeskWithArgon2Test(); + PkeskTest(); } } } \ No newline at end of file diff --git a/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs b/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs index 351304752..a9122ba29 100644 --- a/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs +++ b/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.Encoders; using Org.BouncyCastle.Utilities.Test; using System; @@ -114,6 +115,17 @@ private static PgpPublicKeyRingBundle CreateBundle(params PgpPublicKeyRing[] key return new PgpPublicKeyRingBundle(ms.ToArray()); } } + private static PgpSecretKeyRingBundle CreateBundle(params PgpSecretKeyRing[] keyrings) + { + using (MemoryStream ms = new MemoryStream()) + { + foreach (var keyring in keyrings) + { + keyring.Encode(ms); + } + return new PgpSecretKeyRingBundle(ms.ToArray()); + } + } private void VerifyMultipleInlineSignaturesTest(byte[] message, PgpPublicKeyRingBundle bundle, bool shouldFail = false) { @@ -149,6 +161,70 @@ private void VerifyMultipleInlineSignaturesTest(byte[] message, PgpPublicKeyRing } } + [Test] + public void MultiplePkeskTest() + { + // Encrypt-Decrypt roundtrip with multiple keys: the plaintext + // "Hello World :)" is encrypted with the X25519 sample key from + // Appendix A.3 of crypto-refresh and the 'Alice' ECDH key from + // "OpenPGP Example Keys and Certificates" + byte[] message = Base64.Decode( + "wVQDEsg/HnBvYwgZaeKxsIieN+FvNLxmgMfRKJZGKt8vAa5BYX2k0QAetCMpCQbE" + + "mvXtq2XatB3H8NG7zlY2dyYKcHAK0xvgAo8YbinpCZ+xOciOkmDBXgNHZva51fIe" + + "thIBB0CRPS2kBUVTTtVLGjBKVCmc+KoPTzUXqVpPJdgiPmNvGTBME7unL3IP2CdO" + + "hL+uO3LVBJGfRy3JJDH1SIQhQ7oS47AFIOjpG0R0CBtf8M6dzwDSPwG1BrsfRn86" + + "mFm666ZINIHL1IDH1HQVF5OYxcRRVFjTJhms03+nu6N8I6Vy2G5yekVb1Vh2tM39" + + "/aGWVXTHJw=="); + + PgpSecretKeyRingBundle bundle = CreateBundle( + new PgpSecretKeyRing(aliceSecretkey), + new PgpSecretKeyRing(v6UnlockedSecretKey)); + + byte[] plaintext = Encoding.UTF8.GetBytes("Hello World :)"); + PgpObjectFactory factory = new PgpObjectFactory(message); + PgpEncryptedDataList encDataList = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encDataList is null); + + IsEquals(2, encDataList.Count); + + // decrypt with crypto-refresh sample X25519 key + var encData = encDataList[0] as PgpPublicKeyEncryptedData; + FailIf("invalid PgpPublicKeyEncryptedData", encData is null); + PgpSecretKey secKey = bundle.GetSecretKey(encData.KeyId); + PgpPrivateKey privKey = secKey.ExtractPrivateKey(emptyPassphrase); + using (var stream = encData.GetDataStream(privKey)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (var ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + var decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + + // decrypt with 'Alice' ECDH key + factory = new PgpObjectFactory(message); + encDataList = factory.NextPgpObject() as PgpEncryptedDataList; + encData = encDataList[1] as PgpPublicKeyEncryptedData; + FailIf("invalid PgpPublicKeyEncryptedData", encData is null); + secKey = bundle.GetSecretKey(encData.KeyId); + privKey = secKey.ExtractPrivateKey(emptyPassphrase); + using (var stream = encData.GetDataStream(privKey)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (var ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + var decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + + } + [Test] public void MultipleInlineSignatureTest() { @@ -435,6 +511,8 @@ public void Version5InlineSignatureTest() public override void PerformTest() { + MultiplePkeskTest(); + MultipleInlineSignatureTest(); GenerateAndVerifyMultipleInlineSignatureTest(); From b063307daf1bb2de4227640886e0ec7ac7470746 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 18 Apr 2024 18:26:41 +0200 Subject: [PATCH 24/37] V4 SKESK + V1 SEIPD Encryption with Argon2 s2k --- crypto/src/bcpg/S2k.cs | 125 ++++++++++++++++++ .../src/openpgp/PgpEncryptedDataGenerator.cs | 40 ++++++ .../src/openpgp/test/PgpCryptoRefreshTest.cs | 53 ++++++++ 3 files changed, 218 insertions(+) diff --git a/crypto/src/bcpg/S2k.cs b/crypto/src/bcpg/S2k.cs index 3dafe8759..05adc530b 100644 --- a/crypto/src/bcpg/S2k.cs +++ b/crypto/src/bcpg/S2k.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.IO; @@ -123,6 +125,12 @@ public S2k(byte[] salt, int passes, int parallelism, int memorySizeExponent) this.memorySizeExponent = memorySizeExponent; } + /// Constructs a specifier for an S2K method using Argon2 + public S2k(Argon2Parameters argon2Params) + :this(argon2Params.Salt, argon2Params.Passes, argon2Params.Parallelism, argon2Params.MemSizeExp) + { + } + public virtual int Type { get { return type; } @@ -214,5 +222,122 @@ public override void Encode( throw new InvalidOperationException($"Unknown S2K type {type}"); } } + + /// + /// Parameters for Argon2 S2K + /// Sect. 3.7.1.4 of crypto-refreshsee> + /// + public class Argon2Parameters + { + private readonly byte[] salt; + private readonly int passes; + private readonly int parallelism; + private readonly int memSizeExp; + + internal byte[] Salt => salt; + internal int Passes => passes; + internal int Parallelism => parallelism; + internal int MemSizeExp => memSizeExp; + + /// + /// Uniformly safe and recommended parameters not tailored to any hardware. + /// Uses Argon2id, 1 pass, 4 parallelism, 2 GiB RAM. + /// RFC 9106: §4. Parameter Choice + /// + public Argon2Parameters() + :this (CryptoServicesRegistrar.GetSecureRandom()) + { + } + + + /// + /// Uniformly safe and recommended parameters not tailored to any hardware. + /// Uses Argon2id, 1 pass, 4 parallelism, 2 GiB RAM. + /// RFC 9106: §4. Parameter Choice + /// + /// + public Argon2Parameters(SecureRandom secureRandom) + : this(1, 4, 21, secureRandom) + { + } + + /// + /// Create customized Argon2 S2K parameters. + /// + /// number of iterations, must be greater than 0 + /// number of lanes, must be greater 0 + /// exponent for memory consumption, must be between 3+ceil(log_2(p)) and 31 + /// A secure random generator + /// + public Argon2Parameters(int passes, int parallelism, int memSizeExp, SecureRandom secureRandom) + :this(GenerateSalt(secureRandom), passes, parallelism, memSizeExp) + { + } + + /// + /// Create customized Argon2 S2K parameters. + /// + /// 16 bytes of random salt + /// number of iterations, must be greater than 0 + /// number of lanes, must be greater 0 + /// exponent for memory consumption, must be between 3+ceil(log_2(p)) and 31 + /// + public Argon2Parameters(byte[] salt, int passes, int parallelism, int memSizeExp) + { + if (salt.Length != 16) + { + throw new ArgumentException("Argon2 uses 16 bytes of salt"); + } + this.salt = salt; + + if (passes < 1) + { + throw new ArgumentException("Number of passes MUST be positive, non-zero"); + } + this.passes = passes; + + if (parallelism < 1) + { + throw new ArgumentException("Parallelism MUST be positive, non-zero."); + } + this.parallelism = parallelism; + + // log_2(p) = log_e(p) / log_e(2) + double log2_p = System.Math.Log(parallelism) / System.Math.Log(2); + // see https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-argon2 + if (memSizeExp < (3 + System.Math.Ceiling(log2_p)) || memSizeExp > 31) + { + throw new ArgumentException("Memory size exponent MUST be between 3+ceil(log_2(parallelism)) and 31"); + } + this.memSizeExp = memSizeExp; + } + + /// + /// Uniformly safe and recommended parameters not tailored to any hardware. + /// Uses Argon2id, 1 pass, 4 parallelism, 2 GiB RAM. + /// RFC 9106: §4. Parameter Choice + /// + public static Argon2Parameters UniversallyRecommendedParameters() + { + return new Argon2Parameters(1, 4, 21, CryptoServicesRegistrar.GetSecureRandom()); + } + + /// + /// Recommended parameters for memory constrained environments(64MiB RAM). + /// Uses Argon2id with 3 passes, 4 lanes and 64 MiB RAM. + /// RFC 9106: §4. Parameter Choice + /// + public static Argon2Parameters MemoryConstrainedParameters() + { + return new Argon2Parameters(3, 4, 16, CryptoServicesRegistrar.GetSecureRandom()); + } + + private static byte[] GenerateSalt(SecureRandom secureRandom) + { + byte[] salt = new byte[16]; + secureRandom.NextBytes(salt); + return salt; + } + } } } diff --git a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs index 13648d1e8..a423009bf 100644 --- a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs +++ b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs @@ -402,6 +402,46 @@ internal void DoAddMethod(byte[] rawPassPhrase, bool clearPassPhrase, HashAlgori methods.Add(new PbeMethod(defAlgorithm, s2k, PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase))); } + /// Add a PBE encryption method to the encrypted object. + /// + /// Conversion of the passphrase characters to bytes is performed using Convert.ToByte(), which is + /// the historical behaviour of the library (1.7 and earlier). + /// + public void AddMethod(char[] passPhrase, S2k.Argon2Parameters argon2Parameters) + { + DoAddMethod(PgpUtilities.EncodePassPhrase(passPhrase, false), true, argon2Parameters); + } + + /// Add a PBE encryption method to the encrypted object. + /// + /// The passphrase is encoded to bytes using UTF8 (Encoding.UTF8.GetBytes). + /// + public void AddMethodUtf8(char[] passPhrase, S2k.Argon2Parameters argon2Parameters) + { + DoAddMethod(PgpUtilities.EncodePassPhrase(passPhrase, true), true, argon2Parameters); + } + + /// Add a PBE encryption method to the encrypted object. + /// + /// Allows the caller to handle the encoding of the passphrase to bytes. + /// + public void AddMethodRaw(byte[] rawPassPhrase, S2k.Argon2Parameters argon2Parameters) + { + DoAddMethod(rawPassPhrase, false, argon2Parameters); + } + + internal void DoAddMethod(byte[] rawPassPhrase, bool clearPassPhrase, S2k.Argon2Parameters argon2Parameters) + { + S2k s2k = new S2k(argon2Parameters); + + methods.Add( + new PbeMethod( + defAlgorithm, + s2k, + PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase) + )); + } + /// Add a public key encrypted session key to the encrypted object. public void AddMethod(PgpPublicKey key) { diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index fab7c485e..c7eb3aa63 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -1038,6 +1038,59 @@ public void SkeskWithArgon2Test() var stream = encData0.GetDataStreamRaw(wrongpassword); }); } + + // encrypt-decrypt roundtrip - V4 SKESK + V1 SEIPD + { + // encrypt + byte[] enc; + using (MemoryStream ms = new MemoryStream()) + { + byte[] buffer = new byte[1024]; + PgpEncryptedDataGenerator endDataGen = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Aes256, true, new SecureRandom()); + endDataGen.AddMethodRaw(password, S2k.Argon2Parameters.MemoryConstrainedParameters()); + + using (Stream cOut = endDataGen.Open(ms, buffer)) + { + using (BcpgOutputStream bcOut = new BcpgOutputStream(cOut, newFormatOnly: true)) + { + PgpLiteralDataGenerator literalDataGen = new PgpLiteralDataGenerator(); + DateTime modificationTime = DateTime.UtcNow; + + using (Stream lOut = literalDataGen.Open( + new UncloseableStream(bcOut), + PgpLiteralData.Utf8, + PgpLiteralData.Console, + plaintext.Length, + modificationTime)) + { + lOut.Write(plaintext, 0, plaintext.Length); + } + } + } + + enc = ms.ToArray(); + } + + // decrypt + PgpObjectFactory factory = new PgpObjectFactory(enc); + PgpEncryptedDataList encDataList = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encDataList is null); + + PgpPbeEncryptedData encData = encDataList[0] as PgpPbeEncryptedData; + FailIf("invalid PgpPbeEncryptedData", encData is null); + + using (Stream stream = encData.GetDataStreamRaw(password)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (MemoryStream ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + byte[] decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + } } [Test] From 68d3821c2446e6b36fc23c638a835cd566b76581 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Sat, 20 Apr 2024 18:43:59 +0200 Subject: [PATCH 25/37] V3 PKESK + V1 SEIPD Encryption with X25519 and X448 --- .../src/openpgp/PgpEncryptedDataGenerator.cs | 157 ++++++++++++++---- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 131 ++++++++------- 2 files changed, 203 insertions(+), 85 deletions(-) diff --git a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs index a423009bf..afc785300 100644 --- a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs +++ b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; - +using System.Text; using Org.BouncyCastle.Asn1.Cryptlib; using Org.BouncyCastle.Asn1.EdEC; using Org.BouncyCastle.Crypto; @@ -46,16 +46,19 @@ private class PbeMethod : EncMethod { private readonly S2k s2k; + private readonly int skeskVersion; internal PbeMethod( SymmetricKeyAlgorithmTag encAlgorithm, S2k s2k, - KeyParameter key) + KeyParameter key, + int skeskVersion) : base(PacketTag.SymmetricKeyEncryptedSessionKey) { this.encAlgorithm = encAlgorithm; this.s2k = s2k; this.key = key; + this.skeskVersion = skeskVersion; } public KeyParameter GetKey() @@ -91,12 +94,14 @@ private class PubMethod internal PgpPublicKey pubKey; internal bool sessionKeyObfuscation; internal byte[][] data; + private readonly int pkeskVersion; - internal PubMethod(PgpPublicKey pubKey, bool sessionKeyObfuscation) + internal PubMethod(PgpPublicKey pubKey, bool sessionKeyObfuscation, int pkeskVersion) : base(PacketTag.PublicKeyEncryptedSession) { this.pubKey = pubKey; this.sessionKeyObfuscation = sessionKeyObfuscation; + this.pkeskVersion = pkeskVersion; } public override void AddSessionInfo( @@ -112,6 +117,86 @@ private byte[] EncryptSessionInfo(byte[] sessionInfo, SecureRandom random) { var cryptoPublicKey = pubKey.GetKey(); + if (pubKey.Algorithm == PublicKeyAlgorithmTag.X25519 || pubKey.Algorithm == PublicKeyAlgorithmTag.X448) + { + IAsymmetricCipherKeyPairGenerator kpGen; + IRawAgreement agreement; + AsymmetricCipherKeyPair ephemeral; + byte[] ephPubEncoding; + IDigest digestForHkdf; + byte[] hkdfInfo; + SymmetricKeyAlgorithmTag wrappingAlgo; + + if (pubKey.Algorithm == PublicKeyAlgorithmTag.X25519) + { + agreement = new X25519Agreement(); + kpGen = new X25519KeyPairGenerator(); + kpGen.Init(new X25519KeyGenerationParameters(random)); + digestForHkdf = PgpUtilities.CreateDigest(HashAlgorithmTag.Sha256); + hkdfInfo = Encoding.ASCII.GetBytes("OpenPGP X25519"); + wrappingAlgo = SymmetricKeyAlgorithmTag.Aes128; + ephemeral = kpGen.GenerateKeyPair(); + ephPubEncoding = new byte[X25519PublicKeyParameters.KeySize]; + ((X25519PublicKeyParameters)ephemeral.Public).Encode(ephPubEncoding, 0); + } + else + { + // X448 + agreement = new X448Agreement(); + kpGen = new X448KeyPairGenerator(); + kpGen.Init(new X448KeyGenerationParameters(random)); + digestForHkdf = PgpUtilities.CreateDigest(HashAlgorithmTag.Sha512); + hkdfInfo = Encoding.ASCII.GetBytes("OpenPGP X448"); + wrappingAlgo = SymmetricKeyAlgorithmTag.Aes256; + ephemeral = kpGen.GenerateKeyPair(); + ephPubEncoding = new byte[X448PublicKeyParameters.KeySize]; + ((X448PublicKeyParameters)ephemeral.Public).Encode(ephPubEncoding, 0); + } + + agreement.Init(ephemeral.Private); + byte[] sharedSecret = new byte[agreement.AgreementSize]; + agreement.CalculateAgreement(cryptoPublicKey, sharedSecret, 0); + + byte[] pubKeyMaterial = ((OctetArrayBcpgKey)pubKey.PublicKeyPacket.Key).GetKey(); + byte[] ikm = Arrays.ConcatenateAll(ephPubEncoding, pubKeyMaterial, sharedSecret); + byte[] hkdfSalt = Array.Empty(); + var hkdfParams = new HkdfParameters(ikm, hkdfSalt, hkdfInfo); + var hkdfGen = new HkdfBytesGenerator(digestForHkdf); + hkdfGen.Init(hkdfParams); + var hkdfOutput = new byte[PgpUtilities.GetKeySizeInOctets(wrappingAlgo)]; + hkdfGen.GenerateBytes(hkdfOutput, 0, hkdfOutput.Length); + + KeyParameter kek = ParameterUtilities.CreateKeyParameter("AES", hkdfOutput); + var wrapper = PgpUtilities.CreateWrapper(wrappingAlgo); + wrapper.Init(true, kek); + + int offset = 0; + int length = sessionInfo.Length - 2; // no checksum for X25519 and X448 keys + // for X25519 and X448 keys the SymmetricKeyAlgorithmTag, when present (V3 PKESK) + // is not encrypted, is prepended to the ESK in plaintext + if (pkeskVersion == PublicKeyEncSessionPacket.Version3) + { + offset = 1; + length--; + } + var keyBytes = wrapper.Wrap(sessionInfo, offset, length); + + byte[] esk; + using (var ms = new MemoryStream()) + { + ms.Write(ephPubEncoding, 0, ephPubEncoding.Length); + if (pkeskVersion == PublicKeyEncSessionPacket.Version3) + { + // Unencrypted SymmetricKeyAlgorithmTag (V3 PKESK only) + ms.WriteByte(sessionInfo[0]); + } + ms.Write(keyBytes, 0, keyBytes.Length); + esk = ms.ToArray(); + } + + return esk; + } + if (pubKey.Algorithm != PublicKeyAlgorithmTag.ECDH) { IBufferedCipher c; @@ -275,6 +360,16 @@ private byte[][] ProcessSessionInfo(byte[] encryptedSessionInfo) case PublicKeyAlgorithmTag.ECDH: data = new byte[1][]{ encryptedSessionInfo }; break; + case PublicKeyAlgorithmTag.X25519: + case PublicKeyAlgorithmTag.X448: + int ephemeralKeyLen = pubKey.Algorithm == PublicKeyAlgorithmTag.X25519 ? 32 : 56; + byte[] ephemeralKey = new byte[ephemeralKeyLen]; + byte[] encryptedSessionKey = new byte[encryptedSessionInfo.Length - ephemeralKeyLen]; + Array.Copy(encryptedSessionInfo, 0, ephemeralKey, 0, ephemeralKeyLen); + Array.Copy(encryptedSessionInfo, ephemeralKeyLen, encryptedSessionKey, 0, encryptedSessionKey.Length); + + data = new byte[][] { ephemeralKey, encryptedSessionKey }; + break; default: throw new PgpException("unknown asymmetric algorithm: " + pubKey.Algorithm); } @@ -306,20 +401,21 @@ public override void Encode(BcpgOutputStream pOut) private readonly SymmetricKeyAlgorithmTag defAlgorithm; private readonly SecureRandom rand; - public PgpEncryptedDataGenerator( + private readonly int skeskVersion; + private readonly int pkeskVersion; + private readonly int seipdVersion; + + public PgpEncryptedDataGenerator( SymmetricKeyAlgorithmTag encAlgorithm) + :this(encAlgorithm, CryptoServicesRegistrar.GetSecureRandom(), false, false) { - this.defAlgorithm = encAlgorithm; - this.rand = CryptoServicesRegistrar.GetSecureRandom(); } public PgpEncryptedDataGenerator( SymmetricKeyAlgorithmTag encAlgorithm, bool withIntegrityPacket) - { - this.defAlgorithm = encAlgorithm; - this.withIntegrityPacket = withIntegrityPacket; - this.rand = CryptoServicesRegistrar.GetSecureRandom(); + : this(encAlgorithm, CryptoServicesRegistrar.GetSecureRandom(), false, withIntegrityPacket) + { } /// Existing SecureRandom constructor. @@ -328,12 +424,8 @@ public PgpEncryptedDataGenerator( public PgpEncryptedDataGenerator( SymmetricKeyAlgorithmTag encAlgorithm, SecureRandom random) + : this(encAlgorithm, random, false, false) { - if (random == null) - throw new ArgumentNullException(nameof(random)); - - this.defAlgorithm = encAlgorithm; - this.rand = random; } /// Creates a cipher stream which will have an integrity packet associated with it. @@ -341,13 +433,8 @@ public PgpEncryptedDataGenerator( SymmetricKeyAlgorithmTag encAlgorithm, bool withIntegrityPacket, SecureRandom random) + :this(encAlgorithm, random, false, withIntegrityPacket) { - if (random == null) - throw new ArgumentNullException(nameof(random)); - - this.defAlgorithm = encAlgorithm; - this.rand = random; - this.withIntegrityPacket = withIntegrityPacket; } /// Base constructor. @@ -358,13 +445,24 @@ public PgpEncryptedDataGenerator( SymmetricKeyAlgorithmTag encAlgorithm, SecureRandom random, bool oldFormat) + :this (encAlgorithm, random, oldFormat, false) { - if (random == null) - throw new ArgumentNullException(nameof(random)); + } + private PgpEncryptedDataGenerator( + SymmetricKeyAlgorithmTag encAlgorithm, + SecureRandom random, + bool oldFormat, + bool withIntegrityPacket) + { + this.rand = random ?? throw new ArgumentNullException(nameof(random)); this.defAlgorithm = encAlgorithm; - this.rand = random; this.oldFormat = oldFormat; + this.withIntegrityPacket = withIntegrityPacket; + + skeskVersion = SymmetricKeyEncSessionPacket.Version4; + pkeskVersion = PublicKeyEncSessionPacket.Version3; + seipdVersion = SymmetricEncIntegrityPacket.Version1; } /// Add a PBE encryption method to the encrypted object. @@ -399,7 +497,7 @@ internal void DoAddMethod(byte[] rawPassPhrase, bool clearPassPhrase, HashAlgori { S2k s2k = PgpUtilities.GenerateS2k(s2kDigest, 0x60, rand); - methods.Add(new PbeMethod(defAlgorithm, s2k, PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase))); + methods.Add(new PbeMethod(defAlgorithm, s2k, PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase), skeskVersion)); } /// Add a PBE encryption method to the encrypted object. @@ -438,7 +536,8 @@ internal void DoAddMethod(byte[] rawPassPhrase, bool clearPassPhrase, S2k.Argon2 new PbeMethod( defAlgorithm, s2k, - PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase) + PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase), + skeskVersion )); } @@ -455,7 +554,7 @@ public void AddMethod(PgpPublicKey key, bool sessionKeyObfuscation) throw new ArgumentException("passed in key not an encryption key!"); } - methods.Add(new PubMethod(key, sessionKeyObfuscation)); + methods.Add(new PubMethod(key, sessionKeyObfuscation, pkeskVersion)); } private void AddCheckSum( @@ -478,7 +577,7 @@ private void AddCheckSum( private byte[] CreateSessionInfo(SymmetricKeyAlgorithmTag algorithm, KeyParameter key) { int keyLength = key.KeyLength; - byte[] sessionInfo = new byte[keyLength + 3]; + byte[] sessionInfo = new byte[keyLength + 3]; sessionInfo[0] = (byte)algorithm; key.CopyTo(sessionInfo, 1, keyLength); AddCheckSum(sessionInfo); @@ -516,7 +615,7 @@ private Stream Open( { if (methods[0] is PbeMethod pbeMethod) { - key = pbeMethod.GetKey(); + key = pbeMethod.GetKey(); } else if (methods[0] is PubMethod pubMethod) { diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index c7eb3aa63..74ad744f1 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -507,13 +507,10 @@ public void Version6Ed25519KeyPairCreationTest() SignVerifyRoundtrip(pgpseckey, emptyPassphrase); // encrypt-decrypt test - AsymmetricCipherKeyPair alice = GetKeyPair(subKey); - - PgpSecretKeyRing bobSecring = new PgpSecretKeyRing(v6UnlockedSecretKey); - PgpSecretKey bobEncryptionKey = bobSecring.GetSecretKeys().ToArray()[1]; - AsymmetricCipherKeyPair bob = GetKeyPair(bobEncryptionKey); - - IsTrue("X25519 agreement failed", EncryptThenDecryptTest(alice, bob, subKey.PublicKey.Algorithm)); + EncryptDecryptRoundtrip( + Encoding.UTF8.GetBytes("Hello, World!"), + subKey.PublicKey, + subKey.ExtractPrivateKey(emptyPassphrase)); } [Test] @@ -593,12 +590,11 @@ public void Version6Ed448KeyPairCreationTest() // Sign-Verify roundtrip SignVerifyRoundtrip(pgpseckey, emptyPassphrase); - // Encrypt-Decrypt test - AsymmetricCipherKeyPair alice = GetKeyPair(subKey); - IAsymmetricCipherKeyPairGenerator kpGen = new X448KeyPairGenerator(); - kpGen.Init(new X448KeyGenerationParameters(rand)); - AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); - IsTrue("X448 agreement failed", EncryptThenDecryptTest(alice, bob, subKey.PublicKey.Algorithm)); + // Encrypt-Decrypt roundtrip + EncryptDecryptRoundtrip( + Encoding.UTF8.GetBytes("Hello, World!"), + subKey.PublicKey, + subKey.ExtractPrivateKey(emptyPassphrase)); } [Test] @@ -641,13 +637,11 @@ public void Version6LockedSecretKeyParsingTest() IsEquals(encryptionKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X25519); IsEquals(encryptionKey.PublicKey.KeyId, 0x12C83F1E706F6308); - // decrypt test - AsymmetricCipherKeyPair alice = GetKeyPair(encryptionKey, passphrase); - IAsymmetricCipherKeyPairGenerator kpGen = new X25519KeyPairGenerator(); - kpGen.Init(new X25519KeyGenerationParameters(new SecureRandom())); - AsymmetricCipherKeyPair bob = kpGen.GenerateKeyPair(); - IsTrue("X25519 agreement failed", EncryptThenDecryptTest(alice, bob, encryptionKey.PublicKey.Algorithm)); - + // encrypt-decrypt + EncryptDecryptRoundtrip( + Encoding.UTF8.GetBytes("Hello, World!"), + encryptionKey.PublicKey, + encryptionKey.ExtractPrivateKey(passphrase.ToCharArray())); // Encode-Decode roundtrip using (MemoryStream ms = new MemoryStream()) @@ -799,42 +793,6 @@ private void VerifyEncodedSignature(byte[] sigPacket, byte[] data, PgpPublicKey VerifySignature(signature, data, signer, shouldFail); } - private static AsymmetricCipherKeyPair GetKeyPair(PgpSecretKey secretKey, string password = "") - { - return new AsymmetricCipherKeyPair( - secretKey.PublicKey.GetKey(), - secretKey.ExtractPrivateKey(password.ToCharArray()).Key); - } - - private static bool EncryptThenDecryptTest(AsymmetricCipherKeyPair alice, AsymmetricCipherKeyPair bob, PublicKeyAlgorithmTag algo) - { - IRawAgreement agreeA, agreeB; - - switch (algo) - { - case PublicKeyAlgorithmTag.X25519: - agreeA = new X25519Agreement(); - agreeB = new X25519Agreement(); - break; - case PublicKeyAlgorithmTag.X448: - agreeA = new X448Agreement(); - agreeB = new X448Agreement(); - break; - default: - throw new ArgumentException($"Unsupported algo {algo}"); - } - - agreeA.Init(alice.Private); - byte[] secretA = new byte[agreeA.AgreementSize]; - agreeA.CalculateAgreement(bob.Public, secretA, 0); - - agreeB.Init(bob.Private); - byte[] secretB = new byte[agreeB.AgreementSize]; - agreeB.CalculateAgreement(alice.Public, secretB, 0); - - return Arrays.AreEqual(secretA, secretB); - } - [Test] public void Version6SkeskVersion2SeipdTest() { @@ -1093,6 +1051,58 @@ public void SkeskWithArgon2Test() } } + + private void EncryptDecryptRoundtrip(byte[] plaintext, PgpPublicKey pubKey, PgpPrivateKey privKey) + { + byte[] enc; + using (MemoryStream ms = new MemoryStream()) + { + byte[] buffer = new byte[1024]; + PgpEncryptedDataGenerator endDataGen = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Aes256, true, new SecureRandom()); + endDataGen.AddMethod(pubKey); + + using (Stream cOut = endDataGen.Open(ms, buffer)) + { + using (BcpgOutputStream bcOut = new BcpgOutputStream(cOut, newFormatOnly: true)) + { + PgpLiteralDataGenerator literalDataGen = new PgpLiteralDataGenerator(); + DateTime modificationTime = DateTime.UtcNow; + + using (Stream lOut = literalDataGen.Open( + new UncloseableStream(bcOut), + PgpLiteralData.Utf8, + PgpLiteralData.Console, + plaintext.Length, + modificationTime)) + { + lOut.Write(plaintext, 0, plaintext.Length); + } + } + } + enc = ms.ToArray(); + } + + // decrypt + PgpObjectFactory factory = new PgpObjectFactory(enc); + PgpEncryptedDataList encDataList = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encDataList is null); + + PgpPublicKeyEncryptedData encData = encDataList[0] as PgpPublicKeyEncryptedData; + FailIf("invalid PgpPublicKeyEncryptedData", encData is null); + + using (Stream stream = encData.GetDataStream(privKey)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (MemoryStream ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + byte[] decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + } + [Test] public void PkeskTest() { @@ -1191,6 +1201,15 @@ public void PkeskTest() } } } + + // encrypt-decrypt roundtrip - V3 PKESK + V1 SEIPD X25519 + { + byte[] plaintext = Encoding.UTF8.GetBytes("Hello, world!"); + PgpPublicKeyRing publicKeyRing = new PgpPublicKeyRing(v6Certificate); + PgpPublicKey pubKey = publicKeyRing.GetPublicKeys().First(k => k.IsEncryptionKey); + + EncryptDecryptRoundtrip(plaintext, pubKey, privKey); + } } public override string Name => "PgpCryptoRefreshTest"; From 5e72362892f753a4850b7a670092a54c6bc5f6e0 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Wed, 24 Apr 2024 18:39:27 +0200 Subject: [PATCH 26/37] AEAD Encryption - V6 PKESK, V6 SKESK, V2 SEIPD --- crypto/src/bcpg/AeadEncDataPacket.cs | 25 +- crypto/src/bcpg/AeadInputStream.cs | 69 +-- crypto/src/bcpg/AeadOutputStream.cs | 228 +++++++++ crypto/src/bcpg/AeadUtils.cs | 61 +++ crypto/src/bcpg/PublicKeyEncSessionPacket.cs | 26 + .../src/bcpg/SymmetricEncIntegrityPacket.cs | 6 +- .../src/bcpg/SymmetricKeyEncSessionPacket.cs | 39 +- .../src/openpgp/PgpEncryptedDataGenerator.cs | 226 ++++++++- crypto/src/openpgp/PgpPbeEncryptedData.cs | 2 +- .../src/openpgp/PgpPublicKeyEncryptedData.cs | 2 +- crypto/src/openpgp/PgpUtilities.cs | 30 -- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 480 ++++++++++-------- 12 files changed, 898 insertions(+), 296 deletions(-) create mode 100644 crypto/src/bcpg/AeadOutputStream.cs diff --git a/crypto/src/bcpg/AeadEncDataPacket.cs b/crypto/src/bcpg/AeadEncDataPacket.cs index 61368904b..512a24e54 100644 --- a/crypto/src/bcpg/AeadEncDataPacket.cs +++ b/crypto/src/bcpg/AeadEncDataPacket.cs @@ -20,11 +20,13 @@ public class AeadEncDataPacket private readonly byte m_chunkSize; private readonly byte[] m_iv; + public const int Version1 = 1; + public AeadEncDataPacket(BcpgInputStream bcpgIn) : base(bcpgIn, PacketTag.ReservedAeadEncryptedData) { m_version = (byte)bcpgIn.ReadByte(); - if (m_version != 1) + if (m_version != Version1) throw new ArgumentException("wrong AEAD packet version: " + m_version); m_algorithm = (SymmetricKeyAlgorithmTag)bcpgIn.ReadByte(); @@ -39,7 +41,7 @@ public AeadEncDataPacket(SymmetricKeyAlgorithmTag algorithm, AeadAlgorithmTag ae byte[] iv) : base(null, PacketTag.ReservedAeadEncryptedData) { - m_version = 1; + m_version = Version1; m_algorithm = algorithm; m_aeadAlgorithm = aeadAlgorithm; m_chunkSize = (byte)chunkSize; @@ -73,5 +75,24 @@ public static int GetIVLength(AeadAlgorithmTag aeadAlgorithm) throw new ArgumentException("unknown mode: " + aeadAlgorithm); } } + + public byte[] GetAAData() + { + return CreateAAData(Version, Algorithm, AeadAlgorithm, ChunkSize); + } + + public static byte[] CreateAAData(int version, SymmetricKeyAlgorithmTag symAlgorithm, AeadAlgorithmTag aeadAlgorithm, int chunkSize) + { + byte[] aaData = new byte[] + { + 0xC0 | (byte)PacketTag.ReservedAeadEncryptedData, + (byte)version, + (byte)symAlgorithm, + (byte)aeadAlgorithm, + (byte)chunkSize + }; + + return aaData; + } } } diff --git a/crypto/src/bcpg/AeadInputStream.cs b/crypto/src/bcpg/AeadInputStream.cs index 39a591834..e88334ab2 100644 --- a/crypto/src/bcpg/AeadInputStream.cs +++ b/crypto/src/bcpg/AeadInputStream.cs @@ -26,12 +26,23 @@ internal class AeadInputStream private int dataOff; private long chunkIndex = 0; private long totalBytes = 0; + private readonly bool isV5StyleAead; private static int GetChunkLength(int chunkSize) { return 1 << (chunkSize + 6); } + /// + /// InputStream for decrypting AEAD encrypted data. + /// + /// underlying InputStream + /// encryption cipher + /// decryption key + /// initialization vector + /// AEAD algorithm + /// chunk size of the AEAD encryption + /// associated data public AeadInputStream( Stream inputStream, BufferedAeadBlockCipher cipher, @@ -40,12 +51,37 @@ public AeadInputStream( AeadAlgorithmTag aeadAlgorithm, int chunkSize, byte[] aaData) + :this(false, inputStream, cipher, secretKey, iv, aeadAlgorithm, chunkSize, aaData) + { + } + + /// + /// InputStream for decrypting AEAD encrypted data. + /// + /// flavour of AEAD (OpenPGP v5 or v6) + /// underlying InputStream + /// encryption cipher + /// decryption key + /// initialization vector + /// AEAD algorithm + /// chunk size of the AEAD encryption + /// associated data + public AeadInputStream( + bool isV5StyleAead, + Stream inputStream, + BufferedAeadBlockCipher cipher, + KeyParameter secretKey, + byte[] iv, + AeadAlgorithmTag aeadAlgorithm, + int chunkSize, + byte[] aaData) { this.inputStream = inputStream; this.cipher = cipher; this.secretKey = secretKey; this.aaData = aaData; this.iv = iv; + this.isV5StyleAead = isV5StyleAead; chunkLength = GetChunkLength(chunkSize); tagLen = AeadUtils.GetAuthTagLength(aeadAlgorithm); @@ -87,33 +123,6 @@ public override int Read(byte[] buffer, int offset, int count) return supplyLen; } - private static byte[] GetNonce(byte[] iv, long chunkIndex) - { - byte[] nonce = Arrays.Clone(iv); - byte[] chunkid = Pack.UInt64_To_BE((ulong)chunkIndex); - Array.Copy(chunkid, 0, nonce, nonce.Length - 8, 8); - - return nonce; - } - - private static byte[] GetAdata(bool isV5StyleAead, byte[] aaData, long chunkIndex, long totalBytes) - { - byte[] adata; - if (isV5StyleAead) - { - adata = new byte[13]; - Array.Copy(aaData, 0, adata, 0, aaData.Length); - Array.Copy(Pack.UInt64_To_BE((ulong)chunkIndex), 0, adata, aaData.Length, 8); - } - else - { - adata = new byte[aaData.Length + 8]; - Array.Copy(aaData, 0, adata, 0, aaData.Length); - Array.Copy(Pack.UInt64_To_BE((ulong)totalBytes), 0, adata, aaData.Length, 8); - } - return adata; - } - private byte[] ReadBlock() { int dataLen = Streams.ReadFully(inputStream, buf, tagLen + tagLen, chunkLength); @@ -130,7 +139,7 @@ private byte[] ReadBlock() AeadParameters aeadParams = new AeadParameters( secretKey, 8 * tagLen, - GetNonce(iv, chunkIndex), + AeadUtils.CreateNonce(iv, chunkIndex), adata); cipher.Init(false, aeadParams); @@ -153,11 +162,11 @@ private byte[] ReadBlock() // last block try { - adata = GetAdata(false, aaData, chunkIndex, totalBytes); + adata = AeadUtils.CreateLastBlockAAData(isV5StyleAead, aaData, chunkIndex, totalBytes); AeadParameters aeadParams = new AeadParameters( secretKey, 8 * tagLen, - GetNonce(iv, chunkIndex), + AeadUtils.CreateNonce(iv, chunkIndex), adata); cipher.Init(false, aeadParams); diff --git a/crypto/src/bcpg/AeadOutputStream.cs b/crypto/src/bcpg/AeadOutputStream.cs new file mode 100644 index 000000000..8d4971866 --- /dev/null +++ b/crypto/src/bcpg/AeadOutputStream.cs @@ -0,0 +1,228 @@ +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Utilities.IO; +using System; +using System.IO; + +namespace Org.BouncyCastle.Bcpg +{ + internal class AeadOutputStream + : BaseOutputStream + { + // ported from bc-java + + private readonly bool isV5StyleAead; + private readonly Stream outputStream; + private readonly byte[] data; + private readonly BufferedAeadBlockCipher cipher; + private readonly KeyParameter secretKey; + private readonly byte[] aaData; + private readonly byte[] iv; + private readonly int chunkLength; + private readonly int tagLen; + + private int dataOff; + private long chunkIndex = 0; + private long totalBytes = 0; + + private static int GetChunkLength(int chunkSize) + { + return 1 << (chunkSize + 6); + } + + /// + /// OutputStream for AEAD encryption. + /// + /// underlying OutputStream + /// AEAD cipher + /// encryption key + /// initialization vector + /// encryption algorithm + /// AEAD algorithm + /// chunk size octet of the AEAD encryption + public AeadOutputStream( + Stream outputStream, + BufferedAeadBlockCipher cipher, + KeyParameter secretKey, + byte[] iv, + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, + int chunkSize) + :this(false, outputStream, cipher, secretKey, iv, encAlgorithm, aeadAlgorithm, chunkSize) + { + + } + + /// + /// OutputStream for AEAD encryption. + /// + /// flavour of AEAD (OpenPGP v5 or v6) + /// underlying OutputStream + /// AEAD cipher + /// encryption key + /// initialization vector + /// encryption algorithm + /// AEAD algorithm + /// chunk size octet of the AEAD encryption + public AeadOutputStream( + bool isV5StyleAead, + Stream outputStream, + BufferedAeadBlockCipher cipher, + KeyParameter secretKey, + byte[] iv, + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, + int chunkSize) + { + this.isV5StyleAead = isV5StyleAead; + this.outputStream = outputStream; + this.iv = iv; + this.chunkLength = GetChunkLength(chunkSize); + this.tagLen = AeadUtils.GetAuthTagLength(aeadAlgorithm); + this.data = new byte[chunkLength]; + this.cipher = cipher; + this.secretKey = secretKey; + + aaData = CreateAAD(isV5StyleAead, encAlgorithm, aeadAlgorithm, chunkSize); + } + + private static byte[] CreateAAD(bool isV5StyleAEAD, SymmetricKeyAlgorithmTag encAlgorithm, AeadAlgorithmTag aeadAlgorithm, int chunkSize) + { + if (isV5StyleAEAD) + { + return AeadEncDataPacket.CreateAAData(AeadEncDataPacket.Version1, encAlgorithm, aeadAlgorithm, chunkSize); + } + else + { + return SymmetricEncIntegrityPacket.CreateAAData(SymmetricEncIntegrityPacket.Version2, encAlgorithm, aeadAlgorithm, chunkSize); + } + } + + public override void WriteByte(byte b) + { + if (dataOff == data.Length) + { + WriteBlock(); + } + data[dataOff++] = (byte) b; + } + + public override void Write(byte[] b, int off, int len) + { + if (dataOff == data.Length) + { + WriteBlock(); + } + + if (len= data.Length) + { + Array.Copy(b, off, data, 0, data.Length); + dataOff = data.Length; + WriteBlock(); + len -= data.Length; + off += data.Length; + } + if (len > 0) + { + Array.Copy(b, off, data, 0, len); + dataOff = len; + } + } + } + + private void WriteBlock() + { + //bool v5StyleAEAD = isV5StyleAEAD; + + //byte[] adata = v5StyleAEAD ? new byte[13] : new byte[aaData.Length]; + byte[] adata = new byte[aaData.Length]; + Array.Copy(aaData, 0, adata, 0, aaData.Length); + + //if (v5StyleAEAD) + //{ + // xorChunkId(adata, chunkIndex); + //} + + try + { + AeadParameters aeadParams = new AeadParameters( + secretKey, + 8 * tagLen, + AeadUtils.CreateNonce(iv, chunkIndex), + adata); + + cipher.Init(true, aeadParams); + + int len = cipher.ProcessBytes(data, 0, dataOff, data, 0); + outputStream.Write(data, 0, len); + + len = cipher.DoFinal(data, 0); + outputStream.Write(data, 0, len); + } + catch (InvalidCipherTextException e) + { + throw new IOException("exception processing chunk " + chunkIndex + ": " + e.Message); + } + + totalBytes += dataOff; + chunkIndex++; + dataOff = 0; + } + + private bool disposed = false; + protected override void Dispose(bool disponing) + { + if (!disposed) + { + base.Dispose(disponing); + Finish(); + disposed = true; + } + } + + private void Finish() + { + if (dataOff > 0) + { + WriteBlock(); + } + + byte[] adata = AeadUtils.CreateLastBlockAAData(isV5StyleAead, aaData, chunkIndex, totalBytes); + try + { + AeadParameters aeadParams = new AeadParameters( + secretKey, + 8 * tagLen, + AeadUtils.CreateNonce(iv, chunkIndex), + adata); + + cipher.Init(true, aeadParams); + //if (isV5StyleAead) + //{ + // cipher.processAADBytes(Pack.longToBigEndian(totalBytes), 0, 8); + //} + cipher.DoFinal(data, 0); + outputStream.Write(data, 0, tagLen); // output final tag + } + catch (InvalidCipherTextException e) + { + throw new IOException("exception processing final tag: " + e.Message); + } + outputStream.Close(); + } + } +} diff --git a/crypto/src/bcpg/AeadUtils.cs b/crypto/src/bcpg/AeadUtils.cs index e828f735a..0f2adce49 100644 --- a/crypto/src/bcpg/AeadUtils.cs +++ b/crypto/src/bcpg/AeadUtils.cs @@ -1,7 +1,11 @@ using System; using Org.BouncyCastle.Bcpg.OpenPgp; using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Utilities; using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; namespace Org.BouncyCastle.Bcpg @@ -93,6 +97,63 @@ public static void SplitMessageKeyAndIv(byte[] messageKeyAndIv, SymmetricKeyAlgo Array.Copy(messageKeyAndIv, messageKey.Length, iv, 0, ivLen-8); } + /// + /// Derive a message key and IV from the given session key. + /// + /// session key + /// symmetric cipher algorithm tag + /// AEAD algorithm tag + /// salt + /// HKDF info + /// + /// + public static void DeriveAeadMessageKeyAndIv( + KeyParameter sessionKey, + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, + byte[] salt, + byte[] hkdfInfo, + out KeyParameter messageKey, + out byte[] iv) + { + var hkdfGen = new HkdfBytesGenerator(PgpUtilities.CreateDigest(HashAlgorithmTag.Sha256)); + var hkdfParams = new HkdfParameters(sessionKey.GetKey(), salt, hkdfInfo); + hkdfGen.Init(hkdfParams); + var hkdfOutput = new byte[PgpUtilities.GetKeySizeInOctets(encAlgorithm) + AeadUtils.GetIVLength(aeadAlgorithm) - 8]; + hkdfGen.GenerateBytes(hkdfOutput, 0, hkdfOutput.Length); + + AeadUtils.SplitMessageKeyAndIv(hkdfOutput, encAlgorithm, aeadAlgorithm, out var messageKeyBytes, out iv); + + messageKey = new KeyParameter(messageKeyBytes); + } + + public static byte[] CreateNonce(byte[] iv, long chunkIndex) + { + byte[] nonce = Arrays.Clone(iv); + byte[] chunkid = Pack.UInt64_To_BE((ulong)chunkIndex); + Array.Copy(chunkid, 0, nonce, nonce.Length - 8, 8); + + return nonce; + } + + public static byte[] CreateLastBlockAAData(bool isV5StyleAead, byte[] aaData, long chunkIndex, long totalBytes) + { + byte[] adata; + if (isV5StyleAead) + { + adata = new byte[13]; + Array.Copy(aaData, 0, adata, 0, aaData.Length); + Array.Copy(Pack.UInt64_To_BE((ulong)chunkIndex), 0, adata, aaData.Length, 8); + } + else + { + adata = new byte[aaData.Length + 8]; + Array.Copy(aaData, 0, adata, 0, aaData.Length); + Array.Copy(Pack.UInt64_To_BE((ulong)totalBytes), 0, adata, aaData.Length, 8); + } + return adata; + } + public static BufferedAeadBlockCipher CreateAeadCipher( SymmetricKeyAlgorithmTag encAlgorithm, AeadAlgorithmTag aeadAlgorithm) { diff --git a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs index dd7e1d7f0..2a23bc3f3 100644 --- a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs @@ -153,6 +153,32 @@ public PublicKeyEncSessionPacket( } } + /// + /// Create a new V6 PKESK packet. + /// + /// version of the key + /// fingerprint of the key + /// public key algorith + /// session data + public PublicKeyEncSessionPacket( + int keyVersion, + byte[] keyFingerprint, + PublicKeyAlgorithmTag algorithm, + byte[][] data) + : base(PacketTag.PublicKeyEncryptedSession) + { + this.version = Version6; + this.keyVersion = keyVersion; + this.keyFingerprint = Arrays.Clone(keyFingerprint); + this.algorithm = algorithm; + this.data = new byte[data.Length][]; + + for (int i = 0; i < data.Length; i++) + { + this.data[i] = Arrays.Clone(data[i]); + } + } + public int Version { get { return version; } diff --git a/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs b/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs index e30da6f1e..6692851be 100644 --- a/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs +++ b/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs @@ -70,13 +70,13 @@ public byte[] GetSalt() internal byte[] GetAAData() { - return CreateAAData(Tag, Version, cipherAlgorithm, aeadAlgorithm, chunkSize); + return CreateAAData(Version, cipherAlgorithm, aeadAlgorithm, chunkSize); } - internal static byte[] CreateAAData(PacketTag tag, int version, SymmetricKeyAlgorithmTag cipherAlgorithm, AeadAlgorithmTag aeadAlgorithm, int chunkSize) + internal static byte[] CreateAAData(int version, SymmetricKeyAlgorithmTag cipherAlgorithm, AeadAlgorithmTag aeadAlgorithm, int chunkSize) { return new byte[]{ - (byte)(0xC0 | (byte)tag), + 0xC0 | (byte)PacketTag.SymmetricEncryptedIntegrityProtected, (byte)version, (byte)cipherAlgorithm, (byte)aeadAlgorithm, diff --git a/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs b/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs index 215ff98d0..224ca8243 100644 --- a/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs @@ -95,6 +95,39 @@ public SymmetricKeyEncSessionPacket( this.secKeyData = secKeyData; } + + /// + /// Create a v6 SKESK packet. + /// + /// + /// + /// + /// + /// + /// + public SymmetricKeyEncSessionPacket( + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, + byte[] iv, + S2k s2k, + byte[] secKeyData) + : base(PacketTag.SymmetricKeyEncryptedSessionKey) + { + this.version = Version6; + this.encAlgorithm = encAlgorithm; + this.aeadAlgorithm = aeadAlgorithm; + this.s2k = s2k; + this.secKeyData = Arrays.Clone(secKeyData); + + int expectedIVLen = AeadUtils.GetIVLength(aeadAlgorithm); + if (expectedIVLen != iv.Length) + { + throw new ArgumentException($"Mismatched AEAD IV length. Expected {expectedIVLen}, got {iv.Length}"); + } + + this.iv = Arrays.Clone(iv); + } + /** * @return int */ @@ -134,14 +167,14 @@ public int Version internal byte[] GetAAData() { - return CreateAAData(Tag, Version, EncAlgorithm, AeadAlgorithm); + return CreateAAData(Version, EncAlgorithm, AeadAlgorithm); } - internal static byte[] CreateAAData(PacketTag tag, int version, SymmetricKeyAlgorithmTag encAlgorithm, AeadAlgorithmTag aeadAlgorithm) + internal static byte[] CreateAAData(int version, SymmetricKeyAlgorithmTag encAlgorithm, AeadAlgorithmTag aeadAlgorithm) { return new byte[] { - (byte)(0xC0 | (byte)tag), + 0xC0 | (byte)PacketTag.SymmetricKeyEncryptedSessionKey, (byte)version, (byte)encAlgorithm, (byte)aeadAlgorithm diff --git a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs index afc785300..f32be2fec 100644 --- a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs +++ b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs @@ -22,6 +22,7 @@ public class PgpEncryptedDataGenerator { private BcpgOutputStream pOut; private CipherStream cOut; + private AeadOutputStream aeadOut; private IBufferedCipher c; private readonly bool withIntegrityPacket; private readonly bool oldFormat; @@ -32,7 +33,9 @@ private abstract class EncMethod { protected byte[] sessionInfo; protected SymmetricKeyAlgorithmTag encAlgorithm; + protected AeadAlgorithmTag aeadAlgorithm; protected KeyParameter key; + protected byte[] aeadIv; protected EncMethod(PacketTag packetTag) : base(packetTag) @@ -50,12 +53,14 @@ private class PbeMethod internal PbeMethod( SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, S2k s2k, KeyParameter key, int skeskVersion) : base(PacketTag.SymmetricKeyEncryptedSessionKey) { this.encAlgorithm = encAlgorithm; + this.aeadAlgorithm = aeadAlgorithm; this.s2k = s2k; this.key = key; this.skeskVersion = skeskVersion; @@ -66,23 +71,71 @@ public KeyParameter GetKey() return key; } - public override void AddSessionInfo( - byte[] si, - SecureRandom random) + private byte[] EncryptSessionInfoForVersion4(byte[] si, SecureRandom random) { string cName = PgpUtilities.GetSymmetricCipherName(encAlgorithm); - IBufferedCipher c = CipherUtilities.GetCipher(cName + "/CFB/NoPadding"); + IBufferedCipher cipher = CipherUtilities.GetCipher($"{cName}/CFB/NoPadding"); - byte[] iv = new byte[c.GetBlockSize()]; - c.Init(true, new ParametersWithRandom(new ParametersWithIV(key, iv), random)); + byte[] iv = new byte[cipher.GetBlockSize()]; + cipher.Init(true, new ParametersWithRandom(new ParametersWithIV(key, iv), random)); + + return cipher.DoFinal(si, 0, si.Length - 2); + } + + private byte[] EncryptSessionInfoForVersion6(byte[] si, SecureRandom random) + { + byte[] aadata = SymmetricKeyEncSessionPacket.CreateAAData(skeskVersion, encAlgorithm, aeadAlgorithm); + + // key-encryption key derivation + var hkdfParams = new HkdfParameters(key.GetKey(), Array.Empty(), aadata); + var hkdfGen = new HkdfBytesGenerator(PgpUtilities.CreateDigest(HashAlgorithmTag.Sha256)); + hkdfGen.Init(hkdfParams); + var hkdfOutput = new byte[PgpUtilities.GetKeySizeInOctets(encAlgorithm)]; + hkdfGen.GenerateBytes(hkdfOutput, 0, hkdfOutput.Length); + + BufferedAeadBlockCipher cipher = AeadUtils.CreateAeadCipher(encAlgorithm, aeadAlgorithm); + + aeadIv = new byte[AeadUtils.GetIVLength(aeadAlgorithm)]; + random.NextBytes(aeadIv); + + var aeadParams = new AeadParameters( + new KeyParameter(hkdfOutput), + 8 * AeadUtils.GetAuthTagLength(aeadAlgorithm), + aeadIv, + aadata); - this.sessionInfo = c.DoFinal(si, 0, si.Length - 2); - } + cipher.Init(true, aeadParams); + byte[] keyBytes = cipher.DoFinal(si, 0, si.Length-2); + + return keyBytes; + } + + public override void AddSessionInfo( + byte[] si, + SecureRandom random) + { + if (skeskVersion == SymmetricKeyEncSessionPacket.Version4) + { + this.sessionInfo = EncryptSessionInfoForVersion4(si, random); + } + else if (skeskVersion == SymmetricKeyEncSessionPacket.Version6) + { + this.sessionInfo = EncryptSessionInfoForVersion6(si, random); + } + } public override void Encode(BcpgOutputStream pOut) { - SymmetricKeyEncSessionPacket pk = new SymmetricKeyEncSessionPacket( - encAlgorithm, s2k, sessionInfo); + SymmetricKeyEncSessionPacket pk; + if (skeskVersion == SymmetricKeyEncSessionPacket.Version6) + { + pk = new SymmetricKeyEncSessionPacket( + encAlgorithm, aeadAlgorithm, aeadIv, s2k, sessionInfo); + } + else + { + pk = new SymmetricKeyEncSessionPacket(encAlgorithm, s2k, sessionInfo); + } pOut.WritePacket(pk); } @@ -391,7 +444,16 @@ private byte[] ConvertToEncodedMpi(byte[] encryptedSessionInfo) public override void Encode(BcpgOutputStream pOut) { - PublicKeyEncSessionPacket pk = new PublicKeyEncSessionPacket(pubKey.KeyId, pubKey.Algorithm, data); + PublicKeyEncSessionPacket pk; + + if (pkeskVersion == PublicKeyEncSessionPacket.Version6) + { + pk = new PublicKeyEncSessionPacket(pubKey.Version, pubKey.GetFingerprint(), pubKey.Algorithm, data); + } + else + { + pk = new PublicKeyEncSessionPacket(pubKey.KeyId, pubKey.Algorithm, data); + } pOut.WritePacket(pk); } @@ -399,11 +461,13 @@ public override void Encode(BcpgOutputStream pOut) private readonly List methods = new List(); private readonly SymmetricKeyAlgorithmTag defAlgorithm; + private readonly AeadAlgorithmTag defAeadAlgorithm; private readonly SecureRandom rand; private readonly int skeskVersion; private readonly int pkeskVersion; private readonly int seipdVersion; + private readonly byte chunkSizeOctet = 6; // 1 << (chunkSize + 6) = 4096 public PgpEncryptedDataGenerator( SymmetricKeyAlgorithmTag encAlgorithm) @@ -465,6 +529,39 @@ private PgpEncryptedDataGenerator( seipdVersion = SymmetricEncIntegrityPacket.Version1; } + + public PgpEncryptedDataGenerator( + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm) + : this(encAlgorithm, aeadAlgorithm, CryptoServicesRegistrar.GetSecureRandom(), false) + { + } + + public PgpEncryptedDataGenerator( + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, + SecureRandom random) + : this(encAlgorithm, aeadAlgorithm, random, false) + { + } + + public PgpEncryptedDataGenerator( + SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, + SecureRandom random, + bool oldFormat) + { + this.rand = random ?? throw new ArgumentNullException(nameof(random)); + this.defAlgorithm = encAlgorithm; + this.defAeadAlgorithm = aeadAlgorithm; + this.oldFormat = oldFormat; + this.withIntegrityPacket = true; + + skeskVersion = SymmetricKeyEncSessionPacket.Version6; + pkeskVersion = PublicKeyEncSessionPacket.Version6; + seipdVersion = SymmetricEncIntegrityPacket.Version2; + } + /// Add a PBE encryption method to the encrypted object. /// /// Conversion of the passphrase characters to bytes is performed using Convert.ToByte(), which is @@ -497,7 +594,7 @@ internal void DoAddMethod(byte[] rawPassPhrase, bool clearPassPhrase, HashAlgori { S2k s2k = PgpUtilities.GenerateS2k(s2kDigest, 0x60, rand); - methods.Add(new PbeMethod(defAlgorithm, s2k, PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase), skeskVersion)); + methods.Add(new PbeMethod(defAlgorithm, defAeadAlgorithm, s2k, PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase), skeskVersion)); } /// Add a PBE encryption method to the encrypted object. @@ -535,6 +632,7 @@ internal void DoAddMethod(byte[] rawPassPhrase, bool clearPassPhrase, S2k.Argon2 methods.Add( new PbeMethod( defAlgorithm, + defAeadAlgorithm, s2k, PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase), skeskVersion @@ -577,9 +675,22 @@ private void AddCheckSum( private byte[] CreateSessionInfo(SymmetricKeyAlgorithmTag algorithm, KeyParameter key) { int keyLength = key.KeyLength; - byte[] sessionInfo = new byte[keyLength + 3]; - sessionInfo[0] = (byte)algorithm; - key.CopyTo(sessionInfo, 1, keyLength); + int infoLen = keyLength + 2; + int offset = 0; + + if (seipdVersion == SymmetricEncIntegrityPacket.Version1) + { + infoLen++; + offset = 1; + } + + byte[] sessionInfo = new byte[infoLen]; + + if (seipdVersion == SymmetricEncIntegrityPacket.Version1) + { + sessionInfo[0] = (byte)algorithm; + } + key.CopyTo(sessionInfo, offset, keyLength); AddCheckSum(sessionInfo); return sessionInfo; } @@ -600,7 +711,7 @@ private Stream Open( long length, byte[] buffer) { - if (cOut != null) + if (cOut != null || aeadOut != null) throw new InvalidOperationException("generator already in open state"); if (methods.Count == 0) throw new InvalidOperationException("No encryption methods specified"); @@ -613,29 +724,31 @@ private Stream Open( if (methods.Count == 1) { - if (methods[0] is PbeMethod pbeMethod) + if (methods[0] is PbeMethod pbeMethod && skeskVersion == SymmetricKeyEncSessionPacket.Version4) { + // For V4 SKESK, the encrypted session key is optional. If not present, the session key + // is derived directly with the S2K algorithm applied to the passphrase key = pbeMethod.GetKey(); } - else if (methods[0] is PubMethod pubMethod) + //else if (methods[0] is PubMethod pubMethod) + else { key = PgpUtilities.MakeRandomKey(defAlgorithm, rand); - byte[] sessionInfo = CreateSessionInfo(defAlgorithm, key); try { - pubMethod.AddSessionInfo(sessionInfo, rand); + methods[0].AddSessionInfo(sessionInfo, rand); } catch (Exception e) { throw new PgpException("exception encrypting session key", e); } } - else - { - throw new InvalidOperationException(); - } + //else + //{ + // throw new InvalidOperationException(); + //} pOut.WritePacket(methods[0]); } @@ -659,6 +772,63 @@ private Stream Open( } } + + if (seipdVersion == SymmetricEncIntegrityPacket.Version2) + { + if (buffer == null) + { + int chunkSize = 1 << (chunkSizeOctet + 6); + long chunks = ((length + chunkSize - 1) / chunkSize); + + long outputLength = length + + 1 // version + + 1 // algo ID + + 1 // AEAD algo ID + + 1 // chunk size octet + + 32 // salt + + AeadUtils.GetAuthTagLength(defAeadAlgorithm) * (chunks + 1); // one auth tag for each chunk plus final tag + + pOut = new BcpgOutputStream(outStr, PacketTag.SymmetricEncryptedIntegrityProtected, outputLength); + } + else + { + pOut = new BcpgOutputStream(outStr, PacketTag.SymmetricEncryptedIntegrityProtected, buffer); + } + + pOut.WriteByte(SymmetricEncIntegrityPacket.Version2); + pOut.WriteByte((byte)defAlgorithm); + pOut.WriteByte((byte)defAeadAlgorithm); + pOut.WriteByte((byte)chunkSizeOctet); + + byte[] salt = new byte[32]; + rand.NextBytes(salt); + pOut.Write(salt); + + var cipher = AeadUtils.CreateAeadCipher(defAlgorithm, defAeadAlgorithm); + byte[] aadata = SymmetricEncIntegrityPacket.CreateAAData(SymmetricEncIntegrityPacket.Version2, defAlgorithm, defAeadAlgorithm, chunkSizeOctet); + + AeadUtils.DeriveAeadMessageKeyAndIv( + key, + defAlgorithm, + defAeadAlgorithm, + salt, + aadata, + out var messageKey, + out var iv); + + aeadOut = new AeadOutputStream( + pOut, + cipher, + messageKey, + iv, + defAlgorithm, + defAeadAlgorithm, + chunkSizeOctet); + + return new WrappedGeneratorStream(this, aeadOut); + + } + string cName = PgpUtilities.GetSymmetricCipherName(defAlgorithm); if (cName == null) throw new PgpException("null cipher specified"); @@ -773,6 +943,14 @@ public Stream Open( [Obsolete("Dispose any opened Stream directly")] public void Close() { + if(aeadOut != null) + { + aeadOut.Close(); + pOut.Finish(); + + aeadOut = null; + pOut = null; + } if (cOut != null) { // TODO Should this all be under the try/catch block? diff --git a/crypto/src/openpgp/PgpPbeEncryptedData.cs b/crypto/src/openpgp/PgpPbeEncryptedData.cs index e007b6a6d..8ba9b4bdf 100644 --- a/crypto/src/openpgp/PgpPbeEncryptedData.cs +++ b/crypto/src/openpgp/PgpPbeEncryptedData.cs @@ -237,7 +237,7 @@ private Stream DoGetDataStreamVersion2(SymmetricEncIntegrityPacket seipd, byte[] var aadata = seipd.GetAAData(); var salt = seipd.GetSalt(); - PgpUtilities.DeriveAeadMessageKeyAndIv(sessionKey, seipd.CipherAlgorithm, seipd.AeadAlgorithm, salt, aadata, out var messageKey, out var iv); + AeadUtils.DeriveAeadMessageKeyAndIv(sessionKey, seipd.CipherAlgorithm, seipd.AeadAlgorithm, salt, aadata, out var messageKey, out var iv); var cipher = AeadUtils.CreateAeadCipher(seipd.CipherAlgorithm, seipd.AeadAlgorithm); var aeadStream = new AeadInputStream( diff --git a/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs b/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs index 36d23a764..859d48420 100644 --- a/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs +++ b/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs @@ -183,7 +183,7 @@ private Stream GetDataStreamSeipdVersion2(byte[] sessionData, SymmetricEncIntegr PgpUtilities.GetSymmetricCipherName(encAlgo), sessionData, 0, sessionData.Length); - PgpUtilities.DeriveAeadMessageKeyAndIv(sessionKey, encAlgo, aeadAlgo, salt, aadata, out var messageKey, out var iv); + AeadUtils.DeriveAeadMessageKeyAndIv(sessionKey, encAlgo, aeadAlgo, salt, aadata, out var messageKey, out var iv); var cipher = AeadUtils.CreateAeadCipher(seipd.CipherAlgorithm, seipd.AeadAlgorithm); var aeadStream = new AeadInputStream( diff --git a/crypto/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index 5210f5d1e..620724008 100644 --- a/crypto/src/openpgp/PgpUtilities.cs +++ b/crypto/src/openpgp/PgpUtilities.cs @@ -278,36 +278,6 @@ public static int GetKeySizeInOctets(SymmetricKeyAlgorithmTag algorithm) return (GetKeySize(algorithm) + 7) / 8; } - /// - /// Derive a message key and IV from the given session key. - /// - /// session key - /// symmetric cipher algorithm tag - /// AEAD algorithm tag - /// salt - /// HKDF info - /// - /// - public static void DeriveAeadMessageKeyAndIv( - KeyParameter sessionKey, - SymmetricKeyAlgorithmTag encAlgorithm, - AeadAlgorithmTag aeadAlgorithm, - byte[] salt, - byte[] hkdfInfo, - out KeyParameter messageKey, - out byte[] iv) - { - var hkdfGen = new HkdfBytesGenerator(CreateDigest(HashAlgorithmTag.Sha256)); - var hkdfParams = new HkdfParameters(sessionKey.GetKey(), salt, hkdfInfo); - hkdfGen.Init(hkdfParams); - var hkdfOutput = new byte[GetKeySizeInOctets(encAlgorithm) + AeadUtils.GetIVLength(aeadAlgorithm) - 8]; - hkdfGen.GenerateBytes(hkdfOutput, 0, hkdfOutput.Length); - - AeadUtils.SplitMessageKeyAndIv(hkdfOutput, encAlgorithm, aeadAlgorithm, out var messageKeyBytes, out iv); - - messageKey = new KeyParameter(messageKeyBytes); - } - public static KeyParameter MakeKey( SymmetricKeyAlgorithmTag algorithm, byte[] keyBytes) diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 74ad744f1..87bf55ab1 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -1,6 +1,5 @@ using NUnit.Framework; using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.Crypto.Agreement; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; @@ -166,6 +165,228 @@ public class PgpCryptoRefreshTest private readonly char[] emptyPassphrase = Array.Empty(); + #region "Helpers" + private void SignVerifyRoundtrip(PgpSecretKey signingKey, char[] passphrase) + { + byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); + byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); + + PgpSignatureGenerator sigGen = new PgpSignatureGenerator(signingKey.PublicKey.Algorithm, HashAlgorithmTag.Sha512); + PgpSignatureSubpacketGenerator spkGen = new PgpSignatureSubpacketGenerator(); + PgpPrivateKey privKey = signingKey.ExtractPrivateKey(passphrase); + spkGen.SetIssuerFingerprint(false, signingKey); + sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); + sigGen.Update(data); + sigGen.SetHashedSubpackets(spkGen.Generate()); + PgpSignature signature = sigGen.Generate(); + + AreEqual(signature.GetIssuerFingerprint(), signingKey.GetFingerprint()); + + VerifySignature(signature, data, signingKey.PublicKey); + VerifySignature(signature, wrongData, signingKey.PublicKey, shouldFail: true); + + byte[] encodedSignature = signature.GetEncoded(); + VerifyEncodedSignature(encodedSignature, data, signingKey.PublicKey); + VerifyEncodedSignature(encodedSignature, wrongData, signingKey.PublicKey, shouldFail: true); + } + + private void VerifyInlineSignature(byte[] message, PgpPublicKey signer, bool shouldFail = false) + { + byte[] data; + PgpObjectFactory factory = new PgpObjectFactory(message); + + PgpOnePassSignatureList p1 = factory.NextPgpObject() as PgpOnePassSignatureList; + PgpOnePassSignature ops = p1[0]; + + PgpLiteralData p2 = factory.NextPgpObject() as PgpLiteralData; + Stream dIn = p2.GetInputStream(); + + ops.InitVerify(signer); + + using (MemoryStream ms = new MemoryStream()) + { + byte[] buffer = new byte[30]; + int bytesRead; + while ((bytesRead = dIn.Read(buffer, 0, buffer.Length)) > 0) + { + ops.Update(buffer, 0, bytesRead); + ms.Write(buffer, 0, bytesRead); + } + + data = ms.ToArray(); + } + PgpSignatureList p3 = factory.NextPgpObject() as PgpSignatureList; + PgpSignature sig = p3[0]; + + bool result = ops.Verify(sig) != shouldFail; + IsTrue("signature test failed", result); + + VerifySignature(sig, data, signer, shouldFail); + } + + private void VerifySignature(PgpSignature signature, byte[] data, PgpPublicKey signer, bool shouldFail = false) + { + IsEquals(signature.KeyAlgorithm, signer.Algorithm); + // the version of the signature is bound to the version of the signing key + IsEquals(signature.Version, signer.Version); + + if (signature.KeyId != 0) + { + IsEquals(signature.KeyId, signer.KeyId); + } + byte[] issuerFpt = signature.GetIssuerFingerprint(); + if (issuerFpt != null) + { + IsTrue(AreEqual(issuerFpt, signer.GetFingerprint())); + } + + signature.InitVerify(signer); + signature.Update(data); + + bool result = signature.Verify() != shouldFail; + IsTrue("signature test failed", result); + } + + private void VerifyEncodedSignature(byte[] sigPacket, byte[] data, PgpPublicKey signer, bool shouldFail = false) + { + PgpObjectFactory factory = new PgpObjectFactory(sigPacket); + PgpSignatureList sigList = factory.NextPgpObject() as PgpSignatureList; + PgpSignature signature = sigList[0]; + + VerifySignature(signature, data, signer, shouldFail); + } + + private static PgpEncryptedDataGenerator CreatePgpEncryptedDataGenerator(bool useAead) + { + if (useAead) + { + return new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Aes256, AeadAlgorithmTag.Ocb, new SecureRandom()); + } + else + { + return new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Aes256, true, new SecureRandom()); + } + } + + private static byte[] EncryptPlaintext(PgpEncryptedDataGenerator encDataGen, byte[] plaintext, bool useBuffer) + { + byte[] enc; + + if (useBuffer) + { + byte[] buffer = new byte[1024]; + using (MemoryStream ms = new MemoryStream()) + { + using (Stream cOut = encDataGen.Open(ms, buffer)) + { + using (BcpgOutputStream bcOut = new BcpgOutputStream(cOut, newFormatOnly: true)) + { + PgpLiteralDataGenerator literalDataGen = new PgpLiteralDataGenerator(); + DateTime modificationTime = DateTime.UtcNow; + + using (Stream lOut = literalDataGen.Open( + new UncloseableStream(bcOut), + PgpLiteralData.Utf8, + PgpLiteralData.Console, + plaintext.Length, + modificationTime)) + { + lOut.Write(plaintext, 0, plaintext.Length); + } + } + } + enc = ms.ToArray(); + } + } + else + { + byte[] literal; + using (MemoryStream ms = new MemoryStream()) + { + PgpLiteralDataGenerator literalDataGen = new PgpLiteralDataGenerator(); + DateTime modificationTime = DateTime.UtcNow; + + using (Stream lOut = literalDataGen.Open( + new UncloseableStream(ms), + PgpLiteralData.Utf8, + PgpLiteralData.Console, + plaintext.Length, + modificationTime)) + { + lOut.Write(plaintext, 0, plaintext.Length); + } + literal = ms.ToArray(); + } + using (MemoryStream ms = new MemoryStream()) + { + using (Stream cOut = encDataGen.Open(ms, literal.Length)) + { + cOut.Write(literal, 0, literal.Length); + } + enc = ms.ToArray(); + } + } + + return enc; + } + + private void SymmetricEncryptDecryptRoundtrip(byte[] plaintext, bool useAead, bool useBuffer, byte[] rawPassword) + { + // encrypt + PgpEncryptedDataGenerator encDataGen = CreatePgpEncryptedDataGenerator(useAead); + encDataGen.AddMethodRaw(rawPassword, S2k.Argon2Parameters.MemoryConstrainedParameters()); + byte[] enc = EncryptPlaintext(encDataGen, plaintext, useBuffer); + + // decrypt + PgpObjectFactory factory = new PgpObjectFactory(enc); + PgpEncryptedDataList encDataList = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encDataList is null); + + PgpPbeEncryptedData encData = encDataList[0] as PgpPbeEncryptedData; + FailIf("invalid PgpPublicKeyEncryptedData", encData is null); + + using (Stream stream = encData.GetDataStreamRaw(rawPassword)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (MemoryStream ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + byte[] decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + } + + private void PubkeyEncryptDecryptRoundtrip(byte[] plaintext, bool useAead,bool useBuffer, PgpPublicKey pubKey, PgpPrivateKey privKey) + { + // encrypt + PgpEncryptedDataGenerator encDataGen = CreatePgpEncryptedDataGenerator(useAead); + encDataGen.AddMethod(pubKey); + byte[] enc = EncryptPlaintext(encDataGen, plaintext, useBuffer); + + // decrypt + PgpObjectFactory factory = new PgpObjectFactory(enc); + PgpEncryptedDataList encDataList = factory.NextPgpObject() as PgpEncryptedDataList; + FailIf("invalid PgpEncryptedDataList", encDataList is null); + + PgpPublicKeyEncryptedData encData = encDataList[0] as PgpPublicKeyEncryptedData; + FailIf("invalid PgpPublicKeyEncryptedData", encData is null); + + using (Stream stream = encData.GetDataStream(privKey)) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (MemoryStream ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + byte[] decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(plaintext, decrypted)); + } + } + } + #endregion + [Test] public void Version4Ed25519LegacyPubkeySampleTest() { @@ -308,31 +529,6 @@ public void Version6PublicKeyCreationTest() shouldFail: true); } - - private void SignVerifyRoundtrip(PgpSecretKey signingKey, char[] passphrase) - { - byte[] data = Encoding.UTF8.GetBytes("OpenPGP"); - byte[] wrongData = Encoding.UTF8.GetBytes("OpePGP"); - - PgpSignatureGenerator sigGen = new PgpSignatureGenerator(signingKey.PublicKey.Algorithm, HashAlgorithmTag.Sha512); - PgpSignatureSubpacketGenerator spkGen = new PgpSignatureSubpacketGenerator(); - PgpPrivateKey privKey = signingKey.ExtractPrivateKey(passphrase); - spkGen.SetIssuerFingerprint(false, signingKey); - sigGen.InitSign(PgpSignature.CanonicalTextDocument, privKey, new SecureRandom()); - sigGen.Update(data); - sigGen.SetHashedSubpackets(spkGen.Generate()); - PgpSignature signature = sigGen.Generate(); - - AreEqual(signature.GetIssuerFingerprint(), signingKey.GetFingerprint()); - - VerifySignature(signature, data, signingKey.PublicKey); - VerifySignature(signature, wrongData, signingKey.PublicKey, shouldFail: true); - - byte[] encodedSignature = signature.GetEncoded(); - VerifyEncodedSignature(encodedSignature, data, signingKey.PublicKey); - VerifyEncodedSignature(encodedSignature, wrongData, signingKey.PublicKey, shouldFail: true); - } - [Test] public void Version6UnlockedSecretKeyParsingTest() { @@ -506,9 +702,20 @@ public void Version6Ed25519KeyPairCreationTest() // Sign-Verify roundtrip SignVerifyRoundtrip(pgpseckey, emptyPassphrase); - // encrypt-decrypt test - EncryptDecryptRoundtrip( + // encrypt-decrypt roundtrip + // V3 PKESK + V1 SEIPD + PubkeyEncryptDecryptRoundtrip( Encoding.UTF8.GetBytes("Hello, World!"), + false, + false, + subKey.PublicKey, + subKey.ExtractPrivateKey(emptyPassphrase)); + + // V6 PKESK + V2 SEIPD + PubkeyEncryptDecryptRoundtrip( + Encoding.UTF8.GetBytes("Hello, World!"), + true, + false, subKey.PublicKey, subKey.ExtractPrivateKey(emptyPassphrase)); } @@ -591,8 +798,10 @@ public void Version6Ed448KeyPairCreationTest() SignVerifyRoundtrip(pgpseckey, emptyPassphrase); // Encrypt-Decrypt roundtrip - EncryptDecryptRoundtrip( + PubkeyEncryptDecryptRoundtrip( Encoding.UTF8.GetBytes("Hello, World!"), + false, + false, subKey.PublicKey, subKey.ExtractPrivateKey(emptyPassphrase)); } @@ -638,8 +847,10 @@ public void Version6LockedSecretKeyParsingTest() IsEquals(encryptionKey.PublicKey.KeyId, 0x12C83F1E706F6308); // encrypt-decrypt - EncryptDecryptRoundtrip( + PubkeyEncryptDecryptRoundtrip( Encoding.UTF8.GetBytes("Hello, World!"), + false, + false, encryptionKey.PublicKey, encryptionKey.ExtractPrivateKey(passphrase.ToCharArray())); @@ -727,72 +938,6 @@ public void Version6GenerateAndVerifyInlineSignatureTest() VerifyInlineSignature(inlineSignatureMessage, signingKey.PublicKey, shouldFail: true); } - private void VerifyInlineSignature(byte[] message, PgpPublicKey signer, bool shouldFail = false) - { - byte[] data; - PgpObjectFactory factory = new PgpObjectFactory(message); - - PgpOnePassSignatureList p1 = factory.NextPgpObject() as PgpOnePassSignatureList; - PgpOnePassSignature ops = p1[0]; - - PgpLiteralData p2 = factory.NextPgpObject() as PgpLiteralData; - Stream dIn = p2.GetInputStream(); - - ops.InitVerify(signer); - - using (MemoryStream ms = new MemoryStream()) - { - byte[] buffer = new byte[30]; - int bytesRead; - while ((bytesRead = dIn.Read(buffer, 0, buffer.Length)) > 0) - { - ops.Update(buffer, 0, bytesRead); - ms.Write(buffer, 0, bytesRead); - } - - data = ms.ToArray(); - } - PgpSignatureList p3 = factory.NextPgpObject() as PgpSignatureList; - PgpSignature sig = p3[0]; - - bool result = ops.Verify(sig) != shouldFail; - IsTrue("signature test failed", result); - - VerifySignature(sig, data, signer, shouldFail); - } - - private void VerifySignature(PgpSignature signature, byte[] data, PgpPublicKey signer, bool shouldFail = false) - { - IsEquals(signature.KeyAlgorithm, signer.Algorithm); - // the version of the signature is bound to the version of the signing key - IsEquals(signature.Version, signer.Version); - - if (signature.KeyId != 0) - { - IsEquals(signature.KeyId, signer.KeyId); - } - byte[] issuerFpt = signature.GetIssuerFingerprint(); - if (issuerFpt != null) - { - IsTrue(AreEqual(issuerFpt, signer.GetFingerprint())); - } - - signature.InitVerify(signer); - signature.Update(data); - - bool result = signature.Verify() != shouldFail; - IsTrue("signature test failed", result); - } - - private void VerifyEncodedSignature(byte[] sigPacket, byte[] data, PgpPublicKey signer, bool shouldFail = false) - { - PgpObjectFactory factory = new PgpObjectFactory(sigPacket); - PgpSignatureList sigList = factory.NextPgpObject() as PgpSignatureList; - PgpSignature signature = sigList[0]; - - VerifySignature(signature, data, signer, shouldFail); - } - [Test] public void Version6SkeskVersion2SeipdTest() { @@ -997,112 +1142,26 @@ public void SkeskWithArgon2Test() }); } - // encrypt-decrypt roundtrip - V4 SKESK + V1 SEIPD - { - // encrypt - byte[] enc; - using (MemoryStream ms = new MemoryStream()) - { - byte[] buffer = new byte[1024]; - PgpEncryptedDataGenerator endDataGen = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Aes256, true, new SecureRandom()); - endDataGen.AddMethodRaw(password, S2k.Argon2Parameters.MemoryConstrainedParameters()); - - using (Stream cOut = endDataGen.Open(ms, buffer)) - { - using (BcpgOutputStream bcOut = new BcpgOutputStream(cOut, newFormatOnly: true)) - { - PgpLiteralDataGenerator literalDataGen = new PgpLiteralDataGenerator(); - DateTime modificationTime = DateTime.UtcNow; - - using (Stream lOut = literalDataGen.Open( - new UncloseableStream(bcOut), - PgpLiteralData.Utf8, - PgpLiteralData.Console, - plaintext.Length, - modificationTime)) - { - lOut.Write(plaintext, 0, plaintext.Length); - } - } - } - - enc = ms.ToArray(); - } - - // decrypt - PgpObjectFactory factory = new PgpObjectFactory(enc); - PgpEncryptedDataList encDataList = factory.NextPgpObject() as PgpEncryptedDataList; - FailIf("invalid PgpEncryptedDataList", encDataList is null); - - PgpPbeEncryptedData encData = encDataList[0] as PgpPbeEncryptedData; - FailIf("invalid PgpPbeEncryptedData", encData is null); - - using (Stream stream = encData.GetDataStreamRaw(password)) - { - factory = new PgpObjectFactory(stream); - PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; - using (MemoryStream ms = new MemoryStream()) - { - lit.GetDataStream().CopyTo(ms); - byte[] decrypted = ms.ToArray(); - IsTrue(Arrays.AreEqual(plaintext, decrypted)); - } - } - } + // encrypt-decrypt roundtrip + byte[] largePlaintext = new byte[50000]; + Arrays.Fill(largePlaintext, 0); + // V4 SKESK + V1 SEIPD + // Using length in PgpEncryptedDataGenerator.Open + SymmetricEncryptDecryptRoundtrip(plaintext, false, false, password); + SymmetricEncryptDecryptRoundtrip(largePlaintext, false, false, password); + // Using buffer in PgpEncryptedDataGenerator.Open + SymmetricEncryptDecryptRoundtrip(plaintext, false, true, password); + SymmetricEncryptDecryptRoundtrip(largePlaintext, false, true, password); + // AEAD V6 SKESK + V2 SEIPD + // Using length + SymmetricEncryptDecryptRoundtrip(plaintext, true, false, password); + SymmetricEncryptDecryptRoundtrip(largePlaintext, true, false, password); + // Using buffer + SymmetricEncryptDecryptRoundtrip(plaintext, true, true, password); + SymmetricEncryptDecryptRoundtrip(largePlaintext, true, true, password); } - private void EncryptDecryptRoundtrip(byte[] plaintext, PgpPublicKey pubKey, PgpPrivateKey privKey) - { - byte[] enc; - using (MemoryStream ms = new MemoryStream()) - { - byte[] buffer = new byte[1024]; - PgpEncryptedDataGenerator endDataGen = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Aes256, true, new SecureRandom()); - endDataGen.AddMethod(pubKey); - - using (Stream cOut = endDataGen.Open(ms, buffer)) - { - using (BcpgOutputStream bcOut = new BcpgOutputStream(cOut, newFormatOnly: true)) - { - PgpLiteralDataGenerator literalDataGen = new PgpLiteralDataGenerator(); - DateTime modificationTime = DateTime.UtcNow; - - using (Stream lOut = literalDataGen.Open( - new UncloseableStream(bcOut), - PgpLiteralData.Utf8, - PgpLiteralData.Console, - plaintext.Length, - modificationTime)) - { - lOut.Write(plaintext, 0, plaintext.Length); - } - } - } - enc = ms.ToArray(); - } - - // decrypt - PgpObjectFactory factory = new PgpObjectFactory(enc); - PgpEncryptedDataList encDataList = factory.NextPgpObject() as PgpEncryptedDataList; - FailIf("invalid PgpEncryptedDataList", encDataList is null); - - PgpPublicKeyEncryptedData encData = encDataList[0] as PgpPublicKeyEncryptedData; - FailIf("invalid PgpPublicKeyEncryptedData", encData is null); - - using (Stream stream = encData.GetDataStream(privKey)) - { - factory = new PgpObjectFactory(stream); - PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; - using (MemoryStream ms = new MemoryStream()) - { - lit.GetDataStream().CopyTo(ms); - byte[] decrypted = ms.ToArray(); - IsTrue(Arrays.AreEqual(plaintext, decrypted)); - } - } - } - [Test] public void PkeskTest() { @@ -1202,13 +1261,30 @@ public void PkeskTest() } } - // encrypt-decrypt roundtrip - V3 PKESK + V1 SEIPD X25519 + // encrypt-decrypt roundtrip { - byte[] plaintext = Encoding.UTF8.GetBytes("Hello, world!"); + byte[] shortPlaintext = Encoding.UTF8.GetBytes("Hello, world!"); + byte[] largePlaintext = new byte[50000]; + Arrays.Fill(largePlaintext, 0); + PgpPublicKeyRing publicKeyRing = new PgpPublicKeyRing(v6Certificate); PgpPublicKey pubKey = publicKeyRing.GetPublicKeys().First(k => k.IsEncryptionKey); - EncryptDecryptRoundtrip(plaintext, pubKey, privKey); + // V3 PKESK + V1 SEIPD X25519 + // Using length in PgpEncryptedDataGenerator.Open + PubkeyEncryptDecryptRoundtrip(shortPlaintext, false, false, pubKey, privKey); + PubkeyEncryptDecryptRoundtrip(largePlaintext, false, false, pubKey, privKey); + // Using buffer in PgpEncryptedDataGenerator.Open + PubkeyEncryptDecryptRoundtrip(shortPlaintext, false, true, pubKey, privKey); + PubkeyEncryptDecryptRoundtrip(largePlaintext, false, true, pubKey, privKey); + + // V6 PKESK + V2 SEIPD X25519 + // Using length + PubkeyEncryptDecryptRoundtrip(shortPlaintext, true, false, pubKey, privKey); + PubkeyEncryptDecryptRoundtrip(largePlaintext, true, false, pubKey, privKey); + // Using buffer + PubkeyEncryptDecryptRoundtrip(shortPlaintext, true, true, pubKey, privKey); + PubkeyEncryptDecryptRoundtrip(largePlaintext, true, true, pubKey, privKey); } } From b0848c052253a50a79de942e01c9d098fa463ab4 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 25 Apr 2024 12:09:33 +0200 Subject: [PATCH 27/37] Correct usage of RSA and ECDH in V6 PKESK --- .../src/openpgp/PgpEncryptedDataGenerator.cs | 6 + .../src/openpgp/PgpPublicKeyEncryptedData.cs | 28 +- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 2 +- .../test/PgpInteroperabilityTestSuite.cs | 298 +++++++++++++++++- 4 files changed, 325 insertions(+), 9 deletions(-) diff --git a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs index f32be2fec..a80578549 100644 --- a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs +++ b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs @@ -652,6 +652,12 @@ public void AddMethod(PgpPublicKey key, bool sessionKeyObfuscation) throw new ArgumentException("passed in key not an encryption key!"); } + if (pkeskVersion == PublicKeyEncSessionPacket.Version6 + && (key.Algorithm == PublicKeyAlgorithmTag.ElGamalEncrypt || key.Algorithm == PublicKeyAlgorithmTag.ElGamalGeneral)) + { + throw new PgpException("cannot generate ElGamal v6 PKESK (see https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-fo)"); + } + methods.Add(new PubMethod(key, sessionKeyObfuscation, pkeskVersion)); } diff --git a/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs b/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs index 859d48420..64d02ebfe 100644 --- a/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs +++ b/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs @@ -179,9 +179,16 @@ private Stream GetDataStreamSeipdVersion2(byte[] sessionData, SymmetricEncIntegr var aadata = seipd.GetAAData(); var salt = seipd.GetSalt(); + // no checksum and padding for X25519 and X448 + int length = sessionData.Length; + if (keyData.Algorithm != PublicKeyAlgorithmTag.X25519 && keyData.Algorithm != PublicKeyAlgorithmTag.X448) + { + length -= 2; + } + var sessionKey = ParameterUtilities.CreateKeyParameter( PgpUtilities.GetSymmetricCipherName(encAlgo), - sessionData, 0, sessionData.Length); + sessionData, 0, length); AeadUtils.DeriveAeadMessageKeyAndIv(sessionKey, encAlgo, aeadAlgo, salt, aadata, out var messageKey, out var iv); var cipher = AeadUtils.CreateAeadCipher(seipd.CipherAlgorithm, seipd.AeadAlgorithm); @@ -205,15 +212,26 @@ public Stream GetDataStream( { byte[] sessionData = RecoverSessionData(privKey); + if (!ConfirmCheckSum(sessionData)) + throw new PgpKeyValidationException("key checksum failed"); + if (keyData.Version == PublicKeyEncSessionPacket.Version6) { // V6 PKESK + V2 SEIPD - return GetDataStreamSeipdVersion2(sessionData, (SymmetricEncIntegrityPacket)encData); + try + { + return GetDataStreamSeipdVersion2(sessionData, (SymmetricEncIntegrityPacket)encData); + } + catch (PgpException) + { + throw; + } + catch (Exception e) + { + throw new PgpException("Exception starting decryption", e); + } } - if (!ConfirmCheckSum(sessionData)) - throw new PgpKeyValidationException("key checksum failed"); - SymmetricKeyAlgorithmTag symmAlg; if (keyData.Algorithm == PublicKeyAlgorithmTag.X25519 || keyData.Algorithm == PublicKeyAlgorithmTag.X448) { diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 87bf55ab1..053d08f34 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -343,7 +343,7 @@ private void SymmetricEncryptDecryptRoundtrip(byte[] plaintext, bool useAead, bo FailIf("invalid PgpEncryptedDataList", encDataList is null); PgpPbeEncryptedData encData = encDataList[0] as PgpPbeEncryptedData; - FailIf("invalid PgpPublicKeyEncryptedData", encData is null); + FailIf("invalid PgpPbeEncryptedData", encData is null); using (Stream stream = encData.GetDataStreamRaw(rawPassword)) { diff --git a/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs b/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs index a9122ba29..d02195566 100644 --- a/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs +++ b/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs @@ -6,6 +6,7 @@ using System; using System.IO; using System.Linq; +using System.Runtime.Serialization.Formatters; using System.Text; namespace Org.BouncyCastle.Bcpg.OpenPgp.Tests @@ -14,7 +15,7 @@ namespace Org.BouncyCastle.Bcpg.OpenPgp.Tests public class PgpInteroperabilityTestSuite : SimpleTest { - // v4 key "Alice" from "OpenPGP Example Keys and Certificates" + // v4 EdDSA/ECDH key "Alice" from "OpenPGP Example Keys and Certificates" // https://www.ietf.org/archive/id/draft-bre-openpgp-samples-01.html#name-alices-ed25519-samples private static readonly byte[] alicePubkey = Base64.Decode( "mDMEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U" + @@ -40,7 +41,191 @@ public class PgpInteroperabilityTestSuite "hzGs1O0RkWNQWbUzQ8nUOeD9wNbjE3zR+yfRAQDbYqvtWQKN4AQLTxVJN5X5AWyb" + "Pnn+We1aTBhaGa86AQ=="); - // v6 keys from crypto-refresh + // v4 RSA-3072 key "Bob" from "OpenPGP Example Keys and Certificates" + // https://www.ietf.org/archive/id/draft-bre-openpgp-samples-01.html#name-bobs-rsa-3072-samples + private static readonly byte[] bobPubkey = Base64.Decode( + "mQGNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb" + + "vLIwa3T4CyshfT0AEQEAAbQhQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w" + + "bGU+iQHOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx" + + "gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz" + + "XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO" + + "ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g" + + "9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF" + + "DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c" + + "ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1" + + "6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ" + + "ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo" + + "zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGuQGNBF2lnPIBDADW" + + "ML9cbGMrp12CtF9b2P6z9TTT74S8iyBOzaSvdGDQY/sUtZXRg21HWamXnn9sSXvI" + + "DEINOQ6A9QxdxoqWdCHrOuW3ofneYXoG+zeKc4dC86wa1TR2q9vW+RMXSO4uImA+" + + "Uzula/6k1DogDf28qhCxMwG/i/m9g1c/0aApuDyKdQ1PXsHHNlgd/Dn6rrd5y2AO" + + "baifV7wIhEJnvqgFXDN2RXGjLeCOHV4Q2WTYPg/S4k1nMXVDwZXrvIsA0YwIMgIT" + + "86Rafp1qKlgPNbiIlC1g9RY/iFaGN2b4Ir6GDohBQSfZW2+LXoPZuVE/wGlQ01rh" + + "827KVZW4lXvqsge+wtnWlszcselGATyzqOK9LdHPdZGzROZYI2e8c+paLNDdVPL6" + + "vdRBUnkCaEkOtl1mr2JpQi5nTU+gTX4IeInC7E+1a9UDF/Y85ybUz8XV8rUnR76U" + + "qVC7KidNepdHbZjjXCt8/Zo+Tec9JNbYNQB/e9ExmDntmlHEsSEQzFwzj8sxH48A" + + "EQEAAYkBtgQYAQoAIBYhBNGmbhojsYLJmA94jPv8yCoBXnMwBQJdpZzyAhsMAAoJ" + + "EPv8yCoBXnMw6f8L/26C34dkjBffTzMj5Bdzm8MtF67OYneJ4TQMw7+41IL4rVcS" + + "KhIhk/3Ud5knaRtP2ef1+5F66h9/RPQOJ5+tvBwhBAcUWSupKnUrdVaZQanYmtSx" + + "cVV2PL9+QEiNN3tzluhaWO//rACxJ+K/ZXQlIzwQVTpNhfGzAaMVV9zpf3u0k14i" + + "tcv6alKY8+rLZvO1wIIeRZLmU0tZDD5HtWDvUV7rIFI1WuoLb+KZgbYn3OWjCPHV" + + "dTrdZ2CqnZbG3SXw6awH9bzRLV9EXkbhIMez0deCVdeo+wFFklh8/5VK2b0vk/+w" + + "qMJxfpa1lHvJLobzOP9fvrswsr92MA2+k901WeISR7qEzcI0Fdg8AyFAExaEK6Vy" + + "jP7SXGLwvfisw34OxuZr3qmx1Sufu4toH3XrB7QJN8XyqqbsGxUCBqWif9RSK4xj" + + "zRTe56iPeiSJJOIciMP9i2ldI+KgLycyeDvGoBj0HCLO3gVaBe4ubVrj5KjhX2PV" + + "NEJd3XZRzaXZE2aAMQ=="); + + private static readonly byte[] bobSecretkey = Base64.Decode( + "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb" + + "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM" + + "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK" + + "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z" + + "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs" + + "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ" + + "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4" + + "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI" + + "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP" + + "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6" + + "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E" + + "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx" + + "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL" + + "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN" + + "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/" + + "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC" + + "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC" + + "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E" + + "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8" + + "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P" + + "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT" + + "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w" + + "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad" + + "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx" + + "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp" + + "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h" + + "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np" + + "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c" + + "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0" + + "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o" + + "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny" + + "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK" + + "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl" + + "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr" + + "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ" + + "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5" + + "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/" + + "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5" + + "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf" + + "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd" + + "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ" + + "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM" + + "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY" + + "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ" + + "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP" + + "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX" + + "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief" + + "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0" + + "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg" + + "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH" + + "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd" + + "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91" + + "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7" + + "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE="); + + // v4 DSA/ElGamal key "Carol" from "OpenPGP interoperability test suite" + // https://tests.sequoia-pgp.org/#Encrypt-Decrypt_roundtrip_with_key__Carol_ + private static readonly byte[] carolPubkey = Base64.Decode( + "xsPuBF3+CmgRDADZhdKTM3ms3XpXnQke83FgaIBtP1g1qhqpCfg50WiPS0kjiMC0" + + "OJz2vh59nusbBLzgI//Y1VMhKfIWYbqMcIY+lWbseHjl52rqW6AaJ0TH4NgVt7vh" + + "yVeJt0k/NnxvNhMd0587KXmfpDxrwBqc/l5cVB+p0rL8vs8kxojHXAi5V3koM0Uj" + + "REWs5Jpj/XU9LhEoyXZkeJC/pes1u6UKoFYn7dFIP49Kkd1kb+1bNfdPYtA0JpcG" + + "zYgeMNOvdWJwn43dNhxoeuXfmAEhA8LdzT0C0O+7akXOKWrfhXJ8MTBqvPgWZYx7" + + "MNuQx/ejIMZHl+Iaf7hG976ILH+NCGiKkhidd9GIuA/WteHiQbXLyfiQ4n8P12q9" + + "+4dq6ybUM65tnozRyyN+1m3rU2a/+Ly3JCh4TeO27w+cxMWkaeHyTQaJVMbMbDpX" + + "duVd32MA33UVNH5/KXMVczVi5asVjuKDSojJDV1QwX8izZNl1t+AI0L3balCabV0" + + "SFhlfnBEUj1my1sBAMOSO/I67BvBS3IPHZWXHjgclhs26mPzRlZLryAUWR2DDACH" + + "5fx+yUAdZ8Vu/2zWTHxwWJ/X6gGTLqa9CmfDq5UDqYFFzuWwN4HJ+ryOuak1CGwS" + + "KJUBSA75HExbv0naWg+suy+pEDvF0VALPU9VUkSQtHyR10YO2FWOe3AEtpbYDRwp" + + "dr1ZwEbb3L6IGQ5i/4CNHbJ2u3yUeXsDNAvrpVSEcIjA01RPCOKmf58SDZp4yDdP" + + "xGhM8w6a18+fdQr22f2cJ0xgfPlbzFbO+FUsEgKvn6QTLhbaYw4zs7rdQDejWHV8" + + "2hP4K+rb9FwknYdV9uo4m77MgGlU+4yvJnGEYaL3jwjI3bH9aooNOl6XbvVAzNzo" + + "mYmaTO7mp6xFAu43yuGyd9K+1E4k7CQTROxTZ+RdtQjV95hSsEmMg792nQvDSBW4" + + "xwfOQ7pf3kC7r9fm8u9nBlEN12HsbQ8Yvux/ld5q5RaIlD19jzfVR6+hJzbj2ZnU" + + "yQs4ksAfIHTzTdLttRxS9lTRTkVx2vbUnoSBy6TYF1mf6nRPpSm1riZxnkR4+BQL" + + "/0rUAxwegTNIG/5M612s2a45QvYK1turZ7spI1RGitJUIjBXUuR76jIsyqagIhBl" + + "5nEsQ4HLv8OQ3EgJ5T9gldLFpHNczLxBQnnNwfPoD2e0kC/iy0rfiNX8HWpTgQpb" + + "zAosLj5/E0iNlildynIhuqBosyRWFqGva0O6qioL90srlzlfKCloe9R9w3HizjCb" + + "f59yEspuJt9iHVNOPOW2Wj5ub0KTiJPp9vBmrFaB79/IlgojpQoYvQ77Hx5A9CJq" + + "paMCHGOW6Uz9euN1ozzETEkIPtL8XAxcogfpe2JKE1uS7ugxsKEGEDfxOQFKAGV0" + + "XFtIx50vFCr2vQro0WB858CGN47dCxChhNUxNtGc11JNEkNv/X7hKtRf/5VCmnaz" + + "GWwNK47cqZ7GJfEBnElD7s/tQvTC5Qp7lg9gEt47TUX0bjzUTCxNvLosuKL9+J1W" + + "ln1myRpff/5ZOAnZTPHR+AbX4bRB4sK5zijQe4139Dn2oRYK+EIYoBAxFxSOzehP" + + "IcKKBB8RCAA8BQJd/gppAwsJCgkQm6eJ3HbWhJoEFQoJCAIWAQIXgAIbAwIeARYh" + + "BHH/2gBECeXdsMPo8Zunidx21oSaAABihQD/VWnF1HbBhP+kLwWsqxuYjEslEsM2" + + "UQPeKGK9an8HZ78BAJPaiL3OpuOmsIoCfOghhMZOKXjIV+Z57LwaMw7FQfPgzSZD" + + "YXJvbCBPbGRzdHlsZSA8Y2Fyb2xAb3BlbnBncC5leGFtcGxlPsKKBBMRCAA8BQJd" + + "/gppAwsJCgkQm6eJ3HbWhJoEFQoJCAIWAQIXgAIbAwIeARYhBHH/2gBECeXdsMPo" + + "8Zunidx21oSaAABQTAD/ZMXAvSbKaMJJpAfwp1C7KAj6K2k2CAz5jwUXyGf1+jUA" + + "/2iAMiX1XcLy3n0L8ytzge8/UAFHafBl4rn4DmUugfhjzsPMBF3+CmgQDADZhdKT" + + "M3ms3XpXnQke83FgaIBtP1g1qhqpCfg50WiPS0kjiMC0OJz2vh59nusbBLzgI//Y" + + "1VMhKfIWYbqMcIY+lWbseHjl52rqW6AaJ0TH4NgVt7vhyVeJt0k/NnxvNhMd0587" + + "KXmfpDxrwBqc/l5cVB+p0rL8vs8kxojHXAi5V3koM0UjREWs5Jpj/XU9LhEoyXZk" + + "eJC/pes1u6UKoFYn7dFIP49Kkd1kb+1bNfdPYtA0JpcGzYgeMNOvdWJwn43dNhxo" + + "euXfmAEhA8LdzT0C0O+7akXOKWrfhXJ8MTBqvPgWZYx7MNuQx/ejIMZHl+Iaf7hG" + + "976ILH+NCGiKkhidd9GIuA/WteHiQbXLyfiQ4n8P12q9+4dq6ybUM65tnozRyyN+" + + "1m3rU2a/+Ly3JCh4TeO27w+cxMWkaeHyTQaJVMbMbDpXduVd32MA33UVNH5/KXMV" + + "czVi5asVjuKDSojJDV1QwX8izZNl1t+AI0L3balCabV0SFhlfnBEUj1my1sMAIfl" + + "/H7JQB1nxW7/bNZMfHBYn9fqAZMupr0KZ8OrlQOpgUXO5bA3gcn6vI65qTUIbBIo" + + "lQFIDvkcTFu/SdpaD6y7L6kQO8XRUAs9T1VSRJC0fJHXRg7YVY57cAS2ltgNHCl2" + + "vVnARtvcvogZDmL/gI0dsna7fJR5ewM0C+ulVIRwiMDTVE8I4qZ/nxINmnjIN0/E" + + "aEzzDprXz591CvbZ/ZwnTGB8+VvMVs74VSwSAq+fpBMuFtpjDjOzut1AN6NYdXza" + + "E/gr6tv0XCSdh1X26jibvsyAaVT7jK8mcYRhovePCMjdsf1qig06Xpdu9UDM3OiZ" + + "iZpM7uanrEUC7jfK4bJ30r7UTiTsJBNE7FNn5F21CNX3mFKwSYyDv3adC8NIFbjH" + + "B85Dul/eQLuv1+by72cGUQ3XYextDxi+7H+V3mrlFoiUPX2PN9VHr6EnNuPZmdTJ" + + "CziSwB8gdPNN0u21HFL2VNFORXHa9tSehIHLpNgXWZ/qdE+lKbWuJnGeRHj4FAv+" + + "MQaafW0uHF+N8MDm8UWPvf4Vd0UJ0UpIjRWl2hTV+BHkNfvZlBRhhQIphNiKRe/W" + + "ap0f/lW2Gm2uS0KgByjjNXEzTiwrte2GX65M6F6Lz8N31kt1Iig1xGOuv+6HmxTN" + + "R8gL2K5PdJeJn8PTJWrRS7+BY8Hdkgb+wVpzE5cCvpFiG/P0yqfBdLWxVPlPI7dc" + + "hDkmx4iAhHJX9J/gX/hC6L3AzPNJqNPAKy20wYp/ruTbbwBolW/4ikWij460JrvB" + + "sm6Sp81A3ebaiN9XkJygLOyhGyhMieGulCYz6AahAFcECtPXGTcordV1mJth8yjF" + + "4gZfDQyg0nMW4Yr49yeFXcRMUw1yzN3Q9v2zzqDuFi2lGYTXYmVqLYzM9KbLO2Wx" + + "E/21xnBjLsl09l/FdA/bhdZq3t4/apbFOeQQ/j/AphvzWbsJnhG9Q7+d3VoDlz0g" + + "FiSduCYIAAq8dUOJNjrUTkZsL1pOIjhYjCMi2uiKS6RQkT6nvuumPF/D/VTnUGeZ" + + "wooEGBEIADwFAl3+CmkDCwkKCRCbp4ncdtaEmgQVCgkIAhYBAheAAhsMAh4BFiEE" + + "cf/aAEQJ5d2ww+jxm6eJ3HbWhJoAAEEpAP91hFqmcb2ZqVcaRDMSVmhkEcFIRmpH" + + "vDoQtVn8sArWqwEAi8HwbMhL+YwRItRZDknpC4vFjTHVMd1zMrz/JyeuT9k=" + ); + + // v6 Ed25519/X25519 key from crypto-refresh // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-secret-key-transf private static readonly byte[] v6Certificate = Base64.Decode( @@ -67,7 +252,7 @@ public class PgpInteroperabilityTestSuite "M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr" + "k0mXubZvyl4GBg=="); - // v5 keys from "OpenPGP interoperability test suite" + // v5 key "Emma" from "OpenPGP interoperability test suite" // https://tests.sequoia-pgp.org/#Inline_Sign-Verify_roundtrip_with_key__Emma_ private static readonly byte[] v5Certificate = Base64.Decode( "mDcFXJH05BYAAAAtCSsGAQQB2kcPAQEHQFhZlVcVVtwf+21xNQPX+ecMJJBL0MPd" + @@ -506,12 +691,119 @@ public void Version5InlineSignatureTest() IsTrue(ops.Verify(sigs[0], metadata)); } + [Test] + public void SeipdVersion2WithMultipleMethodsTest() + { + // Encrypt-Decrypt roundtrip V6 SKESK/PKESK, V2 SEIPD, AES-256 in EAX mode + // multiple different passwords and public keys with different algorithms + // and S2K schemes + byte[] largePlaintext = new byte[50000]; + Arrays.Fill(largePlaintext, (byte)'A'); + byte[] enc; + + byte[][] passwords = new byte[][] + { + Encoding.UTF8.GetBytes("password"), + Encoding.UTF8.GetBytes("drowssap"), + Encoding.UTF8.GetBytes("P@ssw0rd") + }; + + var alice = new PgpPublicKeyRing(alicePubkey).GetPublicKeys().First(k => k.IsEncryptionKey); + var bob = new PgpPublicKeyRing(bobPubkey).GetPublicKeys().First(k => k.IsEncryptionKey); + var v6 = new PgpPublicKeyRing(v6Certificate).GetPublicKeys().First(k => k.IsEncryptionKey); + var carol = new PgpPublicKeyRing(carolPubkey).GetPublicKeys().First(k => k.IsEncryptionKey); + + PgpPrivateKey[] privateKeys = new PgpPrivateKey[] + { + new PgpSecretKeyRing(aliceSecretkey).GetSecretKey(alice.KeyId).ExtractPrivateKey(emptyPassphrase), + new PgpSecretKeyRing(bobSecretkey).GetSecretKey(bob.KeyId).ExtractPrivateKey(emptyPassphrase), + new PgpSecretKeyRing(v6UnlockedSecretKey).GetSecretKey(v6.KeyId).ExtractPrivateKey(emptyPassphrase) + }; + + var gen = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Aes256, AeadAlgorithmTag.Eax); + gen.AddMethodRaw(passwords[0], S2k.Argon2Parameters.UniversallyRecommendedParameters()); + gen.AddMethodRaw(passwords[1], S2k.Argon2Parameters.MemoryConstrainedParameters()); + gen.AddMethodRaw(passwords[2], HashAlgorithmTag.Sha256); + gen.AddMethod(alice); + gen.AddMethod(bob); + gen.AddMethod(v6); + + // Check constraint: An implementation MUST NOT generate ElGamal v6 PKESKs. + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-fo + Assert.Throws(() => + { + gen.AddMethod(carol); + }); + + using (MemoryStream ms = new MemoryStream()) + { + byte[] buffer = new byte[3000]; + using (Stream cOut = gen.Open(ms, buffer)) + { + using (BcpgOutputStream bcOut = new BcpgOutputStream(cOut, newFormatOnly: true)) + { + PgpLiteralDataGenerator literalDataGen = new PgpLiteralDataGenerator(); + DateTime modificationTime = DateTime.UtcNow; + + using (Stream lOut = literalDataGen.Open( + new UncloseableStream(bcOut), + PgpLiteralData.Utf8, + PgpLiteralData.Console, + largePlaintext.Length, + modificationTime)) + { + lOut.Write(largePlaintext, 0, largePlaintext.Length); + } + } + } + enc = ms.ToArray(); + } + + // decrypt + for (int i = 0; i < passwords.Length; i++) + { + PgpObjectFactory factory = new PgpObjectFactory(enc); + PgpEncryptedDataList encDataList = factory.NextPgpObject() as PgpEncryptedDataList; + PgpPbeEncryptedData encData = encDataList[i] as PgpPbeEncryptedData; + using (Stream stream = encData.GetDataStreamRaw(passwords[i])) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (MemoryStream ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + byte[] decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(largePlaintext, decrypted)); + } + } + } + + for (int i = 0; i < privateKeys.Length; i++) + { + PgpObjectFactory factory = new PgpObjectFactory(enc); + PgpEncryptedDataList encDataList = factory.NextPgpObject() as PgpEncryptedDataList; + PgpPublicKeyEncryptedData encData = encDataList[3+i] as PgpPublicKeyEncryptedData; + + using (Stream stream = encData.GetDataStream(privateKeys[i])) + { + factory = new PgpObjectFactory(stream); + PgpLiteralData lit = factory.NextPgpObject() as PgpLiteralData; + using (MemoryStream ms = new MemoryStream()) + { + lit.GetDataStream().CopyTo(ms); + byte[] decrypted = ms.ToArray(); + IsTrue(Arrays.AreEqual(largePlaintext, decrypted)); + } + } + } + } public override string Name => "PgpInteroperabilityTestSuite"; public override void PerformTest() { MultiplePkeskTest(); + SeipdVersion2WithMultipleMethodsTest(); MultipleInlineSignatureTest(); GenerateAndVerifyMultipleInlineSignatureTest(); From bdbb66cf50e59a3b6e48d4fbe21006751f63d4c9 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Fri, 16 Aug 2024 17:50:52 +0200 Subject: [PATCH 28/37] Update references to "crypto-refresh" to point to RFC 9580. --- crypto/src/bcpg/ArmoredOutputStream.cs | 4 +- crypto/src/bcpg/Ed25519PublicBCPGKey.cs | 2 +- crypto/src/bcpg/Ed25519SecretBCPGKey.cs | 2 +- crypto/src/bcpg/Ed448PublicBCPGKey.cs | 2 +- crypto/src/bcpg/Ed448SecretBCPGKey.cs | 2 +- crypto/src/bcpg/OnePassSignaturePacket.cs | 4 +- crypto/src/bcpg/Packet.cs | 2 +- crypto/src/bcpg/PacketTags.cs | 2 +- crypto/src/bcpg/PublicKeyAlgorithmTags.cs | 4 +- crypto/src/bcpg/PublicKeyEncSessionPacket.cs | 8 +- crypto/src/bcpg/S2k.cs | 4 +- crypto/src/bcpg/SignaturePacket.cs | 12 +-- crypto/src/bcpg/SignatureSubpacketTags.cs | 2 +- .../src/bcpg/SymmetricEncIntegrityPacket.cs | 4 +- .../src/bcpg/SymmetricKeyEncSessionPacket.cs | 2 +- crypto/src/bcpg/X25519PublicBCPGKey.cs | 2 +- crypto/src/bcpg/X25519SecretBCPGKey.cs | 2 +- crypto/src/bcpg/X448PublicBCPGKey.cs | 2 +- crypto/src/bcpg/X448SecretBCPGKey.cs | 2 +- .../src/openpgp/PgpEncryptedDataGenerator.cs | 2 +- crypto/src/openpgp/PgpKeyRingGenerator.cs | 5 +- crypto/src/openpgp/PgpOnePassSignature.cs | 2 +- crypto/src/openpgp/PgpPbeEncryptedData.cs | 4 +- .../src/openpgp/PgpPublicKeyEncryptedData.cs | 12 +-- crypto/src/openpgp/PgpSecretKey.cs | 5 +- crypto/src/openpgp/PgpSignatureGenerator.cs | 20 ++-- crypto/src/openpgp/PgpUtilities.cs | 2 +- .../test/data/openpgp/big-pkesk-aead-msg.asc | 2 +- .../src/openpgp/test/PgpCryptoRefreshTest.cs | 96 +++++++++++++------ .../test/PgpInteroperabilityTestSuite.cs | 20 ++-- 30 files changed, 139 insertions(+), 95 deletions(-) diff --git a/crypto/src/bcpg/ArmoredOutputStream.cs b/crypto/src/bcpg/ArmoredOutputStream.cs index f39e5095b..b27d54008 100644 --- a/crypto/src/bcpg/ArmoredOutputStream.cs +++ b/crypto/src/bcpg/ArmoredOutputStream.cs @@ -263,7 +263,7 @@ public void BeginClearText() /// This header is deprecated and SHOULD NOT be emitted unless: /// * The cleartext signed message contains a v4 signature made using a SHA2-based digest(SHA224, SHA256, SHA384, or SHA512), and /// * The cleartext signed message might be verified by a legacy OpenPGP implementation that requires this header. - /// + /// /// /// /// @@ -373,7 +373,7 @@ public override void WriteByte(byte value) DoWrite(headerStart + type + headerTail + NewLine); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-armor-header + // https://www.rfc-editor.org/rfc/rfc9580#name-version-armor-header // To minimize metadata, implementations SHOULD NOT emit this key and its corresponding value except // for debugging purposes with explicit user consent. if (showVersion && m_headers.TryGetValue(HeaderVersion, out var versionHeaders)) diff --git a/crypto/src/bcpg/Ed25519PublicBCPGKey.cs b/crypto/src/bcpg/Ed25519PublicBCPGKey.cs index 802657909..8c23d8218 100644 --- a/crypto/src/bcpg/Ed25519PublicBCPGKey.cs +++ b/crypto/src/bcpg/Ed25519PublicBCPGKey.cs @@ -3,7 +3,7 @@ public sealed class Ed25519PublicBcpgKey : OctetArrayBcpgKey { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-ed2 + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-part-for-ed2 public const int length = 32; public Ed25519PublicBcpgKey(BcpgInputStream bcpgIn) diff --git a/crypto/src/bcpg/Ed25519SecretBCPGKey.cs b/crypto/src/bcpg/Ed25519SecretBCPGKey.cs index e161707e9..67e549738 100644 --- a/crypto/src/bcpg/Ed25519SecretBCPGKey.cs +++ b/crypto/src/bcpg/Ed25519SecretBCPGKey.cs @@ -3,7 +3,7 @@ public sealed class Ed25519SecretBcpgKey : OctetArrayBcpgKey { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-ed2 + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-part-for-ed2 public const int length = 32; public Ed25519SecretBcpgKey(BcpgInputStream bcpgIn) diff --git a/crypto/src/bcpg/Ed448PublicBCPGKey.cs b/crypto/src/bcpg/Ed448PublicBCPGKey.cs index fc0283969..102fbf7a5 100644 --- a/crypto/src/bcpg/Ed448PublicBCPGKey.cs +++ b/crypto/src/bcpg/Ed448PublicBCPGKey.cs @@ -3,7 +3,7 @@ public sealed class Ed448PublicBcpgKey : OctetArrayBcpgKey { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-ed4 + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-part-for-ed4 public const int length = 57; public Ed448PublicBcpgKey(BcpgInputStream bcpgIn) diff --git a/crypto/src/bcpg/Ed448SecretBCPGKey.cs b/crypto/src/bcpg/Ed448SecretBCPGKey.cs index 3b355ceb9..489692cf3 100644 --- a/crypto/src/bcpg/Ed448SecretBCPGKey.cs +++ b/crypto/src/bcpg/Ed448SecretBCPGKey.cs @@ -3,7 +3,7 @@ public sealed class Ed448SecretBcpgKey : OctetArrayBcpgKey { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-ed4 + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-part-for-ed4 public const int length = 57; public Ed448SecretBcpgKey(BcpgInputStream bcpgIn) diff --git a/crypto/src/bcpg/OnePassSignaturePacket.cs b/crypto/src/bcpg/OnePassSignaturePacket.cs index cde5501b4..3f22a8f16 100644 --- a/crypto/src/bcpg/OnePassSignaturePacket.cs +++ b/crypto/src/bcpg/OnePassSignaturePacket.cs @@ -29,9 +29,9 @@ private void EnforceConstraints() if (version == Version6 && salt.Length != expectedSaltSize) { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-one-pass-signature-packet-t + // https://www.rfc-editor.org/rfc/rfc9580#name-one-pass-signature-packet-t // The salt size MUST match the value defined for the hash algorithm as specified in Table 23 - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#hash-algorithms-registry + // https://www.rfc-editor.org/rfc/rfc9580#hash-algorithms-registry throw new IOException($"invalid salt size for v6 signature: expected {expectedSaltSize} got {salt.Length}"); } } diff --git a/crypto/src/bcpg/Packet.cs b/crypto/src/bcpg/Packet.cs index 6c8b95b6d..ffca0bf93 100644 --- a/crypto/src/bcpg/Packet.cs +++ b/crypto/src/bcpg/Packet.cs @@ -28,7 +28,7 @@ public Packet(PacketTag packetTag) /// * Packets with tags less or equal to 39 are critical. /// * Tags 40 to 59 are reserved for unassigned, non-critical packets. /// * Tags 60 to 63 are non-critical private or experimental packets. - /// + /// /// public bool IsCritical => (int)Tag <= 39; } diff --git a/crypto/src/bcpg/PacketTags.cs b/crypto/src/bcpg/PacketTags.cs index d6c2fc075..43590e5f9 100644 --- a/crypto/src/bcpg/PacketTags.cs +++ b/crypto/src/bcpg/PacketTags.cs @@ -24,7 +24,7 @@ public enum PacketTag ModificationDetectionCode = 19, // Reserved (formerly Modification Detection Code Packet) ReservedAeadEncryptedData = 20, // Reserved (defined as AEAD Encrypted Data Packet in retired draft [draft-koch-openpgp-rfc4880bis]) - Padding = 21, // Padding Packet [https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#padding-packet] + Padding = 21, // Padding Packet (https://www.rfc-editor.org/rfc/rfc9580#padding-packet) Experimental1 = 60, // Private or Experimental Values Experimental2 = 61, diff --git a/crypto/src/bcpg/PublicKeyAlgorithmTags.cs b/crypto/src/bcpg/PublicKeyAlgorithmTags.cs index a88a84c0d..07ac3c260 100644 --- a/crypto/src/bcpg/PublicKeyAlgorithmTags.cs +++ b/crypto/src/bcpg/PublicKeyAlgorithmTags.cs @@ -21,11 +21,11 @@ public enum PublicKeyAlgorithmTag EdDsa = 22, // EdDSA - (internet draft, but appearing in use) EdDsa_Legacy = 22, // new name for old EdDSA tag. - // defined as Reserved by crypto-refresh draft + // defined as Reserved by RFC 9580 AEDH = 23, AEDSA = 24, - // https://datatracker.ietf.org/doc/draft-ietf-openpgp-crypto-refresh/ + // https://www.rfc-editor.org/rfc/rfc9580 X25519 = 25, X448 = 26, Ed25519 = 27, diff --git a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs index 2a23bc3f3..20a980504 100644 --- a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs @@ -22,13 +22,13 @@ public class PublicKeyEncSessionPacket /// /// Version 3 PKESK packet. /// - /// + /// public const int Version3 = 3; /// /// Version 6 PKESK packet. /// - /// + /// public const int Version6 = 6; internal PublicKeyEncSessionPacket( @@ -103,8 +103,8 @@ internal PublicKeyEncSessionPacket( break; case PublicKeyAlgorithmTag.X25519: case PublicKeyAlgorithmTag.X448: - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for- - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for-x + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-fields-for- + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-fields-for-x // 32 (for X25519) or 56 (for X448) octets representing an ephemeral public key. int keylen = algorithm == PublicKeyAlgorithmTag.X25519 ? 32 : 56; diff --git a/crypto/src/bcpg/S2k.cs b/crypto/src/bcpg/S2k.cs index 05adc530b..027413820 100644 --- a/crypto/src/bcpg/S2k.cs +++ b/crypto/src/bcpg/S2k.cs @@ -225,7 +225,7 @@ public override void Encode( /// /// Parameters for Argon2 S2K - /// Sect. 3.7.1.4 of crypto-refreshsee> + /// Sect. 3.7.1.4 of RFC 9580 /// public class Argon2Parameters { @@ -304,7 +304,7 @@ public Argon2Parameters(byte[] salt, int passes, int parallelism, int memSizeExp // log_2(p) = log_e(p) / log_e(2) double log2_p = System.Math.Log(parallelism) / System.Math.Log(2); - // see https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-argon2 + // see https://www.rfc-editor.org/rfc/rfc9580#name-argon2 if (memSizeExp < (3 + System.Math.Ceiling(log2_p)) || memSizeExp > 31) { throw new ArgumentException("Memory size exponent MUST be between 3+ceil(log_2(parallelism)) and 31"); diff --git a/crypto/src/bcpg/SignaturePacket.cs b/crypto/src/bcpg/SignaturePacket.cs index 5af884387..1f364fdf1 100644 --- a/crypto/src/bcpg/SignaturePacket.cs +++ b/crypto/src/bcpg/SignaturePacket.cs @@ -59,8 +59,8 @@ private void CheckIssuerSubpacket(SignatureSubpacket p) else if (p is IssuerKeyId issuerKeyId && !keyIdAlreadySet) { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-issuer-key-id - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#issuer-fingerprint-subpacket + // https://www.rfc-editor.org/rfc/rfc9580#name-issuer-key-id + // https://www.rfc-editor.org/rfc/rfc9580#issuer-fingerprint-subpacket // V6 signatures MUST NOT include an IssuerKeyId subpacket and SHOULD include an IssuerFingerprint subpacket if (version == Version6) { @@ -197,9 +197,9 @@ internal SignaturePacket(BcpgInputStream bcpgIn) if (saltSize != PgpUtilities.GetSaltSize(hashAlgorithm)) { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-4-and-6-signature-p + // https://www.rfc-editor.org/rfc/rfc9580#name-versions-4-and-6-signature- // The salt size MUST match the value defined for the hash algorithm as specified in Table 23 - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#hash-algorithms-registry + // https://www.rfc-editor.org/rfc/rfc9580#hash-algorithms-registry throw new IOException($"invalid salt size for v6 signature: expected {PgpUtilities.GetSaltSize(hashAlgorithm)} got {saltSize}"); } @@ -235,13 +235,13 @@ internal SignaturePacket(BcpgInputStream bcpgIn) break; case PublicKeyAlgorithmTag.Ed25519: - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for-ed2 + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-fields-for-ed2 signature = null; signatureEncoding = new byte[64]; bcpgIn.ReadFully(signatureEncoding); break; case PublicKeyAlgorithmTag.Ed448: - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for-ed4 + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-fields-for-ed4 signature = null; signatureEncoding = new byte[114]; bcpgIn.ReadFully(signatureEncoding); diff --git a/crypto/src/bcpg/SignatureSubpacketTags.cs b/crypto/src/bcpg/SignatureSubpacketTags.cs index 0574c274b..082d66839 100644 --- a/crypto/src/bcpg/SignatureSubpacketTags.cs +++ b/crypto/src/bcpg/SignatureSubpacketTags.cs @@ -30,7 +30,7 @@ public enum SignatureSubpacketTag SignatureTarget = 31, // signature target EmbeddedSignature = 32, // embedded signature IssuerFingerprint = 33, // issuer key fingerprint - //PreferredAeadAlgorithms = 34, // RESERVED since crypto-refresh-05 + //PreferredAeadAlgorithms = 34, // RESERVED since RFC 9580 IntendedRecipientFingerprint = 35, // intended recipient fingerprint AttestedCertifications = 37, // attested certifications (RESERVED) KeyBlock = 38, // Key Block (RESERVED) diff --git a/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs b/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs index 6692851be..fbb08b694 100644 --- a/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs +++ b/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs @@ -10,12 +10,12 @@ public class SymmetricEncIntegrityPacket /// /// Version 3 SEIPD packet. /// - /// + /// public const int Version1 = 1; /// /// Version 2 SEIPD packet. /// - /// + /// public const int Version2 = 2; private readonly int version; // V1, V2 diff --git a/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs b/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs index 224ca8243..cb5535733 100644 --- a/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs @@ -56,7 +56,7 @@ public SymmetricKeyEncSessionPacket( case Version5: case Version6: - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-6-symmetric-key-enc + // https://www.rfc-editor.org/rfc/rfc9580#name-version-6-symmetric-key-enc // SymmAlgo + AEADAlgo + S2KCount + S2K + IV int next5Fields5Count = bcpgIn.ReadByte(); encAlgorithm = (SymmetricKeyAlgorithmTag)bcpgIn.ReadByte(); diff --git a/crypto/src/bcpg/X25519PublicBCPGKey.cs b/crypto/src/bcpg/X25519PublicBCPGKey.cs index b8eb52a4c..edb8f67e8 100644 --- a/crypto/src/bcpg/X25519PublicBCPGKey.cs +++ b/crypto/src/bcpg/X25519PublicBCPGKey.cs @@ -3,7 +3,7 @@ public sealed class X25519PublicBcpgKey : OctetArrayBcpgKey { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-x + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-part-for-x public const int length = 32; public X25519PublicBcpgKey(BcpgInputStream bcpgIn) diff --git a/crypto/src/bcpg/X25519SecretBCPGKey.cs b/crypto/src/bcpg/X25519SecretBCPGKey.cs index 3da6f01b6..cbdf8abd5 100644 --- a/crypto/src/bcpg/X25519SecretBCPGKey.cs +++ b/crypto/src/bcpg/X25519SecretBCPGKey.cs @@ -3,7 +3,7 @@ public sealed class X25519SecretBcpgKey : OctetArrayBcpgKey { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-x + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-part-for-x public const int length = 32; public X25519SecretBcpgKey(BcpgInputStream bcpgIn) diff --git a/crypto/src/bcpg/X448PublicBCPGKey.cs b/crypto/src/bcpg/X448PublicBCPGKey.cs index 9b35cbd3b..c6f370bb1 100644 --- a/crypto/src/bcpg/X448PublicBCPGKey.cs +++ b/crypto/src/bcpg/X448PublicBCPGKey.cs @@ -3,7 +3,7 @@ public sealed class X448PublicBcpgKey : OctetArrayBcpgKey { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-x4 + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-part-for-x4 public const int length = 56; public X448PublicBcpgKey(BcpgInputStream bcpgIn) diff --git a/crypto/src/bcpg/X448SecretBCPGKey.cs b/crypto/src/bcpg/X448SecretBCPGKey.cs index a8ce5e809..16bfab2f5 100644 --- a/crypto/src/bcpg/X448SecretBCPGKey.cs +++ b/crypto/src/bcpg/X448SecretBCPGKey.cs @@ -3,7 +3,7 @@ public sealed class X448SecretBcpgKey : OctetArrayBcpgKey { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-part-for-x4 + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-part-for-x4 public const int length = 56; public X448SecretBcpgKey(BcpgInputStream bcpgIn) diff --git a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs index a80578549..2c5546953 100644 --- a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs +++ b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs @@ -655,7 +655,7 @@ public void AddMethod(PgpPublicKey key, bool sessionKeyObfuscation) if (pkeskVersion == PublicKeyEncSessionPacket.Version6 && (key.Algorithm == PublicKeyAlgorithmTag.ElGamalEncrypt || key.Algorithm == PublicKeyAlgorithmTag.ElGamalGeneral)) { - throw new PgpException("cannot generate ElGamal v6 PKESK (see https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-fo)"); + throw new PgpException("cannot generate ElGamal v6 PKESK (see https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-fields-fo)"); } methods.Add(new PubMethod(key, sessionKeyObfuscation, pkeskVersion)); diff --git a/crypto/src/openpgp/PgpKeyRingGenerator.cs b/crypto/src/openpgp/PgpKeyRingGenerator.cs index 9f9bbd6ff..6ef7af816 100644 --- a/crypto/src/openpgp/PgpKeyRingGenerator.cs +++ b/crypto/src/openpgp/PgpKeyRingGenerator.cs @@ -272,7 +272,8 @@ public void AddSubKey( PgpSignatureSubpacketVector hashedPackets, PgpSignatureSubpacketVector unhashedPackets) { - AddSubKey(keyPair, hashedPackets, unhashedPackets, HashAlgorithmTag.Sha1); + AddSubKey(keyPair, hashedPackets, unhashedPackets, + keyPair.PublicKey.Version > PublicKeyPacket.Version4 ? HashAlgorithmTag.Sha256 : HashAlgorithmTag.Sha1); } /// @@ -397,7 +398,7 @@ public PgpPublicKeyRing GeneratePublicKeyRing() pgpSecretKey = enumerator.Current; PgpPublicKey k = new PgpPublicKey(pgpSecretKey.PublicKey); - k.publicPk = new PublicSubkeyPacket(k.Algorithm, k.CreationTime, k.publicPk.Key); + k.publicPk = new PublicSubkeyPacket(k.Version, k.Algorithm, k.CreationTime, k.publicPk.Key); pubKeys.Add(k); } diff --git a/crypto/src/openpgp/PgpOnePassSignature.cs b/crypto/src/openpgp/PgpOnePassSignature.cs index 704cc7bd9..c7505b072 100644 --- a/crypto/src/openpgp/PgpOnePassSignature.cs +++ b/crypto/src/openpgp/PgpOnePassSignature.cs @@ -155,7 +155,7 @@ public bool Verify(PgpSignature pgpSig) public bool Verify(PgpSignature pgpSig, byte[] additionalMetadata) { // the versions of the Signature and the One-Pass Signature must be aligned as specified in - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#signed-message-versions + // https://www.rfc-editor.org/rfc/rfc9580#signed-message-versions if (pgpSig.Version == SignaturePacket.Version6 && sigPack.Version != OnePassSignaturePacket.Version6) { return false; diff --git a/crypto/src/openpgp/PgpPbeEncryptedData.cs b/crypto/src/openpgp/PgpPbeEncryptedData.cs index 8ba9b4bdf..c4bca5b20 100644 --- a/crypto/src/openpgp/PgpPbeEncryptedData.cs +++ b/crypto/src/openpgp/PgpPbeEncryptedData.cs @@ -29,7 +29,7 @@ private void EnforceConstraints() switch (keyData.Version) { case SymmetricKeyEncSessionPacket.Version4: - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-4-symmetric-key-enc + // https://www.rfc-editor.org/rfc/rfc9580#name-version-4-symmetric-key-enc // A version 4 SKESK packet precedes a version 1 SEIPD packet. In historic data, it is sometimes found // preceding a deprecated SED packet. A v4 SKESK packet MUST NOT precede a v2 SEIPD packet. if (encData is SymmetricEncDataPacket) @@ -54,7 +54,7 @@ private void EnforceConstraints() break; case SymmetricKeyEncSessionPacket.Version6: - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-6-symmetric-key-enc + // https://www.rfc-editor.org/rfc/rfc9580#name-version-6-symmetric-key-enc // A version 6 SKESK packet precedes a version 2 SEIPD packet. A v6 SKESK packet MUST NOT precede a v1 SEIPD // packet or a deprecated Symmetrically Encrypted Data. if (encData is SymmetricEncDataPacket) diff --git a/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs b/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs index 64d02ebfe..402351bda 100644 --- a/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs +++ b/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs @@ -36,7 +36,7 @@ private void EnforceConstraints() switch (keyData.Version) { case PublicKeyEncSessionPacket.Version3: - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-3-public-key-encryp + // https://www.rfc-editor.org/rfc/rfc9580#name-version-3-public-key-encryp // A version 3 PKESK packet precedes a version 1 SEIPD packet. In historic data, it is sometimes // found preceding a deprecated SED packet. // A V3 PKESK packet MUST NOT precede a V2 SEIPD packet. @@ -55,7 +55,7 @@ private void EnforceConstraints() break; case PublicKeyEncSessionPacket.Version6: - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-version-6-public-key-encryp + // https://www.rfc-editor.org/rfc/rfc9580#name-version-6-public-key-encryp //A version 6 PKESK packet precedes a version 2 SEIPD packet. //A V6 PKESK packet MUST NOT precede a V1 SEIPD packet or a deprecated SED packet. if (encData is SymmetricEncDataPacket) @@ -143,7 +143,7 @@ public SymmetricKeyAlgorithmTag GetSymmetricAlgorithm( { if (keyData.Version == PublicKeyEncSessionPacket.Version3) { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for- + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-fields-for- // In V3 PKESK, the symmetric algorithm Id // * with X25519 and X448 is not encrypted, it's prepended in plaintext // to the encrypted session key. @@ -348,10 +348,10 @@ private byte[] RecoverSessionData(PgpPrivateKey privKey) if (keyData.Algorithm == PublicKeyAlgorithmTag.X25519 || keyData.Algorithm == PublicKeyAlgorithmTag.X448) { - // See sect. 5.1.6. and 5.1.7 of crypto-refresh for the description of + // See sect. 5.1.6. and 5.1.7 of RFC 9580 for the description of // the key derivation algorithm for X25519 and X448 - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for- - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-for-x + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-fields-for- + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-fields-for-x byte[] eph = secKeyData[0]; byte[] esk = secKeyData[1]; diff --git a/crypto/src/openpgp/PgpSecretKey.cs b/crypto/src/openpgp/PgpSecretKey.cs index 7ecad0b99..57b360948 100644 --- a/crypto/src/openpgp/PgpSecretKey.cs +++ b/crypto/src/openpgp/PgpSecretKey.cs @@ -158,7 +158,7 @@ internal PgpSecretKey( s2kUsage = SecretKeyPacket.UsageNone; } - // RFC 4880 § 5.5.3 + "RFC 4880bis" § 5.5.3 + Crypto-refresh § 5.5.3. + // RFC 4880 § 5.5.3 + "RFC 4880bis" § 5.5.3 + RFC 9580 § 5.5.3. // v6 keys with UsageNone: No checksum // v5 v6 keys with MalleableCFB: Not allowed // v3 v4 v5 keys with UsageNone: two-octet checksum @@ -277,7 +277,8 @@ private static PgpPublicKey CertifiedPublicKey( PgpSignatureSubpacketVector unhashedPackets, SecureRandom rand) { - return CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets, HashAlgorithmTag.Sha1, rand); + return CertifiedPublicKey(certificationLevel, keyPair, id, hashedPackets, unhashedPackets, + keyPair.PublicKey.Version > PublicKeyPacket.Version4 ? HashAlgorithmTag.Sha256 : HashAlgorithmTag.Sha1, rand); } diff --git a/crypto/src/openpgp/PgpSignatureGenerator.cs b/crypto/src/openpgp/PgpSignatureGenerator.cs index dbfe93e6f..b5c71ff94 100644 --- a/crypto/src/openpgp/PgpSignatureGenerator.cs +++ b/crypto/src/openpgp/PgpSignatureGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.IO; using Org.BouncyCastle.Bcpg.Sig; @@ -52,14 +53,21 @@ public void InitSign(int sigType, PgpPrivateKey privKey) /// Initialise the generator for signing. public void InitSign(int sigType, PgpPrivateKey privKey, SecureRandom random) { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-signature-packet-type-id-2 + // https://www.rfc-editor.org/rfc/rfc9580#name-signature-packet-type-id-2 // An implementation MUST generate a version 6 signature when signing with a version 6 key. - // An implementation MUST generate a version 4 signature when signing with a version 4 key. + // An implementation MUST generate a version 4 signature when signing with a version 4 key. this.version = privKey.Version; this.privKey = privKey; this.signatureType = sigType; - AsymmetricKeyParameter key = privKey.Key; + // https://www.rfc-editor.org/rfc/rfc9580#name-hash-algorithms + // avoid V6 signatures with weak hash algorithms + if (this.version > SignaturePacket.Version4 && (hashAlgorithm == HashAlgorithmTag.MD5 || hashAlgorithm == HashAlgorithmTag.Sha1 || hashAlgorithm == HashAlgorithmTag.RipeMD160)) + { + throw new PgpException("avoid V6 signatures with weak hash algorithms"); + } + + AsymmetricKeyParameter key = privKey.Key; this.sig = PgpUtilities.CreateSigner(keyAlgorithm, hashAlgorithm, key); @@ -216,7 +224,7 @@ public PgpOnePassSignature GenerateOnePassVersion( OnePassSignaturePacket opsPkt; // the versions of the Signature and the One-Pass Signature must be aligned as specified in - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#signed-message-versions + // https://www.rfc-editor.org/rfc/rfc9580#signed-message-versions if (version == SignaturePacket.Version6) { opsPkt = new OnePassSignaturePacket( @@ -242,8 +250,8 @@ public PgpSignature Generate() hPkts = InsertSubpacket(hPkts, new SignatureCreationTime(false, DateTime.UtcNow)); } - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-issuer-key-id - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#issuer-fingerprint-subpacket + // https://www.rfc-editor.org/rfc/rfc9580#name-issuer-key-id + // https://www.rfc-editor.org/rfc/rfc9580#issuer-fingerprint-subpacket bool containsIssuerKeyId = IsPacketPresent(hashed, SignatureSubpacketTag.IssuerKeyId) || IsPacketPresent(unhashed, SignatureSubpacketTag.IssuerKeyId); bool containsIssuerKeyFpr = IsPacketPresent(hashed, SignatureSubpacketTag.IssuerFingerprint) || IsPacketPresent(unhashed, SignatureSubpacketTag.IssuerFingerprint); switch (version) diff --git a/crypto/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index 620724008..477e9e906 100644 --- a/crypto/src/openpgp/PgpUtilities.cs +++ b/crypto/src/openpgp/PgpUtilities.cs @@ -118,7 +118,7 @@ public static string GetDigestName( /// /// Returns the V6 signature salt size for a hash algorithm. - /// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-hash-algorithms + /// https://www.rfc-editor.org/rfc/rfc9580#name-hash-algorithms /// public static int GetSaltSize(HashAlgorithmTag hashAlgorithm) { diff --git a/crypto/test/data/openpgp/big-pkesk-aead-msg.asc b/crypto/test/data/openpgp/big-pkesk-aead-msg.asc index c481452ad..7380c636e 100644 --- a/crypto/test/data/openpgp/big-pkesk-aead-msg.asc +++ b/crypto/test/data/openpgp/big-pkesk-aead-msg.asc @@ -2,7 +2,7 @@ Comment: V6 PKESK + V2 SEIPD AEAD encrypted message that spans Comment: over 4 chunks (chunk size 512 octets) Comment: 2000 octets of /dev/zero encrypted with sample V6 -Comment: certificate from crypto-refresh Appendix A.3 +Comment: certificate from RFC 9580 Appendix A.3 Comment: generated with gosop 2.0.0-alpha Comment: Session key CFB73D46CF7C13B7535227BEDB5B2D8B4023C5B... Comment: Session key ...58289D19CF2C33B0DB388B0B6 diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 053d08f34..7677a2c27 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -17,17 +17,17 @@ namespace Org.BouncyCastle.Bcpg.OpenPgp.Tests public class PgpCryptoRefreshTest : SimpleTest { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-key + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-4-ed25519leg private readonly byte[] v4Ed25519LegacyPubkeySample = Base64.Decode( "xjMEU/NfCxYJKwYBBAHaRw8BAQdAPwmJlL3ZFu1AUxl5NOSofIBzOhKA1i+AEJku" + "Q+47JAY="); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-sig + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-4-ed25519lega private readonly byte[] v4Ed25519LegacySignatureSample = Base64.Decode( "iF4EABYIAAYFAlX5X5UACgkQjP3hIZeWWpr2IgD/VvkMypjiECY3vZg/2xbBMd/S" + "ftgr9N3lYG4NdWrtM2YBANCcT6EVJ/A44PV/IgHYLy6iyQMyZfps60iehUuuYbQE"); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-6-certificat private readonly byte[] v6Certificate = Base64.Decode( "xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf" + "GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy" + @@ -39,7 +39,7 @@ public class PgpCryptoRefreshTest "j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805" + "I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg=="); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-secret-key-transf + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-6-secret-key private readonly byte[] v6UnlockedSecretKey = Base64.Decode( "xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB" + "exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ" + @@ -53,7 +53,7 @@ public class PgpCryptoRefreshTest "M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr" + "k0mXubZvyl4GBg=="); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-locked-v6-secret-key + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-locked-version-6-sec private readonly byte[] v6LockedSecretKey = Base64.Decode( "xYIGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP9JgkC" + "FARdb9ccngltHraRe25uHuyuAQQVtKipJ0+r5jL4dacGWSAheCWPpITYiyfyIOPS" + @@ -69,7 +69,7 @@ public class PgpCryptoRefreshTest "Nacp8DkBClZRa2c3AMQzSDXa9jGhYzxjzVb5scHDzTkjyRZWRdTq8U6L4da+/+Kt" + "ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG"); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-cleartext-signed-mes + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-cleartext-signed-mes private readonly string v6SampleCleartextSignedMessage = "What we need from the grocery store:\r\n\r\n- tofu\r\n- vegetables\r\n- noodles\r\n"; private readonly byte[] v6SampleCleartextSignedMessageSignature = Base64.Decode( "wpgGARsKAAAAKQWCY5ijYyIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6" + @@ -77,7 +77,7 @@ public class PgpCryptoRefreshTest "/FvLFuGWMbKAdA+epq7V4HOtAPlBWmU8QOd6aud+aSunHQaaEJ+iTFjP2OMW0KBr" + "NK2ay45cX1IVAQ=="); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-inline-signed-messag + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-inline-signed-messag private readonly byte[] v6SampleInlineSignedMessage = Base64.Decode( "xEYGAQobIHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usyxhsTwYJppfk" + "1S36bHIrDB8eJ8GKVnCPZSXsJ7rZrMkBy0p1AAAAAABXaGF0IHdlIG5lZWQgZnJv" + @@ -88,7 +88,7 @@ public class PgpCryptoRefreshTest "FtCgazStmsuOXF9SFQE="); // Sample AEAD encryption and decryption - V6 SKESK + V2 SEIPD - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-aead-eax-encryption- + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-aead-eax-encryption- // encrypts the cleartext string Hello, world! with the passphrase password, S2K type iterated+salted, // using AES-128 with AEAD-EAX encryption. private readonly byte[] v6skesk_aes128_eax = Base64.Decode( @@ -97,7 +97,7 @@ public class PgpCryptoRefreshTest "pJ8EwuZ0F11KPSJu1q/LnKmsEiwUcOEcY9TAqyQcapOK1Iv5mlqZuQu6gyXeYQR1" + "QCWKt5Wala0FHdqW6xVDHf719eIlXKeCYVRuM5o="); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-aead-ocb-encryption- + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-aead-ocb-encryption- // encrypts the cleartext string Hello, world! with the passphrase password, S2K type iterated+salted, // using AES-128 with AEAD-OCB encryption. private readonly byte[] v6skesk_aes128_ocb = Base64.Decode( @@ -106,7 +106,7 @@ public class PgpCryptoRefreshTest "WRDQns3WQf+f04VidYA1vEl1TOG/P/+n2tCjuBBPUTPPQqQQCoPu9MobSAGohGv0" + "K82nyM6dZeIS8wHLzZj9yt5pSod61CRzI/boVw=="); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-aead-gcm-encryption- + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-aead-gcm-encryption- // encrypts the cleartext string Hello, world! with the passphrase password, S2K type iterated+salted, // using AES-128 with AEAD-GCM encryption. private readonly byte[] v6skesk_aes128_gcm = Base64.Decode( @@ -115,7 +115,7 @@ public class PgpCryptoRefreshTest "Ae0Pn/xvxtZbv9JNzQeQlm5tHoWjAFN4TLHYtqBpnvEhVaeyrWJYUxtXZR/Xd3kS" + "+pXjXZtAIW9ppMJI2yj/QzHxYykHOZ5v+Q=="); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-messages-encrypted-u + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-messages-encrypted-u // V4 SKESK + V1 SEIPD using Argon2 (t=1, p=4, m=21) with AES-128/192/256, // cleartext string "Hello, world!", passphrase "password" private readonly byte[] v4skesk_argon2_aes128 = Base64.Decode( @@ -143,7 +143,7 @@ public class PgpCryptoRefreshTest "Zjd4SG7Tv4RJHeycolKmqSHDoK5XlOsA7vlw50nKuRjDyRfsPOFDfHz8hR/z7D1i" + "HST68tjRCRmwqeqVgusCmBlXrXzYTkPXGtmZl2+EYazSACQFVg=="); - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-x25519-aead-ocb-encr + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-x25519-aead-ocb-encr // encrypts the cleartext string "Hello, world!" for the sample certificate v6Certificate // V6 PKESK + V2 SEIPD X25519 AES-128 OCB private readonly byte[] v6pkesk_v2seipd_aes128_ocb = Base64.Decode( @@ -390,7 +390,7 @@ private void PubkeyEncryptDecryptRoundtrip(byte[] plaintext, bool useAead,bool u [Test] public void Version4Ed25519LegacyPubkeySampleTest() { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-key + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-4-ed25519leg PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v4Ed25519LegacyPubkeySample); PgpPublicKey pubKey = pubRing.GetPublicKey(); @@ -406,7 +406,7 @@ public void Version4Ed25519LegacyPubkeySampleTest() public void Version4Ed25519LegacyCreateTest() { // create a v4 EdDsa_Legacy Pubkey with the same key material and creation datetime as the test vector - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-key + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-4-ed25519leg // then check KeyId/Fingerprint var key = new Ed25519PublicKeyParameters(Hex.Decode("3f098994bdd916ed4053197934e4a87c80733a1280d62f8010992e43ee3b2406")); var pubKey = new PgpPublicKey(PublicKeyAlgorithmTag.EdDsa_Legacy, key, DateTime.Parse("2014-08-19 14:28:27Z")); @@ -421,7 +421,7 @@ public void Version4Ed25519LegacyCreateTest() [Test] public void Version4Ed25519LegacySignatureSampleTest() { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v4-ed25519legacy-sig + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-4-ed25519lega PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v4Ed25519LegacyPubkeySample); PgpPublicKey pubKey = pubRing.GetPublicKey(); @@ -446,7 +446,7 @@ public void Version4Ed25519LegacySignatureSampleTest() public void Version6CertificateParsingTest() { /* - * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans + * https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-6-certificat * A Transferable Public Key consisting of: * A v6 Ed25519 Public-Key packet * A v6 direct key self-signature @@ -504,7 +504,7 @@ public void Version6PublicKeyCreationTest() { /* * Create a v6 Ed25519 pubkey with the same key material and creation datetime as the test vector - * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans + * https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-6-certificat * then check the fingerprint and verify a signature */ byte[] keyMaterial = Hex.Decode("f94da7bb48d60a61e567706a6587d0331999bb9d891a08242ead84543df895a3"); @@ -533,7 +533,7 @@ public void Version6PublicKeyCreationTest() public void Version6UnlockedSecretKeyParsingTest() { /* - * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-secret-key-transf + * https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-6-certificat * A Transferable Secret Key consisting of: * A v6 Ed25519 Secret-Key packet * A v6 direct key self-signature @@ -612,8 +612,8 @@ public void Version6Ed25519KeyPairCreationTest() { /* * Create a v6 Ed25519 keypair with the same key material and creation datetime as the test vector - * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans - * then check the fingerprint and verify a signature + * https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-6-certificat + * then check the fingerprint and perform encode-decode, sign-verify, encrypt-decrypt roundtrips */ byte[] keyMaterial = Hex.Decode("1972817b12be707e8d5f586ce61361201d344eb266a2c82fde6835762b65b0b7"); Ed25519PrivateKeyParameters seckey = new Ed25519PrivateKeyParameters(keyMaterial); @@ -718,13 +718,21 @@ public void Version6Ed25519KeyPairCreationTest() false, subKey.PublicKey, subKey.ExtractPrivateKey(emptyPassphrase)); + + PgpPublicKeyRing pubring = keyRingGen.GeneratePublicKeyRing(); + PgpPublicKey[] keys = pubring.GetPublicKeys().ToArray(); + IsEquals(keys[0].Version, PublicKeyPacket.Version6); + IsEquals(keys[0].Algorithm, PublicKeyAlgorithmTag.Ed25519); + IsTrue("wrong master key fingerprint", AreEqual(keys[0].GetFingerprint(), expectedFingerprint)); + IsEquals(keys[1].Version, PublicKeyPacket.Version6); + IsEquals(keys[1].Algorithm, PublicKeyAlgorithmTag.X25519); } [Test] public void Version6Ed448KeyPairCreationTest() { /* - * Create a v6 Ed448 keypair, then perform encode-decode and sign-verify roundtrips + * Create a v6 Ed448 keypair, then perform encode-decode, sign-verify, encrypt-decrypt roundtrips */ SecureRandom rand = new SecureRandom(); DateTime now = DateTime.UtcNow; @@ -804,13 +812,39 @@ public void Version6Ed448KeyPairCreationTest() false, subKey.PublicKey, subKey.ExtractPrivateKey(emptyPassphrase)); + + PgpPublicKeyRing pubring = keyRingGen.GeneratePublicKeyRing(); + PgpPublicKey[] keys = pubring.GetPublicKeys().ToArray(); + IsEquals(keys[0].Version, PublicKeyPacket.Version6); + IsEquals(keys[0].Algorithm, PublicKeyAlgorithmTag.Ed448); + IsTrue("wrong master key fingerprint", AreEqual(keys[0].GetFingerprint(), fpr)); + IsEquals(keys[1].Version, PublicKeyPacket.Version6); + IsEquals(keys[1].Algorithm, PublicKeyAlgorithmTag.X448); + + using (var ms = new MemoryStream()) + { + using (var arms = new ArmoredOutputStream(ms)) + { + decodedsecring.Encode(arms); + } + string armored = Encoding.ASCII.GetString(ms.ToArray()); + } + + using (var ms = new MemoryStream()) + { + using (var arms = new ArmoredOutputStream(ms)) + { + pubring.Encode(arms); + } + string armored = Encoding.ASCII.GetString(ms.ToArray()); + } } [Test] public void Version6LockedSecretKeyParsingTest() { /* - * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-locked-v6-secret-key + * https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-6-certificat * The same secret key as in Version6UnlockedSecretKeyParsingTest, but the secret key * material is locked with a passphrase using AEAD and Argon2. */ @@ -870,7 +904,7 @@ public void Version6LockedSecretKeyParsingTest() [Test] public void Version6SampleCleartextSignedMessageVerifySignatureTest() { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-cleartext-signed-mes + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-cleartext-signed-mes PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v6Certificate); PgpPublicKey pubKey = pubRing.GetPublicKey(); @@ -889,7 +923,7 @@ public void Version6SampleCleartextSignedMessageVerifySignatureTest() [Test] public void Version6SampleInlineSignedMessageVerifySignatureTest() { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-inline-signed-messag + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-inline-signed-messag PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v6Certificate); PgpPublicKey pubKey = pubRing.GetPublicKey(); @@ -945,9 +979,9 @@ public void Version6SkeskVersion2SeipdTest() // S2K type iterated+salted, using AES-128 with AEAD encryption. byte[][] messages = new byte[][] { - v6skesk_aes128_eax, // from crypto-refresh A.9 - v6skesk_aes128_ocb, // from crypto-refresh A.10 - v6skesk_aes128_gcm // from crypto-refresh A.11 + v6skesk_aes128_eax, // from RFC 9580 A.9 + v6skesk_aes128_ocb, // from RFC 9580 A.10 + v6skesk_aes128_gcm // from RFC 9580 A.11 }; byte[] plaintext = Encoding.UTF8.GetBytes("Hello, world!"); @@ -1098,9 +1132,9 @@ public void SkeskWithArgon2Test() { byte[][] messages = new byte[][] { - v4skesk_argon2_aes128, // from crypto-refresh A.12.1 - v4skesk_argon2_aes192, // from crypto-refresh A.12.2 - v4skesk_argon2_aes256, // from crypto-refresh A.12.3 + v4skesk_argon2_aes128, // from RFC 9580 A.12.1 + v4skesk_argon2_aes192, // from RFC 9580 A.12.2 + v4skesk_argon2_aes256, // from RFC 9580 A.12.3 v6skesk_argon2_aes256_ocb // generated with gosop 2.0.0-alpha }; @@ -1201,7 +1235,7 @@ public void PkeskTest() * V6 PKESK + V2 SEIPD AEAD encrypted message that spans over 4 chunks * (chunk size 512 octets) * 2000 octets of /dev/zero encrypted with sample V6 certificate from - * crypto-refresh Appendix A.3 and AES-256 in OCB mode. + * RFC 9580 Appendix A.3 and AES-256 in OCB mode. * Generated with gosop 2.0.0-alpha * Session key CFB73D46CF7C13B7535227BEDB5B2D8B4023C5B58289D19CF2C33B0DB388B0B6 */ diff --git a/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs b/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs index d02195566..f3791c655 100644 --- a/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs +++ b/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs @@ -225,9 +225,9 @@ public class PgpInteroperabilityTestSuite "vDoQtVn8sArWqwEAi8HwbMhL+YwRItRZDknpC4vFjTHVMd1zMrz/JyeuT9k=" ); - // v6 Ed25519/X25519 key from crypto-refresh - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-certificate-trans - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-sample-v6-secret-key-transf + // v6 Ed25519/X25519 key from RFC 9580 + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-6-certificat + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-6-secret-key private static readonly byte[] v6Certificate = Base64.Decode( "xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf" + "GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy" + @@ -351,7 +351,7 @@ public void MultiplePkeskTest() { // Encrypt-Decrypt roundtrip with multiple keys: the plaintext // "Hello World :)" is encrypted with the X25519 sample key from - // Appendix A.3 of crypto-refresh and the 'Alice' ECDH key from + // Appendix A.3 of RFC 9580 and the 'Alice' ECDH key from // "OpenPGP Example Keys and Certificates" byte[] message = Base64.Decode( "wVQDEsg/HnBvYwgZaeKxsIieN+FvNLxmgMfRKJZGKt8vAa5BYX2k0QAetCMpCQbE" + @@ -372,7 +372,7 @@ public void MultiplePkeskTest() IsEquals(2, encDataList.Count); - // decrypt with crypto-refresh sample X25519 key + // decrypt with RFC 9580 sample X25519 key var encData = encDataList[0] as PgpPublicKeyEncryptedData; FailIf("invalid PgpPublicKeyEncryptedData", encData is null); PgpSecretKey secKey = bundle.GetSecretKey(encData.KeyId); @@ -414,7 +414,7 @@ public void MultiplePkeskTest() public void MultipleInlineSignatureTest() { // Verify Inline Signature with multiple keys: - // v6 key from crypto-refresh and v4 key "Alice" from "OpenPGP Example Keys and Certificates" + // v6 key from RFC 9580 and v4 key "Alice" from "OpenPGP Example Keys and Certificates" // https://tests.sequoia-pgp.org/#Inline_Sign_with_minimal_key_from_RFC9760_and_key__Alice___verify_with_key_from_RFC9760 // inline signed message generated by GopenPGP 3.0.0-alpha @@ -452,7 +452,7 @@ public void MultipleInlineSignatureTest() public void GenerateAndVerifyMultipleInlineSignatureTest() { // Inline Sign-Verify roundtrip test with multiple keys: - // v6 key from crypto-refresh and v4 key "Alice" from "OpenPGP Example Keys and Certificates" + // v6 key from RFC 9580 and v4 key "Alice" from "OpenPGP Example Keys and Certificates" byte[] data = Encoding.UTF8.GetBytes("Hello World :)"); byte[] message; @@ -535,7 +535,7 @@ private void VerifyMultipleDetachedSignaturesTest(byte[] signaturePacket, byte[] public void MultipleDetachedSignatureTest() { // Verify Detached Signature with multiple keys: - // v6 key from crypto-refresh and v4 key "Alice" from "OpenPGP Example Keys and Certificates" + // v6 key from RFC 9580 and v4 key "Alice" from "OpenPGP Example Keys and Certificates" // https://tests.sequoia-pgp.org/#Detached_Sign_with_minimal_key_from_RFC9760_and_key__Alice___verify_with_key_from_RFC9760 byte[] data = Encoding.UTF8.GetBytes("Hello World :)"); @@ -575,7 +575,7 @@ public void MultipleDetachedSignatureTest() public void GenerateAndVerifyMultipleDetachedSignatureTest() { // Inline Sign-Verify roundtrip test with multiple keys: - // v6 key from crypto-refresh and v4 key "Alice" from "OpenPGP Example Keys and Certificates" + // v6 key from RFC 9580 and v4 key "Alice" from "OpenPGP Example Keys and Certificates" byte[] data = Encoding.UTF8.GetBytes("Hello World :)"); byte[] corruptedData = Encoding.UTF8.GetBytes("Hello World :("); @@ -729,7 +729,7 @@ public void SeipdVersion2WithMultipleMethodsTest() gen.AddMethod(v6); // Check constraint: An implementation MUST NOT generate ElGamal v6 PKESKs. - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-algorithm-specific-fields-fo + // https://www.rfc-editor.org/rfc/rfc9580#name-algorithm-specific-fields-fo Assert.Throws(() => { gen.AddMethod(carol); From a1496efc6b3f9d945730a4047e5554c1b75fed84 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Sat, 28 Sep 2024 18:44:09 +0200 Subject: [PATCH 29/37] Feature flag for Version 2 SEIPD --- crypto/src/bcpg/sig/Features.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crypto/src/bcpg/sig/Features.cs b/crypto/src/bcpg/sig/Features.cs index a04d2cf9d..df80bde32 100644 --- a/crypto/src/bcpg/sig/Features.cs +++ b/crypto/src/bcpg/sig/Features.cs @@ -19,6 +19,10 @@ public class Features fingerprint format */ public static readonly byte FEATURE_VERSION_5_PUBLIC_KEY = 0x04; + /** Identifier for the Version 2 Symmetrically Encrypted and Integrity Protected + Data packet */ + public static readonly byte FEATURE_VERSION_2_SEIPD = 0x08; + private static byte[] FeatureToByteArray(byte feature) { return new byte[1]{ feature }; From 28db83ebbb2cfb1a7c6005b800a04c3673283a8a Mon Sep 17 00:00:00 2001 From: Peter Dettman Date: Wed, 2 Oct 2024 23:44:48 +0700 Subject: [PATCH 30/37] Followup changes to Argon2 PR --- crypto/src/crypto/ICharToByteConverter.cs | 37 +-- crypto/src/crypto/PasswordConverter.cs | 54 ++-- .../crypto/generators/Argon2BytesGenerator.cs | 305 +++++++----------- .../src/crypto/parameters/Argon2Parameters.cs | 142 +++----- crypto/test/src/crypto/test/Argon2Test.cs | 217 ++++++------- 5 files changed, 284 insertions(+), 471 deletions(-) diff --git a/crypto/src/crypto/ICharToByteConverter.cs b/crypto/src/crypto/ICharToByteConverter.cs index 94e0279e9..2f88ee61a 100644 --- a/crypto/src/crypto/ICharToByteConverter.cs +++ b/crypto/src/crypto/ICharToByteConverter.cs @@ -1,35 +1,16 @@ namespace Org.BouncyCastle.Crypto { + /// + /// Interface for a converter that produces a byte encoding for a char array. + /// public interface ICharToByteConverter { - /** - * Return the type of the conversion. - * - * @return a type name for the conversion. - */ - string GetName(); + /// The name of the conversion. + string Name { get; } - /** - * Return a byte encoded representation of the passed in password. - * - * @param password the characters to encode. - * @return a byte encoding of password. - */ + /// Return a byte encoded representation of the passed in password. + /// the characters to encode. + /// a byte encoding of password. byte[] Convert(char[] password); } - - public static class CharToByteConverterExtensions - { - - /** - * Return a byte encoded representation of the passed in password. - * - * @param password the string to encode. - * @return a byte encoding of password. - */ - public static byte[] Convert(this ICharToByteConverter converter, string password) - { - return converter.Convert(password.ToCharArray()); - } - } -} \ No newline at end of file +} diff --git a/crypto/src/crypto/PasswordConverter.cs b/crypto/src/crypto/PasswordConverter.cs index c2786152c..0e78a57d2 100644 --- a/crypto/src/crypto/PasswordConverter.cs +++ b/crypto/src/crypto/PasswordConverter.cs @@ -1,44 +1,36 @@ -using System; -using System.Text; - -namespace Org.BouncyCastle.Crypto +namespace Org.BouncyCastle.Crypto { - public class PasswordConverter + /// + /// Standard char[] to byte[] converters for password based derivation algorithms. + /// + public sealed class PasswordConverter : ICharToByteConverter { - private readonly string name; - private readonly Func converterFunction; - - public PasswordConverter(string name, Func converterFunction) - { - this.name = name; - this.converterFunction = converterFunction; - } + private delegate byte[] ConverterFunction(char[] password); - public byte[] Convert(char[] password) - { - return converterFunction.Invoke(password); - } + private readonly string m_name; + private readonly ConverterFunction m_converterFunction; - public string GetName() + private PasswordConverter(string name, ConverterFunction converterFunction) { - return name; + m_name = name; + m_converterFunction = converterFunction; } - public readonly static ICharToByteConverter ASCII = new PasswordConverter("ASCII", PbeParametersGenerator.Pkcs5PasswordToBytes); - - public readonly static ICharToByteConverter UTF8 = new PasswordConverter("UTF8", PbeParametersGenerator.Pkcs5PasswordToUtf8Bytes); - - public readonly static ICharToByteConverter PKCS12 = new PasswordConverter("PKCS12", PbeParametersGenerator.Pkcs12PasswordToBytes); + public byte[] Convert(char[] password) => m_converterFunction(password); - public readonly static ICharToByteConverter UTF32 = new PasswordConverter("UTF32", Encoding.UTF32.GetBytes); + public string Name => m_name; - public readonly static ICharToByteConverter Unicode = new PasswordConverter("Unicode", Encoding.Unicode.GetBytes); + /// Do a straight char[] to 8 bit conversion. + public readonly static ICharToByteConverter Ascii = new PasswordConverter("ASCII", + PbeParametersGenerator.Pkcs5PasswordToBytes); - public readonly static ICharToByteConverter BigEndianUnicode = new PasswordConverter("BigEndianUnicode", Encoding.BigEndianUnicode.GetBytes); + /// Do a char[] conversion by producing UTF-8 data. + public readonly static ICharToByteConverter Utf8 = new PasswordConverter("UTF8", + PbeParametersGenerator.Pkcs5PasswordToUtf8Bytes); -#if NET6_0_OR_GREATER - public readonly static ICharToByteConverter Latin1 = new PasswordConverter("Latin1", Encoding.Latin1.GetBytes); -#endif + /// Do char[] to BMP conversion (i.e. 2 bytes per character). + public readonly static ICharToByteConverter Pkcs12 = new PasswordConverter("PKCS12", + PbeParametersGenerator.Pkcs12PasswordToBytes); } -} \ No newline at end of file +} diff --git a/crypto/src/crypto/generators/Argon2BytesGenerator.cs b/crypto/src/crypto/generators/Argon2BytesGenerator.cs index 3fb0ea7f9..3800ffb12 100644 --- a/crypto/src/crypto/generators/Argon2BytesGenerator.cs +++ b/crypto/src/crypto/generators/Argon2BytesGenerator.cs @@ -1,36 +1,38 @@ -using Org.BouncyCastle.Crypto.Digests; +using System; + +using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Utilities; +using Org.BouncyCastle.Math.Raw; using Org.BouncyCastle.Utilities; -using System; namespace Org.BouncyCastle.Crypto.Generators { public sealed class Argon2BytesGenerator { - private const int ARGON2_BLOCK_SIZE = 1024; - private const int ARGON2_QWORDS_IN_BLOCK = ARGON2_BLOCK_SIZE / 8; + private const int Argon2BlockSize = 1024; + private const int Argon2QwordsInBlock = Argon2BlockSize / 8; - private const int ARGON2_ADDRESSES_IN_BLOCK = 128; + private const int Argon2AddressesInBlock = 128; - private const int ARGON2_PREHASH_DIGEST_LENGTH = 64; - private const int ARGON2_PREHASH_SEED_LENGTH = 72; + private const int Argon2PrehashDigestLength = 64; + private const int Argon2PrehashSeedLength = 72; - private const int ARGON2_SYNC_POINTS = 4; + private const int Argon2SyncPoints = 4; /* Minimum and maximum number of lanes (degree of parallelism) */ - private const int MIN_PARALLELISM = 1; - private const int MAX_PARALLELISM = 16777216; + private const int MinParallelism = 1; + private const int MaxParallelism = 16777216; /* Minimum and maximum digest size in bytes */ - private const int MIN_OUTLEN = 4; + private const int MinOutlen = 4; /* Minimum and maximum number of passes */ - private const int MIN_ITERATIONS = 1; + private const int MinIterations = 1; - private const long M32L = 0xFFFFFFFFL; + private const ulong M32L = 0xFFFFFFFFL; - private readonly byte[] ZERO_BYTES = new byte[4]; + private readonly byte[] ZeroBytes = new byte[4]; private Argon2Parameters parameters; private Block[] memory; @@ -50,59 +52,32 @@ public void Init(Argon2Parameters parameters) { this.parameters = parameters; - if (parameters.GetLanes() < MIN_PARALLELISM) - { - throw new InvalidOperationException($"lanes must be greater than " + MIN_PARALLELISM); - } - else if (parameters.GetLanes() > MAX_PARALLELISM) - { - throw new InvalidOperationException("lanes must be less than " + MAX_PARALLELISM); - } - else if (parameters.GetMemory() < 2 * parameters.GetLanes()) - { - throw new InvalidOperationException("memory is less than: " + (2 * parameters.GetLanes()) + " expected " + (2 * parameters.GetLanes())); - } - else if (parameters.GetIterations() < MIN_ITERATIONS) - { - throw new InvalidOperationException("iterations is less than: " + MIN_ITERATIONS); - } + if (parameters.Lanes < MinParallelism) + throw new InvalidOperationException($"lanes must be greater than " + MinParallelism); + if (parameters.Lanes > MaxParallelism) + throw new InvalidOperationException("lanes must be less than " + MaxParallelism); + if (parameters.Memory < 2 * parameters.Lanes) + throw new InvalidOperationException("memory is less than: " + (2 * parameters.Lanes) + " expected " + (2 * parameters.Lanes)); + if (parameters.Iterations < MinIterations) + throw new InvalidOperationException("iterations is less than: " + MinIterations); DoInit(parameters); } - public int GenerateBytes(string password, byte[] output) - { - return GenerateBytes(password.ToCharArray(), output); - } + public int GenerateBytes(char[] password, byte[] output) => + GenerateBytes(parameters.CharToByteConverter.Convert(password), output); - public int GenerateBytes(string password, byte[] output, int outOff, int outLen) - { - return GenerateBytes(password.ToCharArray(), output, outOff, outLen); - } + public int GenerateBytes(char[] password, byte[] output, int outOff, int outLen) => + GenerateBytes(parameters.CharToByteConverter.Convert(password), output, outOff, outLen); - public int GenerateBytes(char[] password, byte[] output) - { - return GenerateBytes(parameters.GetCharToByteConverter().Convert(password), output); - } - - public int GenerateBytes(char[] password, byte[] output, int outOff, int outLen) - { - return GenerateBytes(parameters.GetCharToByteConverter().Convert(password), output, outOff, outLen); - } - - public int GenerateBytes(byte[] password, byte[] output) - { - return GenerateBytes(password, output, 0, output.Length); - } + public int GenerateBytes(byte[] password, byte[] output) => GenerateBytes(password, output, 0, output.Length); public int GenerateBytes(byte[] password, byte[] output, int outOff, int outLen) { - if (outLen < MIN_OUTLEN) - { - throw new InvalidOperationException("output length less than " + MIN_OUTLEN); - } + if (outLen < MinOutlen) + throw new InvalidOperationException("output length less than " + MinOutlen); - byte[] tmpBlockBytes = new byte[ARGON2_BLOCK_SIZE]; + byte[] tmpBlockBytes = new byte[Argon2BlockSize]; Initialize(tmpBlockBytes, password, outLen); FillMemoryBlocks(); @@ -131,18 +106,13 @@ private void DoInit(Argon2Parameters parameters) { /* 2. Align memory size */ /* Minimum memoryBlocks = 8L blocks, where L is the number of lanes */ - int memoryBlocks = parameters.GetMemory(); - - if (memoryBlocks < 2 * ARGON2_SYNC_POINTS * parameters.GetLanes()) - { - memoryBlocks = 2 * ARGON2_SYNC_POINTS * parameters.GetLanes(); - } + int memoryBlocks = System.Math.Max(parameters.Memory, 2 * Argon2SyncPoints * parameters.Lanes); - this.segmentLength = memoryBlocks / (parameters.GetLanes() * ARGON2_SYNC_POINTS); - this.laneLength = segmentLength * ARGON2_SYNC_POINTS; + this.segmentLength = memoryBlocks / (parameters.Lanes * Argon2SyncPoints); + this.laneLength = segmentLength * Argon2SyncPoints; /* Ensure that all segments have equal length */ - memoryBlocks = segmentLength * (parameters.GetLanes() * ARGON2_SYNC_POINTS); + memoryBlocks = segmentLength * (parameters.Lanes * Argon2SyncPoints); InitMemory(memoryBlocks); } @@ -161,15 +131,15 @@ private void FillMemoryBlocks() { FillBlock filler = new FillBlock(); Position position = new Position(); - for (int pass = 0; pass < parameters.GetIterations(); ++pass) + for (int pass = 0; pass < parameters.Iterations; ++pass) { position.pass = pass; - for (int slice = 0; slice < ARGON2_SYNC_POINTS; ++slice) + for (int slice = 0; slice < Argon2SyncPoints; ++slice) { position.slice = slice; - for (int lane = 0; lane < parameters.GetLanes(); ++lane) + for (int lane = 0; lane < parameters.Lanes; ++lane) { position.lane = lane; @@ -200,7 +170,7 @@ private void FillSegment(FillBlock filler, Position position) for (int index = startingIndex; index < segmentLength; ++index) { - long pseudoRandom = GetPseudoRandom( + ulong pseudoRandom = GetPseudoRandom( filler, index, addressBlock, @@ -232,21 +202,21 @@ private void FillSegment(FillBlock filler, Position position) private bool IsDataIndependentAddressing(Position position) { - return (parameters.GetArgonType() == Argon2Parameters.ARGON2_i) || - (parameters.GetArgonType() == Argon2Parameters.ARGON2_id + return (parameters.Type == Argon2Parameters.Argon2_i) || + (parameters.Type == Argon2Parameters.Argon2_id && (position.pass == 0) - && (position.slice < ARGON2_SYNC_POINTS / 2) + && (position.slice < Argon2SyncPoints / 2) ); } private void InitAddressBlocks(FillBlock filler, Position position, Block inputBlock, Block addressBlock) { - inputBlock.v[0] = (long)position.pass; - inputBlock.v[1] = (long)position.lane; - inputBlock.v[2] = (long)position.slice; - inputBlock.v[3] = (long)memory.Length; - inputBlock.v[4] = (long)parameters.GetIterations(); - inputBlock.v[5] = (long)parameters.GetArgonType(); + inputBlock.v[0] = (ulong)position.pass; + inputBlock.v[1] = (ulong)position.lane; + inputBlock.v[2] = (ulong)position.slice; + inputBlock.v[3] = (ulong)memory.Length; + inputBlock.v[4] = (ulong)parameters.Iterations; + inputBlock.v[5] = (ulong)parameters.Type; if ((position.pass == 0) && (position.slice == 0)) { @@ -257,7 +227,7 @@ private void InitAddressBlocks(FillBlock filler, Position position, Block inputB private bool IsWithXor(Position position) { - return !(position.pass == 0 || parameters.GetVersion() == Argon2Parameters.ARGON2_VERSION_10); + return !(position.pass == 0 || parameters.Version == Argon2Parameters.Argon2_Version10); } private int GetPrevOffset(int currentOffset) @@ -295,7 +265,7 @@ private static void NextAddresses(FillBlock filler, Block inputBlock, Block addr /* 1.2 Computing the index of the reference block */ /* 1.2.1 Taking pseudo-random value from the previous block */ - private long GetPseudoRandom( + private ulong GetPseudoRandom( FillBlock filler, int index, Block addressBlock, @@ -305,7 +275,7 @@ private long GetPseudoRandom( { if (dataIndependentAddressing) { - int addressIndex = index % ARGON2_ADDRESSES_IN_BLOCK; + int addressIndex = index % Argon2AddressesInBlock; if (addressIndex == 0) { NextAddresses(filler, inputBlock, addressBlock); @@ -318,11 +288,9 @@ private long GetPseudoRandom( } } - private int GetRefLane(Position position, long pseudoRandom) + private int GetRefLane(Position position, ulong pseudoRandom) { - // Double-casting to/from ulong required because unsigned right shift operator - // >>> is supported only in C# 11 (.NET 7 or greater) - int refLane = (int)(((ulong)pseudoRandom >> 32) % (ulong)parameters.GetLanes()); + int refLane = (int)((long)(pseudoRandom >> 32) % parameters.Lanes); if ((position.pass == 0) && (position.slice == 0)) { @@ -332,10 +300,10 @@ private int GetRefLane(Position position, long pseudoRandom) return refLane; } - private int GetRefColumn(Position position, int index, long pseudoRandom, bool sameLane) + private int GetRefColumn(Position position, int index, ulong pseudoRandom, bool sameLane) { - long referenceAreaSize; - long startPosition; + ulong referenceAreaSize; + ulong startPosition; if (position.pass == 0) { @@ -344,33 +312,31 @@ private int GetRefColumn(Position position, int index, long pseudoRandom, bool s if (sameLane) { /* The same lane => add current segment */ - referenceAreaSize = position.slice * segmentLength + index - 1; + referenceAreaSize = (ulong)(position.slice * segmentLength + index - 1); } else { /* pass == 0 && !sameLane => position.slice > 0*/ - referenceAreaSize = position.slice * segmentLength + ((index == 0) ? (-1) : 0); + referenceAreaSize = (ulong)(position.slice * segmentLength + ((index == 0) ? (-1) : 0)); } } else { - startPosition = ((position.slice + 1) * segmentLength) % laneLength; + startPosition = (ulong)(((position.slice + 1) * segmentLength) % laneLength); if (sameLane) { - referenceAreaSize = laneLength - segmentLength + index - 1; + referenceAreaSize = (ulong)(laneLength - segmentLength + index - 1); } else { - referenceAreaSize = laneLength - segmentLength + ((index == 0) ? (-1) : 0); + referenceAreaSize = (ulong)(laneLength - segmentLength + ((index == 0) ? (-1) : 0)); } } - long relativePosition = pseudoRandom & 0xFFFFFFFFL; + ulong relativePosition = pseudoRandom & 0xFFFFFFFFUL; - // Double-casting to/from ulong required because unsigned right shift operator - // >>> is supported only in C# 11 (.NET 7 or greater) - relativePosition = (long)((ulong)(relativePosition * relativePosition) >> 32); + relativePosition = (relativePosition * relativePosition) >> 32; relativePosition = referenceAreaSize - 1 - ((referenceAreaSize * relativePosition) >> 32); return (int)(startPosition + relativePosition) % laneLength; @@ -381,7 +347,7 @@ private void Digest(byte[] tmpBlockBytes, byte[] output, int outOff, int outLen) Block finalBlock = memory[laneLength - 1]; /* XOR the last blocks */ - for (int i = 1; i < parameters.GetLanes(); i++) + for (int i = 1; i < parameters.Lanes; i++) { int lastBlockInLane = i * laneLength + (laneLength - 1); finalBlock.XorWith(memory[lastBlockInLane]); @@ -451,7 +417,7 @@ private static void RoundFunction(Block block, int v8, int v9, int v10, int v11, int v12, int v13, int v14, int v15) { - long[] v = block.v; + ulong[] v = block.v; F(v, v0, v4, v8, v12); F(v, v1, v5, v9, v13); @@ -464,7 +430,7 @@ private static void RoundFunction(Block block, F(v, v3, v4, v9, v14); } - private static void F(long[] v, int a, int b, int c, int d) + private static void F(ulong[] v, int a, int b, int c, int d) { QuarterRound(v, a, b, d, 32); QuarterRound(v, c, d, b, 24); @@ -472,12 +438,12 @@ private static void F(long[] v, int a, int b, int c, int d) QuarterRound(v, c, d, b, 63); } - private static void QuarterRound(long[] v, int x, int y, int z, int s) + private static void QuarterRound(ulong[] v, int x, int y, int z, int s) { // fBlaMka(v, x, y); // rotr64(v, z, x, s); - long a = v[x], b = v[y], c = v[z]; + ulong a = v[x], b = v[y], c = v[z]; a += b + 2 * (a & M32L) * (b & M32L); c = Longs.RotateRight(c ^ a, s); @@ -492,44 +458,44 @@ private static void QuarterRound(long[] v, int x, int y, int z, int s) * aL = least 32 bit */ // private static void fBlaMka(long[] v, int x, int y) // { - // final long a = v[x], b = v[y]; - // final long ab = (a & M32L) * (b & M32L); + // final ulong a = v[x], b = v[y]; + // final ulong ab = (a & M32L) * (b & M32L); // // v[x] = a + b + 2 * ab; // } // // private static void rotr64(long[] v, int x, int y, int s) // { - // v[x] = Longs.rotateRight(v[x] ^ v[y], s); + // v[x] = ulongs.rotateRight(v[x] ^ v[y], s); // } private void Initialize(byte[] tmpBlockBytes, byte[] password, int outputLength) { /* * H0 = H64(p, Ï„, m, t, v, y, |P|, P, |S|, S, |L|, K, |X|, X) - * -> 64 byte (ARGON2_PREHASH_DIGEST_LENGTH) + * -> 64 byte (Argon2PrehashDigestLength) */ - Blake2bDigest blake = new Blake2bDigest(ARGON2_PREHASH_DIGEST_LENGTH * 8); + Blake2bDigest blake = new Blake2bDigest(Argon2PrehashDigestLength * 8); - int[] values = { - parameters.GetLanes(), - outputLength, - parameters.GetMemory(), - parameters.GetIterations(), - parameters.GetVersion(), - parameters.GetArgonType() + uint[] values = { + (uint)parameters.Lanes, + (uint)outputLength, + (uint)parameters.Memory, + (uint)parameters.Iterations, + (uint)parameters.Version, + (uint)parameters.Type }; - Helpers.IntArrayToLittleEndian(values, tmpBlockBytes, 0); + Pack.UInt32_To_LE(values, tmpBlockBytes, 0); blake.BlockUpdate(tmpBlockBytes, 0, values.Length * 4); AddByteString(tmpBlockBytes, blake, password); - AddByteString(tmpBlockBytes, blake, parameters.GetSalt()); - AddByteString(tmpBlockBytes, blake, parameters.GetSecret()); - AddByteString(tmpBlockBytes, blake, parameters.GetAdditional()); + AddByteString(tmpBlockBytes, blake, parameters.Salt); + AddByteString(tmpBlockBytes, blake, parameters.Secret); + AddByteString(tmpBlockBytes, blake, parameters.Additional); - byte[] initialHashWithZeros = new byte[ARGON2_PREHASH_SEED_LENGTH]; + byte[] initialHashWithZeros = new byte[Argon2PrehashSeedLength]; blake.DoFinal(initialHashWithZeros, 0); FillFirstBlocks(tmpBlockBytes, initialHashWithZeros); @@ -539,7 +505,7 @@ private void AddByteString(byte[] tmpBlockBytes, IDigest digest, byte[] octets) { if (null == octets) { - digest.BlockUpdate(ZERO_BYTES, 0, 4); + digest.BlockUpdate(ZeroBytes, 0, 4); return; } @@ -554,20 +520,20 @@ private void AddByteString(byte[] tmpBlockBytes, IDigest digest, byte[] octets) */ private void FillFirstBlocks(byte[] tmpBlockBytes, byte[] initialHashWithZeros) { - byte[] initialHashWithOnes = new byte[ARGON2_PREHASH_SEED_LENGTH]; - Array.Copy(initialHashWithZeros, 0, initialHashWithOnes, 0, ARGON2_PREHASH_DIGEST_LENGTH); - // Pack.intToLittleEndian(1, initialHashWithOnes, ARGON2_PREHASH_DIGEST_LENGTH); - initialHashWithOnes[ARGON2_PREHASH_DIGEST_LENGTH] = 1; + byte[] initialHashWithOnes = new byte[Argon2PrehashSeedLength]; + Array.Copy(initialHashWithZeros, 0, initialHashWithOnes, 0, Argon2PrehashDigestLength); + // Pack.intToLittleEndian(1, initialHashWithOnes, Argon2PrehashDigestLength); + initialHashWithOnes[Argon2PrehashDigestLength] = 1; - for (int i = 0; i < parameters.GetLanes(); i++) + for (int i = 0; i < parameters.Lanes; i++) { - Pack.UInt32_To_LE((uint)i, initialHashWithZeros, ARGON2_PREHASH_DIGEST_LENGTH + 4); - Pack.UInt32_To_LE((uint)i, initialHashWithOnes, ARGON2_PREHASH_DIGEST_LENGTH + 4); + Pack.UInt32_To_LE((uint)i, initialHashWithZeros, Argon2PrehashDigestLength + 4); + Pack.UInt32_To_LE((uint)i, initialHashWithOnes, Argon2PrehashDigestLength + 4); - Hash(initialHashWithZeros, tmpBlockBytes, 0, ARGON2_BLOCK_SIZE); + Hash(initialHashWithZeros, tmpBlockBytes, 0, Argon2BlockSize); memory[i * laneLength + 0].FromBytes(tmpBlockBytes); - Hash(initialHashWithOnes, tmpBlockBytes, 0, ARGON2_BLOCK_SIZE); + Hash(initialHashWithOnes, tmpBlockBytes, 0, Argon2BlockSize); memory[i * laneLength + 1].FromBytes(tmpBlockBytes); } } @@ -641,70 +607,54 @@ internal void FillBlockWithXor(Block X, Block Y, Block currentBlock) private sealed class Block { - private const int SIZE = ARGON2_QWORDS_IN_BLOCK; + private const int Size = Argon2QwordsInBlock; /* 128 * 8 Byte QWords */ - internal readonly long[] v; + internal readonly ulong[] v; internal Block() { - v = new long[SIZE]; + v = new ulong[Size]; } internal void FromBytes(byte[] input) { - if (input.Length < ARGON2_BLOCK_SIZE) - { + if (input.Length < Argon2BlockSize) throw new ArgumentException("input shorter than blocksize"); - } - Helpers.LittleEndianToLongArray(input, 0, v); + Pack.LE_To_UInt64(input, 0, v); } - internal void ToBytes(byte[] output) + internal void ToBytes(byte[] output) { - if (output.Length < ARGON2_BLOCK_SIZE) - { + if (output.Length < Argon2BlockSize) throw new ArgumentException("output shorter than blocksize"); - } - Helpers.LongArrayToLittleEndian(v, output, 0); + Pack.UInt64_To_LE(v, output, 0); } internal void CopyBlock(Block other) { - Array.Copy(other.v, 0, v, 0, SIZE); + Array.Copy(other.v, 0, v, 0, Size); } internal void Xor(Block b1, Block b2) { - long[] v0 = v; - long[] v1 = b1.v; - long[] v2 = b2.v; - - for (int i = 0; i < SIZE; i++) - { - v0[i] = v1[i] ^ v2[i]; - } + Nat.Xor64(Size, b1.v, b2.v, v); } internal void XorWith(Block b1) { - long[] v0 = v; - long[] v1 = b1.v; - - for (int i = 0; i < SIZE; i++) - { - v0[i] ^= v1[i]; - } + Nat.XorTo64(Size, b1.v, v); } internal void XorWith(Block b1, Block b2) { - long[] v0 = v; - long[] v1 = b1.v; - long[] v2 = b2.v; - for (int i = 0; i < SIZE; i++) + // TODO New Nat.Xor variant for this + ulong[] v0 = v; + ulong[] v1 = b1.v; + ulong[] v2 = b2.v; + for (int i = 0; i < Size; i++) { v0[i] ^= v1[i] ^ v2[i]; } @@ -727,36 +677,5 @@ internal Position() { } } - - private static class Helpers - { - internal static void LittleEndianToLongArray(byte[] bs, int off, long[] ns) - { - for (int i = 0; i < ns.Length; ++i) - { - ns[i] = (long)Pack.LE_To_UInt64(bs, off); - off += 8; - } - } - - internal static void LongArrayToLittleEndian(long[] ns, byte[] bs, int off) - { - for (int i = 0; i < ns.Length; ++i) - { - Pack.UInt64_To_LE((ulong)ns[i], bs, off); - off += 8; - } - } - - internal static void IntArrayToLittleEndian(int[] ns, byte[] bs, int off) - { - for (int i = 0; i < ns.Length; ++i) - { - Pack.UInt32_To_LE((uint)ns[i], bs, off); - off += 4; - } - } - - } } } diff --git a/crypto/src/crypto/parameters/Argon2Parameters.cs b/crypto/src/crypto/parameters/Argon2Parameters.cs index f514d862e..4da2afca0 100644 --- a/crypto/src/crypto/parameters/Argon2Parameters.cs +++ b/crypto/src/crypto/parameters/Argon2Parameters.cs @@ -1,67 +1,60 @@ -using Org.BouncyCastle.Utilities; -using System; +using System; + +using Org.BouncyCastle.Utilities; namespace Org.BouncyCastle.Crypto.Parameters { public sealed class Argon2Parameters { - public const int ARGON2_d = 0x00; - public const int ARGON2_i = 0x01; - public const int ARGON2_id = 0x02; + public static readonly int Argon2_d = 0x00; + public static readonly int Argon2_i = 0x01; + public static readonly int Argon2_id = 0x02; - public const int ARGON2_VERSION_10 = 0x10; - public const int ARGON2_VERSION_13 = 0x13; + public static readonly int Argon2_Version10 = 0x10; + public static readonly int Argon2_Version13 = 0x13; + private readonly int type; private readonly byte[] salt; private readonly byte[] secret; private readonly byte[] additional; - private readonly int iterations; private readonly int memory; private readonly int lanes; - private readonly int version; - private readonly int type; private readonly ICharToByteConverter converter; - public class Builder + public sealed class Builder { - private const int DEFAULT_ITERATIONS = 3; - private const int DEFAULT_MEMORY_COST = 12; - private const int DEFAULT_LANES = 1; - private const int DEFAULT_TYPE = ARGON2_i; - private const int DEFAULT_VERSION = ARGON2_VERSION_13; + private static readonly int DefaultIterations = 3; + private static readonly int DefaultMemoryCost = 12; + private static readonly int DefaultLanes = 1; + private static readonly int DefaultType = Argon2_i; + private static readonly int DefaultVersion = Argon2_Version13; + + private readonly int type; private byte[] salt = Array.Empty(); private byte[] secret = Array.Empty(); private byte[] additional = Array.Empty(); - - private int iterations; - private int memory; - private int lanes; - - private int version; - private readonly int type; - - private ICharToByteConverter converter = PasswordConverter.UTF8; + private int iterations = DefaultIterations; + private int memory = 1 << DefaultMemoryCost; + private int lanes = DefaultLanes; + private int version = DefaultVersion; + private ICharToByteConverter converter = PasswordConverter.Utf8; public Builder() - : this(DEFAULT_TYPE) + : this(DefaultType) { } public Builder(int type) { this.type = type; - lanes = DEFAULT_LANES; - memory = 1 << DEFAULT_MEMORY_COST; - iterations = DEFAULT_ITERATIONS; - version = DEFAULT_VERSION; } public Builder WithParallelism(int parallelism) { - lanes = parallelism; + this.lanes = parallelism; return this; } @@ -113,20 +106,8 @@ public Builder WithCharToByteConverter(ICharToByteConverter converter) return this; } - public Builder WithCharToByteConverter(string name, Func converterFunction) - { - return WithCharToByteConverter(new PasswordConverter(name, converterFunction)); - } - - public Builder WithCharToByteConverter(Func converterFunction) - { - return WithCharToByteConverter("Custom", converterFunction); - } - - public Argon2Parameters Build() - { - return new Argon2Parameters(type, salt, secret, additional, iterations, memory, lanes, version, converter); - } + public Argon2Parameters Build() => + new Argon2Parameters(type, salt, secret, additional, iterations, memory, lanes, version, converter); public void Clear() { @@ -136,73 +117,42 @@ public void Clear() } } - private Argon2Parameters( - int type, - byte[] salt, - byte[] secret, - byte[] additional, - int iterations, - int memory, - int lanes, - int version, - ICharToByteConverter converter) + private Argon2Parameters(int type, byte[] salt, byte[] secret, byte[] additional, int iterations, int memory, + int lanes, int version, ICharToByteConverter converter) { - - this.salt = Arrays.Clone(salt); - this.secret = Arrays.Clone(secret); - this.additional = Arrays.Clone(additional); + this.type = type; + this.salt = salt; + this.secret = secret; + this.additional = additional; this.iterations = iterations; this.memory = memory; this.lanes = lanes; this.version = version; - this.type = type; this.converter = converter; } - public byte[] GetSalt() - { - return Arrays.Clone(salt); - } + public ICharToByteConverter CharToByteConverter => converter; - public byte[] GetSecret() - { - return Arrays.Clone(secret); - } + public byte[] GetSalt() => Arrays.Clone(salt); - public byte[] GetAdditional() - { - return Arrays.Clone(additional); - } + public byte[] GetSecret() => Arrays.Clone(secret); - public int GetIterations() - { - return iterations; - } + public byte[] GetAdditional() => Arrays.Clone(additional); - public int GetMemory() - { - return memory; - } + public int Iterations => iterations; - public int GetLanes() - { - return lanes; - } + public int Lanes => lanes; - public int GetVersion() - { - return version; - } + public int Memory => memory; - public int GetArgonType() - { - return type; - } + public int Type => type; - public ICharToByteConverter GetCharToByteConverter() - { - return converter; - } + public int Version => version; + + internal byte[] Additional => additional; + + internal byte[] Salt => salt; + internal byte[] Secret => secret; } } diff --git a/crypto/test/src/crypto/test/Argon2Test.cs b/crypto/test/src/crypto/test/Argon2Test.cs index 23a356100..eb3a94693 100644 --- a/crypto/test/src/crypto/test/Argon2Test.cs +++ b/crypto/test/src/crypto/test/Argon2Test.cs @@ -1,84 +1,46 @@ -using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Text; + +using NUnit.Framework; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.Encoders; -using Org.BouncyCastle.Utilities.Test; -using System; -using System.Collections.Generic; -using System.Text; namespace Org.BouncyCastle.Crypto.Tests { [TestFixture] public class Argon2Test - : SimpleTest { - private const int DEFAULT_OUTPUTLEN = 32; - - public override string Name => "ArgonTest"; - public override void PerformTest() - { - TestExceptions(); - TestPermutations(); - TestVectorsFromSpecs(); - HashTestsVersion10(); - HashTestsVersion13(); - } + private const int DefaultOutputLen = 32; #region "Exception tests" [Test] public void TestExceptions() { // lanes less than MIN_PARALLELISM - Assert.Throws(() => { - Argon2BytesGenerator gen = new Argon2BytesGenerator(); - Argon2Parameters.Builder builder = new Argon2Parameters.Builder() - .WithParallelism(0); - - gen.Init(builder.Build()); - }); + CheckInvalidConfig(b => b.WithParallelism(0)); // lanes greater than MAX_PARALLELISM - Assert.Throws(() => { - Argon2BytesGenerator gen = new Argon2BytesGenerator(); - Argon2Parameters.Builder builder = new Argon2Parameters.Builder() - .WithParallelism(16777299); - - gen.Init(builder.Build()); - }); + CheckInvalidConfig(b => b.WithParallelism(16777299)); // iterations less than MIN_ITERATIONS - Assert.Throws(() => { - Argon2BytesGenerator gen = new Argon2BytesGenerator(); - Argon2Parameters.Builder builder = new Argon2Parameters.Builder() - .WithIterations(0); - - gen.Init(builder.Build()); - }); + CheckInvalidConfig(b => b.WithIterations(0)); // memory less than 2 * lanes - Assert.Throws(() => { - Argon2BytesGenerator gen = new Argon2BytesGenerator(); - Argon2Parameters.Builder builder = new Argon2Parameters.Builder() - .WithMemoryAsKB(10) - .WithParallelism(6); - - gen.Init(builder.Build()); - }); + CheckInvalidConfig(b => b.WithMemoryAsKB(10).WithParallelism(6)); // output length less than MIN_OUTLEN - Assert.Throws(() => { + Assert.Throws(() => + { Argon2BytesGenerator gen = new Argon2BytesGenerator(); - Argon2Parameters.Builder builder = new Argon2Parameters.Builder(); - - gen.Init(builder.Build()); - + Argon2Parameters parameters = new Argon2Parameters.Builder().Build(); + gen.Init(parameters); byte[] result = new byte[3]; - gen.GenerateBytes("password", result); + gen.GenerateBytes("password".ToCharArray(), result); }); - } #endregion @@ -88,33 +50,33 @@ public void TestPermutations() { byte[] rootPassword = Encoding.ASCII.GetBytes("aac"); byte[] buf; - + byte[][] salts = new byte[][] { new byte[16], new byte[16], - new byte[16] + new byte[16] }; - for (int t = 0; t< 16; t++) + for (int t = 0; t < 16; t++) { - salts[1][t] = (byte) t; - salts[2][t] = (byte) (16 - t); + salts[1][t] = (byte)t; + salts[2][t] = (byte)(16 - t); } - + // // Permutation, starting with a shorter array, same length then one longer. // - for (int j = rootPassword.Length - 1; j permutations = new List(); Permute(permutations, buf, 0, buf.Length - 1); @@ -124,20 +86,32 @@ public void TestPermutations() for (int k = 0; k != salts.Length; k++) { byte[] salt = salts[k]; - byte[] expected = Generate(Argon2Parameters.ARGON2_VERSION_10, 1, 8, 2, rootPassword, salt, 32); - byte[] testValue = Generate(Argon2Parameters.ARGON2_VERSION_10, 1, 8, 2, candidate, salt, 32); + byte[] expected = Generate(Argon2Parameters.Argon2_Version10, 1, 8, 2, rootPassword, salt, 32); + byte[] testValue = Generate(Argon2Parameters.Argon2_Version10, 1, 8, 2, candidate, salt, 32); // // If the passwords are the same for the same salt we should have the same string. // bool sameAsRoot = Arrays.AreEqual(rootPassword, candidate); - IsTrue("expected same result", sameAsRoot == Arrays.AreEqual(expected, testValue)); + Assert.AreEqual(sameAsRoot, Arrays.AreEqual(expected, testValue), "expected same result"); } } } } } + private static void CheckInvalidConfig(Action config) + { + Assert.Throws(() => + { + var generator = new Argon2BytesGenerator(); + var builder = new Argon2Parameters.Builder(); + config(builder); + var parameters = builder.Build(); + generator.Init(parameters); + }); + } + private static void Swap(byte[] buf, int i, int j) { byte b = buf[i]; @@ -167,26 +141,25 @@ private static void Permute(List permutation, byte[] a, int l, int r) } } } - - private static byte[] Generate(int version, int iterations, int memory, int parallelism, - byte[] password, byte[] salt, int outputLength) + + private static byte[] Generate(int version, int iterations, int memory, int parallelism, byte[] password, + byte[] salt, int outputLength) { - Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i) + Argon2Parameters parameters = new Argon2Parameters.Builder(Argon2Parameters.Argon2_i) .WithVersion(version) .WithIterations(iterations) .WithMemoryPowOfTwo(memory) .WithParallelism(parallelism) - .WithSalt(salt); + .WithSalt(salt) + .Build(); // // Set the password. // Argon2BytesGenerator gen = new Argon2BytesGenerator(); - - gen.Init(builder.Build()); + gen.Init(parameters); byte[] result = new byte[outputLength]; - gen.GenerateBytes(password, result, 0, result.Length); return result; } @@ -199,112 +172,110 @@ public void HashTestsVersion10() { /* Multiple test cases for various input values */ - int version = Argon2Parameters.ARGON2_VERSION_10; + int version = Argon2Parameters.Argon2_Version10; HashTest(version, 2, 16, 1, "password", "somesalt", "f6c4db4a54e2a370627aff3db6176b94a2a209a62c8e36152711802f7b30c694", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 20, 1, "password", "somesalt", "9690ec55d28d3ed32562f2e73ea62b02b018757643a2ae6e79528459de8106e9", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 18, 1, "password", "somesalt", "3e689aaa3d28a77cf2bc72a51ac53166761751182f1ee292e3f677a7da4c2467", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 8, 1, "password", "somesalt", "fd4dd83d762c49bdeaf57c47bdcd0c2f1babf863fdeb490df63ede9975fccf06", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 8, 2, "password", "somesalt", "b6c11560a6a9d61eac706b79a2f97d68b4463aa3ad87e00c07e2b01e90c564fb", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 1, 16, 1, "password", "somesalt", "81630552b8f3b1f48cdb1992c4c678643d490b2b5eb4ff6c4b3438b5621724b2", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 4, 16, 1, "password", "somesalt", "f212f01615e6eb5d74734dc3ef40ade2d51d052468d8c69440a3a1f2c1c2847b", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 16, 1, "differentpassword", "somesalt", "e9c902074b6754531a3a0be519e5baf404b30ce69b3f01ac3bf21229960109a3", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 16, 1, "password", "diffsalt", "79a103b90fe8aef8570cb31fc8b22259778916f8336b7bdac3892569d4f1c497", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 16, 1, "password", "diffsalt", "1a097a5d1c80e579583f6e19c7e4763ccb7c522ca85b7d58143738e12ca39f8e6e42734c950ff2463675b97c37ba" + "39feba4a9cd9cc5b4c798f2aaf70eb4bd044c8d148decb569870dbd923430b82a083f284beae777812cce18cdac68ee8ccef" + "c6ec9789f30a6b5a034591f51af830f4", 112); - } [Test] public void HashTestsVersion13() { - int version = Argon2Parameters.ARGON2_VERSION_13; + int version = Argon2Parameters.Argon2_Version13; HashTest(version, 2, 16, 1, "password", "somesalt", "c1628832147d9720c5bd1cfd61367078729f6dfb6f8fea9ff98158e0d7816ed0", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 20, 1, "password", "somesalt", "d1587aca0922c3b5d6a83edab31bee3c4ebaef342ed6127a55d19b2351ad1f41", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 18, 1, "password", "somesalt", "296dbae80b807cdceaad44ae741b506f14db0959267b183b118f9b24229bc7cb", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 8, 1, "password", "somesalt", "89e9029f4637b295beb027056a7336c414fadd43f6b208645281cb214a56452f", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 8, 2, "password", "somesalt", "4ff5ce2769a1d7f4c8a491df09d41a9fbe90e5eb02155a13e4c01e20cd4eab61", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 1, 16, 1, "password", "somesalt", "d168075c4d985e13ebeae560cf8b94c3b5d8a16c51916b6f4ac2da3ac11bbecf", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 4, 16, 1, "password", "somesalt", "aaa953d58af3706ce3df1aefd4a64a84e31d7f54175231f1285259f88174ce5b", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 16, 1, "differentpassword", "somesalt", "14ae8da01afea8700c2358dcef7c5358d9021282bd88663a4562f59fb74d22ee", - DEFAULT_OUTPUTLEN); + DefaultOutputLen); HashTest(version, 2, 16, 1, "password", "diffsalt", "b0357cccfbef91f3860b0dba447b2348cbefecadaf990abfe9cc40726c521271", - DEFAULT_OUTPUTLEN); - + DefaultOutputLen); } - private void HashTest(int version, int iterations, int memory, int parallelism, - string password, string salt, String passwordRef, int outputLength) + private void HashTest(int version, int iterations, int memory, int parallelism, string password, string salt, + string passwordRef, int outputLength) { - Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i) + Argon2Parameters parameters = new Argon2Parameters.Builder(Argon2Parameters.Argon2_i) .WithVersion(version) .WithIterations(iterations) .WithMemoryPowOfTwo(memory) .WithParallelism(parallelism) - .WithSalt(Encoding.ASCII.GetBytes(salt)); + .WithSalt(Encoding.ASCII.GetBytes(salt)) + .Build(); Argon2BytesGenerator gen = new Argon2BytesGenerator(); - - gen.Init(builder.Build()); + gen.Init(parameters); byte[] result = new byte[outputLength]; - gen.GenerateBytes(password, result, 0, result.Length); + gen.GenerateBytes(password.ToCharArray(), result, 0, result.Length); - IsTrue(passwordRef + " Failed", AreEqual(result, Hex.Decode(passwordRef))); + Assert.True(Arrays.AreEqual(result, Hex.Decode(passwordRef)), passwordRef + " Failed"); } #endregion @@ -317,24 +288,24 @@ private void SpecsTest(int version, int type, string passwordRef) byte[] password = Hex.Decode("0101010101010101010101010101010101010101010101010101010101010101"); byte[] expected = Hex.Decode(passwordRef); - byte[] result = new byte[32]; - Argon2Parameters.Builder builder = new Argon2Parameters.Builder(type) + Argon2Parameters parameters = new Argon2Parameters.Builder(type) .WithVersion(version) .WithIterations(3) .WithMemoryAsKB(32) .WithParallelism(4) .WithAdditional(ad) .WithSecret(secret) - .WithSalt(salt); + .WithSalt(salt) + .Build(); Argon2BytesGenerator gen = new Argon2BytesGenerator(); + gen.Init(parameters); - gen.Init(builder.Build()); - + byte[] result = new byte[32]; gen.GenerateBytes(password, result, 0, result.Length); - IsTrue(passwordRef + " Failed", AreEqual(result, expected)); + Assert.True(Arrays.AreEqual(expected, result), passwordRef + " Failed"); } [Test] @@ -342,34 +313,34 @@ public void TestVectorsFromSpecs() { /* Version 0x13 (19) from RFC 9106 https://datatracker.ietf.org/doc/html/rfc9106#name-test-vectors */ SpecsTest( - Argon2Parameters.ARGON2_VERSION_13, - Argon2Parameters.ARGON2_d, + Argon2Parameters.Argon2_Version13, + Argon2Parameters.Argon2_d, "512b391b6f1162975371d30919734294f868e3be3984f3c1a13a4db9fabe4acb"); SpecsTest( - Argon2Parameters.ARGON2_VERSION_13, - Argon2Parameters.ARGON2_i, + Argon2Parameters.Argon2_Version13, + Argon2Parameters.Argon2_i, "c814d9d1dc7f37aa13f0d77f2494bda1c8de6b016dd388d29952a4c4672b6ce8"); SpecsTest( - Argon2Parameters.ARGON2_VERSION_13, - Argon2Parameters.ARGON2_id, + Argon2Parameters.Argon2_Version13, + Argon2Parameters.Argon2_id, "0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659"); /* Version 0x10 (16) from reference C implementation https://github.com/P-H-C/phc-winner-argon2/tree/master/kats */ SpecsTest( - Argon2Parameters.ARGON2_VERSION_10, - Argon2Parameters.ARGON2_d, + Argon2Parameters.Argon2_Version10, + Argon2Parameters.Argon2_d, "96a9d4e5a1734092c85e29f410a45914a5dd1f5cbf08b2670da68a0285abf32b"); SpecsTest( - Argon2Parameters.ARGON2_VERSION_10, - Argon2Parameters.ARGON2_i, + Argon2Parameters.Argon2_Version10, + Argon2Parameters.Argon2_i, "87aeedd6517ab830cd9765cd8231abb2e647a5dee08f7c05e02fcb763335d0fd"); SpecsTest( - Argon2Parameters.ARGON2_VERSION_10, - Argon2Parameters.ARGON2_id, + Argon2Parameters.Argon2_Version10, + Argon2Parameters.Argon2_id, "b64615f07789b66b645b67ee9ed3b377ae350b6bfcbb0fc95141ea8f322613c0"); } #endregion From 55361bdd7420747929196922de30a64539bf9c6a Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 3 Oct 2024 19:45:02 +0200 Subject: [PATCH 31/37] Followup changes to Argon2 in OpenPGP --- crypto/src/openpgp/PgpUtilities.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crypto/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index 477e9e906..559b70007 100644 --- a/crypto/src/openpgp/PgpUtilities.cs +++ b/crypto/src/openpgp/PgpUtilities.cs @@ -343,8 +343,8 @@ internal static KeyParameter DoMakeKeyFromPassPhrase(SymmetricKeyAlgorithmTag al if (s2k != null && s2k.Type == S2k.Argon2) { Argon2Parameters.Builder builder = - new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id) - .WithVersion(Argon2Parameters.ARGON2_VERSION_13) + new Argon2Parameters.Builder(Argon2Parameters.Argon2_id) + .WithVersion(Argon2Parameters.Argon2_Version13) .WithIterations(s2k.Passes) .WithMemoryPowOfTwo(s2k.MemorySizeExponent) .WithParallelism(s2k.Parallelism) From f9182de615ca1aadebc349727978fb99d2b9455e Mon Sep 17 00:00:00 2001 From: Peter Dettman Date: Thu, 3 Oct 2024 18:44:18 +0700 Subject: [PATCH 32/37] Add XorBothTo methods to Nat(512) --- crypto/src/math/raw/Nat.cs | 82 ++++++++ crypto/src/math/raw/Nat512.cs | 344 ++++++++++++++++++++++++++++++---- 2 files changed, 387 insertions(+), 39 deletions(-) diff --git a/crypto/src/math/raw/Nat.cs b/crypto/src/math/raw/Nat.cs index b524750d8..48bf3b2e1 100644 --- a/crypto/src/math/raw/Nat.cs +++ b/crypto/src/math/raw/Nat.cs @@ -2930,6 +2930,88 @@ public static void Xor64(int len, ReadOnlySpan x, ReadOnlySpan y, } #endif + public static void XorBothTo(int len, uint[] x, uint[] y, uint[] z) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + XorBothTo(len, x.AsSpan(0, len), y.AsSpan(0, len), z.AsSpan(0, len)); +#else + for (int i = 0; i < len; ++i) + { + z[i] ^= x[i] ^ y[i]; + } +#endif + } + + public static void XorBothTo(int len, uint[] x, int xOff, uint[] y, int yOff, uint[] z, int zOff) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + XorBothTo(len, x.AsSpan(xOff, len), y.AsSpan(yOff, len), z.AsSpan(zOff, len)); +#else + for (int i = 0; i < len; ++i) + { + z[zOff + i] ^= x[xOff + i] ^ y[yOff + i]; + } +#endif + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public static void XorBothTo(int len, ReadOnlySpan x, ReadOnlySpan y, Span z) + { + int i = 0, limit16 = len - 16; + while (i <= limit16) + { + Nat512.XorBothTo(x[i..], y[i..], z[i..]); + i += 16; + } + while (i < len) + { + z[i] ^= x[i] ^ y[i]; + ++i; + } + } +#endif + + public static void XorBothTo64(int len, ulong[] x, ulong[] y, ulong[] z) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + XorBothTo64(len, x.AsSpan(0, len), y.AsSpan(0, len), z.AsSpan(0, len)); +#else + for (int i = 0; i < len; ++i) + { + z[i] ^= x[i] ^ y[i]; + } +#endif + } + + public static void XorBothTo64(int len, ulong[] x, int xOff, ulong[] y, int yOff, ulong[] z, int zOff) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + XorBothTo64(len, x.AsSpan(xOff, len), y.AsSpan(yOff, len), z.AsSpan(zOff, len)); +#else + for (int i = 0; i < len; ++i) + { + z[zOff + i] ^= x[xOff + i] ^ y[yOff + i]; + } +#endif + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public static void XorBothTo64(int len, ReadOnlySpan x, ReadOnlySpan y, Span z) + { + int i = 0, limit8 = len - 8; + while (i <= limit8) + { + Nat512.XorBothTo64(x[i..], y[i..], z[i..]); + i += 8; + } + while (i < len) + { + z[i] ^= x[i] ^ y[i]; + ++i; + } + } +#endif + public static void XorTo(int len, uint[] x, uint[] z) { #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER diff --git a/crypto/src/math/raw/Nat512.cs b/crypto/src/math/raw/Nat512.cs index 71b53214c..8e98c37df 100644 --- a/crypto/src/math/raw/Nat512.cs +++ b/crypto/src/math/raw/Nat512.cs @@ -48,6 +48,21 @@ public static void Square(uint[] x, uint[] zz) Nat.AddWordAt(32, c24, zz, 24); } + public static void Xor(uint[] x, uint[] y, uint[] z) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + Xor(x.AsSpan(), y.AsSpan(), z.AsSpan()); +#else + for (int i = 0; i < 16; i += 4) + { + z[i + 0] = x[i + 0] ^ y[i + 0]; + z[i + 1] = x[i + 1] ^ y[i + 1]; + z[i + 2] = x[i + 2] ^ y[i + 2]; + z[i + 3] = x[i + 3] ^ y[i + 3]; + } +#endif + } + public static void Xor(uint[] x, int xOff, uint[] y, int yOff, uint[] z, int zOff) { #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER @@ -128,36 +143,52 @@ public static void Xor(ReadOnlySpan x, ReadOnlySpan y, Span z) } #endif - public static void XorTo(uint[] x, int xOff, uint[] z, int zOff) + public static void Xor64(ulong[] x, ulong[] y, ulong[] z) { #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - XorTo(x.AsSpan(xOff), z.AsSpan(zOff)); + Xor64(x.AsSpan(), y.AsSpan(), z.AsSpan()); #else - for (int i = 0; i < 16; i += 4) + for (int i = 0; i < 8; i += 4) { - z[zOff + i + 0] ^= x[xOff + i + 0]; - z[zOff + i + 1] ^= x[xOff + i + 1]; - z[zOff + i + 2] ^= x[xOff + i + 2]; - z[zOff + i + 3] ^= x[xOff + i + 3]; + z[i + 0] = x[i + 0] ^ y[i + 0]; + z[i + 1] = x[i + 1] ^ y[i + 1]; + z[i + 2] = x[i + 2] ^ y[i + 2]; + z[i + 3] = x[i + 3] ^ y[i + 3]; } #endif } + public static void Xor64(ulong[] x, int xOff, ulong[] y, int yOff, ulong[] z, int zOff) + { #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - public static void XorTo(ReadOnlySpan x, Span z) + Xor64(x.AsSpan(xOff), y.AsSpan(yOff), z.AsSpan(zOff)); +#else + for (int i = 0; i < 8; i += 4) + { + z[zOff + i + 0] = x[xOff + i + 0] ^ y[yOff + i + 0]; + z[zOff + i + 1] = x[xOff + i + 1] ^ y[yOff + i + 1]; + z[zOff + i + 2] = x[xOff + i + 2] ^ y[yOff + i + 2]; + z[zOff + i + 3] = x[xOff + i + 3] ^ y[yOff + i + 3]; + } +#endif + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public static void Xor64(ReadOnlySpan x, ReadOnlySpan y, Span z) { #if NETCOREAPP3_0_OR_GREATER if (Org.BouncyCastle.Runtime.Intrinsics.X86.Avx2.IsEnabled && Org.BouncyCastle.Runtime.Intrinsics.Vector.IsPacked) { - var X = MemoryMarshal.AsBytes(x[..16]); - var Z = MemoryMarshal.AsBytes(z[..16]); + var X = MemoryMarshal.AsBytes(x[..8]); + var Y = MemoryMarshal.AsBytes(y[..8]); + var Z = MemoryMarshal.AsBytes(z[..8]); var X0 = MemoryMarshal.Read>(X[0x00..0x20]); var X1 = MemoryMarshal.Read>(X[0x20..0x40]); - var Y0 = MemoryMarshal.Read>(Z[0x00..0x20]); - var Y1 = MemoryMarshal.Read>(Z[0x20..0x40]); + var Y0 = MemoryMarshal.Read>(Y[0x00..0x20]); + var Y1 = MemoryMarshal.Read>(Y[0x20..0x40]); var Z0 = Avx2.Xor(X0, Y0); var Z1 = Avx2.Xor(X1, Y1); @@ -170,18 +201,19 @@ public static void XorTo(ReadOnlySpan x, Span z) if (Org.BouncyCastle.Runtime.Intrinsics.X86.Sse2.IsEnabled && Org.BouncyCastle.Runtime.Intrinsics.Vector.IsPacked) { - var X = MemoryMarshal.AsBytes(x[..16]); - var Z = MemoryMarshal.AsBytes(z[..16]); + var X = MemoryMarshal.AsBytes(x[..8]); + var Y = MemoryMarshal.AsBytes(y[..8]); + var Z = MemoryMarshal.AsBytes(z[..8]); var X0 = MemoryMarshal.Read>(X[0x00..0x10]); var X1 = MemoryMarshal.Read>(X[0x10..0x20]); var X2 = MemoryMarshal.Read>(X[0x20..0x30]); var X3 = MemoryMarshal.Read>(X[0x30..0x40]); - var Y0 = MemoryMarshal.Read>(Z[0x00..0x10]); - var Y1 = MemoryMarshal.Read>(Z[0x10..0x20]); - var Y2 = MemoryMarshal.Read>(Z[0x20..0x30]); - var Y3 = MemoryMarshal.Read>(Z[0x30..0x40]); + var Y0 = MemoryMarshal.Read>(Y[0x00..0x10]); + var Y1 = MemoryMarshal.Read>(Y[0x10..0x20]); + var Y2 = MemoryMarshal.Read>(Y[0x20..0x30]); + var Y3 = MemoryMarshal.Read>(Y[0x30..0x40]); var Z0 = Sse2.Xor(X0, Y0); var Z1 = Sse2.Xor(X1, Y1); @@ -196,33 +228,151 @@ public static void XorTo(ReadOnlySpan x, Span z) } #endif + for (int i = 0; i < 8; i += 4) + { + z[i + 0] = x[i + 0] ^ y[i + 0]; + z[i + 1] = x[i + 1] ^ y[i + 1]; + z[i + 2] = x[i + 2] ^ y[i + 2]; + z[i + 3] = x[i + 3] ^ y[i + 3]; + } + } +#endif + + public static void XorBothTo(uint[] x, uint[] y, uint[] z) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + XorBothTo(x.AsSpan(), y.AsSpan(), z.AsSpan()); +#else for (int i = 0; i < 16; i += 4) { - z[i + 0] ^= x[i + 0]; - z[i + 1] ^= x[i + 1]; - z[i + 2] ^= x[i + 2]; - z[i + 3] ^= x[i + 3]; + z[i + 0] ^= x[i + 0] ^ y[i + 0]; + z[i + 1] ^= x[i + 1] ^ y[i + 1]; + z[i + 2] ^= x[i + 2] ^ y[i + 2]; + z[i + 3] ^= x[i + 3] ^ y[i + 3]; } +#endif } + + public static void XorBothTo(uint[] x, int xOff, uint[] y, int yOff, uint[] z, int zOff) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + XorBothTo(x.AsSpan(xOff), y.AsSpan(yOff), z.AsSpan(zOff)); +#else + for (int i = 0; i < 16; i += 4) + { + z[zOff + i + 0] ^= x[xOff + i + 0] ^ y[yOff + i + 0]; + z[zOff + i + 1] ^= x[xOff + i + 1] ^ y[yOff + i + 1]; + z[zOff + i + 2] ^= x[xOff + i + 2] ^ y[yOff + i + 2]; + z[zOff + i + 3] ^= x[xOff + i + 3] ^ y[yOff + i + 3]; + } #endif + } - public static void Xor64(ulong[] x, int xOff, ulong[] y, int yOff, ulong[] z, int zOff) +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public static void XorBothTo(ReadOnlySpan x, ReadOnlySpan y, Span z) + { +#if NETCOREAPP3_0_OR_GREATER + if (Org.BouncyCastle.Runtime.Intrinsics.X86.Avx2.IsEnabled && + Org.BouncyCastle.Runtime.Intrinsics.Vector.IsPacked) + { + var X = MemoryMarshal.AsBytes(x[..16]); + var Y = MemoryMarshal.AsBytes(y[..16]); + var Z = MemoryMarshal.AsBytes(z[..16]); + + var X0 = MemoryMarshal.Read>(X[0x00..0x20]); + var X1 = MemoryMarshal.Read>(X[0x20..0x40]); + + var Y0 = MemoryMarshal.Read>(Y[0x00..0x20]); + var Y1 = MemoryMarshal.Read>(Y[0x20..0x40]); + + var Z0 = MemoryMarshal.Read>(Z[0x00..0x20]); + var Z1 = MemoryMarshal.Read>(Z[0x20..0x40]); + + Z0 = Avx2.Xor(Z0, Avx2.Xor(X0, Y0)); + Z1 = Avx2.Xor(Z1, Avx2.Xor(X1, Y1)); + + MemoryMarshal.Write(Z[0x00..0x20], ref Z0); + MemoryMarshal.Write(Z[0x20..0x40], ref Z1); + return; + } + + if (Org.BouncyCastle.Runtime.Intrinsics.X86.Sse2.IsEnabled && + Org.BouncyCastle.Runtime.Intrinsics.Vector.IsPacked) + { + var X = MemoryMarshal.AsBytes(x[..16]); + var Y = MemoryMarshal.AsBytes(y[..16]); + var Z = MemoryMarshal.AsBytes(z[..16]); + + var X0 = MemoryMarshal.Read>(X[0x00..0x10]); + var X1 = MemoryMarshal.Read>(X[0x10..0x20]); + var X2 = MemoryMarshal.Read>(X[0x20..0x30]); + var X3 = MemoryMarshal.Read>(X[0x30..0x40]); + + var Y0 = MemoryMarshal.Read>(Y[0x00..0x10]); + var Y1 = MemoryMarshal.Read>(Y[0x10..0x20]); + var Y2 = MemoryMarshal.Read>(Y[0x20..0x30]); + var Y3 = MemoryMarshal.Read>(Y[0x30..0x40]); + + var Z0 = MemoryMarshal.Read>(Z[0x00..0x10]); + var Z1 = MemoryMarshal.Read>(Z[0x10..0x20]); + var Z2 = MemoryMarshal.Read>(Z[0x20..0x30]); + var Z3 = MemoryMarshal.Read>(Z[0x30..0x40]); + + Z0 = Sse2.Xor(Z0, Sse2.Xor(X0, Y0)); + Z1 = Sse2.Xor(Z1, Sse2.Xor(X1, Y1)); + Z2 = Sse2.Xor(Z2, Sse2.Xor(X2, Y2)); + Z3 = Sse2.Xor(Z3, Sse2.Xor(X3, Y3)); + + MemoryMarshal.Write(Z[0x00..0x10], ref Z0); + MemoryMarshal.Write(Z[0x10..0x20], ref Z1); + MemoryMarshal.Write(Z[0x20..0x30], ref Z2); + MemoryMarshal.Write(Z[0x30..0x40], ref Z3); + return; + } +#endif + + for (int i = 0; i < 16; i += 4) + { + z[i + 0] ^= x[i + 0] ^ y[i + 0]; + z[i + 1] ^= x[i + 1] ^ y[i + 1]; + z[i + 2] ^= x[i + 2] ^ y[i + 2]; + z[i + 3] ^= x[i + 3] ^ y[i + 3]; + } + } +#endif + + public static void XorBothTo64(ulong[] x, ulong[] y, ulong[] z) { #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - Xor64(x.AsSpan(xOff), y.AsSpan(yOff), z.AsSpan(zOff)); + XorBothTo64(x.AsSpan(), y.AsSpan(), z.AsSpan()); #else for (int i = 0; i < 8; i += 4) { - z[zOff + i + 0] = x[xOff + i + 0] ^ y[yOff + i + 0]; - z[zOff + i + 1] = x[xOff + i + 1] ^ y[yOff + i + 1]; - z[zOff + i + 2] = x[xOff + i + 2] ^ y[yOff + i + 2]; - z[zOff + i + 3] = x[xOff + i + 3] ^ y[yOff + i + 3]; + z[i + 0] ^= x[i + 0] ^ y[i + 0]; + z[i + 1] ^= x[i + 1] ^ y[i + 1]; + z[i + 2] ^= x[i + 2] ^ y[i + 2]; + z[i + 3] ^= x[i + 3] ^ y[i + 3]; } #endif } + public static void XorBothTo64(ulong[] x, int xOff, ulong[] y, int yOff, ulong[] z, int zOff) + { #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - public static void Xor64(ReadOnlySpan x, ReadOnlySpan y, Span z) + XorBothTo64(x.AsSpan(xOff), y.AsSpan(yOff), z.AsSpan(zOff)); +#else + for (int i = 0; i < 8; i += 4) + { + z[zOff + i + 0] ^= x[xOff + i + 0] ^ y[yOff + i + 0]; + z[zOff + i + 1] ^= x[xOff + i + 1] ^ y[yOff + i + 1]; + z[zOff + i + 2] ^= x[xOff + i + 2] ^ y[yOff + i + 2]; + z[zOff + i + 3] ^= x[xOff + i + 3] ^ y[yOff + i + 3]; + } +#endif + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public static void XorBothTo64(ReadOnlySpan x, ReadOnlySpan y, Span z) { #if NETCOREAPP3_0_OR_GREATER if (Org.BouncyCastle.Runtime.Intrinsics.X86.Avx2.IsEnabled && @@ -238,8 +388,11 @@ public static void Xor64(ReadOnlySpan x, ReadOnlySpan y, Span>(Y[0x00..0x20]); var Y1 = MemoryMarshal.Read>(Y[0x20..0x40]); - var Z0 = Avx2.Xor(X0, Y0); - var Z1 = Avx2.Xor(X1, Y1); + var Z0 = MemoryMarshal.Read>(Z[0x00..0x20]); + var Z1 = MemoryMarshal.Read>(Z[0x20..0x40]); + + Z0 = Avx2.Xor(Z0, Avx2.Xor(X0, Y0)); + Z1 = Avx2.Xor(Z1, Avx2.Xor(X1, Y1)); MemoryMarshal.Write(Z[0x00..0x20], ref Z0); MemoryMarshal.Write(Z[0x20..0x40], ref Z1); @@ -263,10 +416,15 @@ public static void Xor64(ReadOnlySpan x, ReadOnlySpan y, Span>(Y[0x20..0x30]); var Y3 = MemoryMarshal.Read>(Y[0x30..0x40]); - var Z0 = Sse2.Xor(X0, Y0); - var Z1 = Sse2.Xor(X1, Y1); - var Z2 = Sse2.Xor(X2, Y2); - var Z3 = Sse2.Xor(X3, Y3); + var Z0 = MemoryMarshal.Read>(Z[0x00..0x10]); + var Z1 = MemoryMarshal.Read>(Z[0x10..0x20]); + var Z2 = MemoryMarshal.Read>(Z[0x20..0x30]); + var Z3 = MemoryMarshal.Read>(Z[0x30..0x40]); + + Z0 = Sse2.Xor(Z0, Sse2.Xor(X0, Y0)); + Z1 = Sse2.Xor(Z1, Sse2.Xor(X1, Y1)); + Z2 = Sse2.Xor(Z2, Sse2.Xor(X2, Y2)); + Z3 = Sse2.Xor(Z3, Sse2.Xor(X3, Y3)); MemoryMarshal.Write(Z[0x00..0x10], ref Z0); MemoryMarshal.Write(Z[0x10..0x20], ref Z1); @@ -278,14 +436,122 @@ public static void Xor64(ReadOnlySpan x, ReadOnlySpan y, Span x, Span z) + { +#if NETCOREAPP3_0_OR_GREATER + if (Org.BouncyCastle.Runtime.Intrinsics.X86.Avx2.IsEnabled && + Org.BouncyCastle.Runtime.Intrinsics.Vector.IsPacked) + { + var X = MemoryMarshal.AsBytes(x[..16]); + var Z = MemoryMarshal.AsBytes(z[..16]); + + var X0 = MemoryMarshal.Read>(X[0x00..0x20]); + var X1 = MemoryMarshal.Read>(X[0x20..0x40]); + + var Z0 = MemoryMarshal.Read>(Z[0x00..0x20]); + var Z1 = MemoryMarshal.Read>(Z[0x20..0x40]); + + Z0 = Avx2.Xor(Z0, X0); + Z1 = Avx2.Xor(Z1, X1); + + MemoryMarshal.Write(Z[0x00..0x20], ref Z0); + MemoryMarshal.Write(Z[0x20..0x40], ref Z1); + return; + } + + if (Org.BouncyCastle.Runtime.Intrinsics.X86.Sse2.IsEnabled && + Org.BouncyCastle.Runtime.Intrinsics.Vector.IsPacked) + { + var X = MemoryMarshal.AsBytes(x[..16]); + var Z = MemoryMarshal.AsBytes(z[..16]); + + var X0 = MemoryMarshal.Read>(X[0x00..0x10]); + var X1 = MemoryMarshal.Read>(X[0x10..0x20]); + var X2 = MemoryMarshal.Read>(X[0x20..0x30]); + var X3 = MemoryMarshal.Read>(X[0x30..0x40]); + + var Z0 = MemoryMarshal.Read>(Z[0x00..0x10]); + var Z1 = MemoryMarshal.Read>(Z[0x10..0x20]); + var Z2 = MemoryMarshal.Read>(Z[0x20..0x30]); + var Z3 = MemoryMarshal.Read>(Z[0x30..0x40]); + + Z0 = Sse2.Xor(Z0, X0); + Z1 = Sse2.Xor(Z1, X1); + Z2 = Sse2.Xor(Z2, X2); + Z3 = Sse2.Xor(Z3, X3); + + MemoryMarshal.Write(Z[0x00..0x10], ref Z0); + MemoryMarshal.Write(Z[0x10..0x20], ref Z1); + MemoryMarshal.Write(Z[0x20..0x30], ref Z2); + MemoryMarshal.Write(Z[0x30..0x40], ref Z3); + return; + } +#endif + + for (int i = 0; i < 16; i += 4) + { + z[i + 0] ^= x[i + 0]; + z[i + 1] ^= x[i + 1]; + z[i + 2] ^= x[i + 2]; + z[i + 3] ^= x[i + 3]; + } + } +#endif + + public static void XorTo64(ulong[] x, ulong[] z) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + XorTo64(x.AsSpan(), z.AsSpan()); +#else + for (int i = 0; i < 8; i += 4) + { + z[i + 0] ^= x[i + 0]; + z[i + 1] ^= x[i + 1]; + z[i + 2] ^= x[i + 2]; + z[i + 3] ^= x[i + 3]; + } +#endif + } + public static void XorTo64(ulong[] x, int xOff, ulong[] z, int zOff) { #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER From d396a19d59b6abe3c84f5bcad340c17925848e73 Mon Sep 17 00:00:00 2001 From: Peter Dettman Date: Thu, 3 Oct 2024 19:14:08 +0700 Subject: [PATCH 33/37] Refactoring in Argon2 --- .../crypto/generators/Argon2BytesGenerator.cs | 96 +++++++++---------- .../src/crypto/parameters/Argon2Parameters.cs | 30 +++--- crypto/test/src/crypto/test/Argon2Test.cs | 45 ++++----- 3 files changed, 80 insertions(+), 91 deletions(-) diff --git a/crypto/src/crypto/generators/Argon2BytesGenerator.cs b/crypto/src/crypto/generators/Argon2BytesGenerator.cs index 3800ffb12..b16d0c417 100644 --- a/crypto/src/crypto/generators/Argon2BytesGenerator.cs +++ b/crypto/src/crypto/generators/Argon2BytesGenerator.cs @@ -22,7 +22,7 @@ public sealed class Argon2BytesGenerator /* Minimum and maximum number of lanes (degree of parallelism) */ private const int MinParallelism = 1; - private const int MaxParallelism = 16777216; + private const int MaxParallelism = (1 << 24) - 1; /* Minimum and maximum digest size in bytes */ private const int MinOutlen = 4; @@ -50,18 +50,43 @@ public Argon2BytesGenerator() */ public void Init(Argon2Parameters parameters) { - this.parameters = parameters; + if (parameters.Version != Argon2Parameters.Version10 && + parameters.Version != Argon2Parameters.Version13) + { + throw new NotSupportedException("unknown Argon2 version"); + } + if (parameters.Type != Argon2Parameters.Argon2d && + parameters.Type != Argon2Parameters.Argon2i && + parameters.Type != Argon2Parameters.Argon2id) + { + throw new NotSupportedException("unknown Argon2 type"); + } - if (parameters.Lanes < MinParallelism) - throw new InvalidOperationException($"lanes must be greater than " + MinParallelism); - if (parameters.Lanes > MaxParallelism) - throw new InvalidOperationException("lanes must be less than " + MaxParallelism); - if (parameters.Memory < 2 * parameters.Lanes) - throw new InvalidOperationException("memory is less than: " + (2 * parameters.Lanes) + " expected " + (2 * parameters.Lanes)); + if (parameters.Parallelism < MinParallelism) + throw new InvalidOperationException("parallelism must be at least " + MinParallelism); + if (parameters.Parallelism > MaxParallelism) + throw new InvalidOperationException("parallelism must be at most " + MaxParallelism); if (parameters.Iterations < MinIterations) - throw new InvalidOperationException("iterations is less than: " + MinIterations); + throw new InvalidOperationException("iterations must be at least " + MinIterations); + + this.parameters = parameters; + + // 2. Align memory size + // Minimum memoryBlocks = 8L blocks, where L is the number of lanes + int memoryBlocks = System.Math.Max(parameters.Memory, 2 * Argon2SyncPoints * parameters.Parallelism); + + this.segmentLength = memoryBlocks / (Argon2SyncPoints * parameters.Parallelism); + this.laneLength = segmentLength * Argon2SyncPoints; + + // Ensure that all segments have equal length + memoryBlocks = parameters.Parallelism * laneLength; - DoInit(parameters); + this.memory = new Block[memoryBlocks]; + + for (int i = 0; i < memory.Length; i++) + { + memory[i] = new Block(); + } } public int GenerateBytes(char[] password, byte[] output) => @@ -102,31 +127,6 @@ private void Reset() } } - private void DoInit(Argon2Parameters parameters) - { - /* 2. Align memory size */ - /* Minimum memoryBlocks = 8L blocks, where L is the number of lanes */ - int memoryBlocks = System.Math.Max(parameters.Memory, 2 * Argon2SyncPoints * parameters.Lanes); - - this.segmentLength = memoryBlocks / (parameters.Lanes * Argon2SyncPoints); - this.laneLength = segmentLength * Argon2SyncPoints; - - /* Ensure that all segments have equal length */ - memoryBlocks = segmentLength * (parameters.Lanes * Argon2SyncPoints); - - InitMemory(memoryBlocks); - } - - private void InitMemory(int memoryBlocks) - { - this.memory = new Block[memoryBlocks]; - - for (int i = 0; i < memory.Length; i++) - { - memory[i] = new Block(); - } - } - private void FillMemoryBlocks() { FillBlock filler = new FillBlock(); @@ -139,7 +139,7 @@ private void FillMemoryBlocks() { position.slice = slice; - for (int lane = 0; lane < parameters.Lanes; ++lane) + for (int lane = 0; lane < parameters.Parallelism; ++lane) { position.lane = lane; @@ -202,8 +202,8 @@ private void FillSegment(FillBlock filler, Position position) private bool IsDataIndependentAddressing(Position position) { - return (parameters.Type == Argon2Parameters.Argon2_i) || - (parameters.Type == Argon2Parameters.Argon2_id + return (parameters.Type == Argon2Parameters.Argon2i) || + (parameters.Type == Argon2Parameters.Argon2id && (position.pass == 0) && (position.slice < Argon2SyncPoints / 2) ); @@ -227,7 +227,7 @@ private void InitAddressBlocks(FillBlock filler, Position position, Block inputB private bool IsWithXor(Position position) { - return !(position.pass == 0 || parameters.Version == Argon2Parameters.Argon2_Version10); + return !(position.pass == 0 || parameters.Version == Argon2Parameters.Version10); } private int GetPrevOffset(int currentOffset) @@ -290,7 +290,7 @@ private ulong GetPseudoRandom( private int GetRefLane(Position position, ulong pseudoRandom) { - int refLane = (int)((long)(pseudoRandom >> 32) % parameters.Lanes); + int refLane = (int)((long)(pseudoRandom >> 32) % parameters.Parallelism); if ((position.pass == 0) && (position.slice == 0)) { @@ -347,7 +347,7 @@ private void Digest(byte[] tmpBlockBytes, byte[] output, int outOff, int outLen) Block finalBlock = memory[laneLength - 1]; /* XOR the last blocks */ - for (int i = 1; i < parameters.Lanes; i++) + for (int i = 1; i < parameters.Parallelism; i++) { int lastBlockInLane = i * laneLength + (laneLength - 1); finalBlock.XorWith(memory[lastBlockInLane]); @@ -479,7 +479,7 @@ private void Initialize(byte[] tmpBlockBytes, byte[] password, int outputLength) Blake2bDigest blake = new Blake2bDigest(Argon2PrehashDigestLength * 8); uint[] values = { - (uint)parameters.Lanes, + (uint)parameters.Parallelism, (uint)outputLength, (uint)parameters.Memory, (uint)parameters.Iterations, @@ -522,10 +522,9 @@ private void FillFirstBlocks(byte[] tmpBlockBytes, byte[] initialHashWithZeros) { byte[] initialHashWithOnes = new byte[Argon2PrehashSeedLength]; Array.Copy(initialHashWithZeros, 0, initialHashWithOnes, 0, Argon2PrehashDigestLength); - // Pack.intToLittleEndian(1, initialHashWithOnes, Argon2PrehashDigestLength); initialHashWithOnes[Argon2PrehashDigestLength] = 1; - for (int i = 0; i < parameters.Lanes; i++) + for (int i = 0; i < parameters.Parallelism; i++) { Pack.UInt32_To_LE((uint)i, initialHashWithZeros, Argon2PrehashDigestLength + 4); Pack.UInt32_To_LE((uint)i, initialHashWithOnes, Argon2PrehashDigestLength + 4); @@ -650,14 +649,7 @@ internal void XorWith(Block b1) internal void XorWith(Block b1, Block b2) { - // TODO New Nat.Xor variant for this - ulong[] v0 = v; - ulong[] v1 = b1.v; - ulong[] v2 = b2.v; - for (int i = 0; i < Size; i++) - { - v0[i] ^= v1[i] ^ v2[i]; - } + Nat.XorBothTo64(Size, b1.v, b2.v, v); } internal Block Clear() diff --git a/crypto/src/crypto/parameters/Argon2Parameters.cs b/crypto/src/crypto/parameters/Argon2Parameters.cs index 4da2afca0..0073b9214 100644 --- a/crypto/src/crypto/parameters/Argon2Parameters.cs +++ b/crypto/src/crypto/parameters/Argon2Parameters.cs @@ -6,12 +6,12 @@ namespace Org.BouncyCastle.Crypto.Parameters { public sealed class Argon2Parameters { - public static readonly int Argon2_d = 0x00; - public static readonly int Argon2_i = 0x01; - public static readonly int Argon2_id = 0x02; + public static readonly int Argon2d = 0x00; + public static readonly int Argon2i = 0x01; + public static readonly int Argon2id = 0x02; - public static readonly int Argon2_Version10 = 0x10; - public static readonly int Argon2_Version13 = 0x13; + public static readonly int Version10 = 0x10; + public static readonly int Version13 = 0x13; private readonly int type; private readonly byte[] salt; @@ -19,7 +19,7 @@ public sealed class Argon2Parameters private readonly byte[] additional; private readonly int iterations; private readonly int memory; - private readonly int lanes; + private readonly int parallelism; private readonly int version; private readonly ICharToByteConverter converter; @@ -27,9 +27,9 @@ public sealed class Builder { private static readonly int DefaultIterations = 3; private static readonly int DefaultMemoryCost = 12; - private static readonly int DefaultLanes = 1; - private static readonly int DefaultType = Argon2_i; - private static readonly int DefaultVersion = Argon2_Version13; + private static readonly int DefaultParallelism = 1; + private static readonly int DefaultType = Argon2i; + private static readonly int DefaultVersion = Version13; private readonly int type; @@ -38,7 +38,7 @@ public sealed class Builder private byte[] additional = Array.Empty(); private int iterations = DefaultIterations; private int memory = 1 << DefaultMemoryCost; - private int lanes = DefaultLanes; + private int parallelism = DefaultParallelism; private int version = DefaultVersion; private ICharToByteConverter converter = PasswordConverter.Utf8; @@ -54,7 +54,7 @@ public Builder(int type) public Builder WithParallelism(int parallelism) { - this.lanes = parallelism; + this.parallelism = parallelism; return this; } @@ -107,7 +107,7 @@ public Builder WithCharToByteConverter(ICharToByteConverter converter) } public Argon2Parameters Build() => - new Argon2Parameters(type, salt, secret, additional, iterations, memory, lanes, version, converter); + new Argon2Parameters(type, salt, secret, additional, iterations, memory, parallelism, version, converter); public void Clear() { @@ -118,7 +118,7 @@ public void Clear() } private Argon2Parameters(int type, byte[] salt, byte[] secret, byte[] additional, int iterations, int memory, - int lanes, int version, ICharToByteConverter converter) + int parallelism, int version, ICharToByteConverter converter) { this.type = type; this.salt = salt; @@ -126,7 +126,7 @@ private Argon2Parameters(int type, byte[] salt, byte[] secret, byte[] additional this.additional = additional; this.iterations = iterations; this.memory = memory; - this.lanes = lanes; + this.parallelism = parallelism; this.version = version; this.converter = converter; } @@ -141,7 +141,7 @@ private Argon2Parameters(int type, byte[] salt, byte[] secret, byte[] additional public int Iterations => iterations; - public int Lanes => lanes; + public int Parallelism => parallelism; public int Memory => memory; diff --git a/crypto/test/src/crypto/test/Argon2Test.cs b/crypto/test/src/crypto/test/Argon2Test.cs index eb3a94693..296d2a79c 100644 --- a/crypto/test/src/crypto/test/Argon2Test.cs +++ b/crypto/test/src/crypto/test/Argon2Test.cs @@ -20,18 +20,15 @@ public class Argon2Test [Test] public void TestExceptions() { - // lanes less than MIN_PARALLELISM + // Parallelism less than MIN_PARALLELISM CheckInvalidConfig(b => b.WithParallelism(0)); - // lanes greater than MAX_PARALLELISM - CheckInvalidConfig(b => b.WithParallelism(16777299)); + // Parallelism greater than MAX_PARALLELISM + CheckInvalidConfig(b => b.WithParallelism(1 << 24)); // iterations less than MIN_ITERATIONS CheckInvalidConfig(b => b.WithIterations(0)); - // memory less than 2 * lanes - CheckInvalidConfig(b => b.WithMemoryAsKB(10).WithParallelism(6)); - // output length less than MIN_OUTLEN Assert.Throws(() => { @@ -86,8 +83,8 @@ public void TestPermutations() for (int k = 0; k != salts.Length; k++) { byte[] salt = salts[k]; - byte[] expected = Generate(Argon2Parameters.Argon2_Version10, 1, 8, 2, rootPassword, salt, 32); - byte[] testValue = Generate(Argon2Parameters.Argon2_Version10, 1, 8, 2, candidate, salt, 32); + byte[] expected = Generate(Argon2Parameters.Version10, 1, 8, 2, rootPassword, salt, 32); + byte[] testValue = Generate(Argon2Parameters.Version10, 1, 8, 2, candidate, salt, 32); // // If the passwords are the same for the same salt we should have the same string. @@ -145,7 +142,7 @@ private static void Permute(List permutation, byte[] a, int l, int r) private static byte[] Generate(int version, int iterations, int memory, int parallelism, byte[] password, byte[] salt, int outputLength) { - Argon2Parameters parameters = new Argon2Parameters.Builder(Argon2Parameters.Argon2_i) + Argon2Parameters parameters = new Argon2Parameters.Builder(Argon2Parameters.Argon2i) .WithVersion(version) .WithIterations(iterations) .WithMemoryPowOfTwo(memory) @@ -172,7 +169,7 @@ public void HashTestsVersion10() { /* Multiple test cases for various input values */ - int version = Argon2Parameters.Argon2_Version10; + int version = Argon2Parameters.Version10; HashTest(version, 2, 16, 1, "password", "somesalt", "f6c4db4a54e2a370627aff3db6176b94a2a209a62c8e36152711802f7b30c694", @@ -219,7 +216,7 @@ public void HashTestsVersion10() [Test] public void HashTestsVersion13() { - int version = Argon2Parameters.Argon2_Version13; + int version = Argon2Parameters.Version13; HashTest(version, 2, 16, 1, "password", "somesalt", "c1628832147d9720c5bd1cfd61367078729f6dfb6f8fea9ff98158e0d7816ed0", @@ -261,7 +258,7 @@ public void HashTestsVersion13() private void HashTest(int version, int iterations, int memory, int parallelism, string password, string salt, string passwordRef, int outputLength) { - Argon2Parameters parameters = new Argon2Parameters.Builder(Argon2Parameters.Argon2_i) + Argon2Parameters parameters = new Argon2Parameters.Builder(Argon2Parameters.Argon2i) .WithVersion(version) .WithIterations(iterations) .WithMemoryPowOfTwo(memory) @@ -313,34 +310,34 @@ public void TestVectorsFromSpecs() { /* Version 0x13 (19) from RFC 9106 https://datatracker.ietf.org/doc/html/rfc9106#name-test-vectors */ SpecsTest( - Argon2Parameters.Argon2_Version13, - Argon2Parameters.Argon2_d, + Argon2Parameters.Version13, + Argon2Parameters.Argon2d, "512b391b6f1162975371d30919734294f868e3be3984f3c1a13a4db9fabe4acb"); SpecsTest( - Argon2Parameters.Argon2_Version13, - Argon2Parameters.Argon2_i, + Argon2Parameters.Version13, + Argon2Parameters.Argon2i, "c814d9d1dc7f37aa13f0d77f2494bda1c8de6b016dd388d29952a4c4672b6ce8"); SpecsTest( - Argon2Parameters.Argon2_Version13, - Argon2Parameters.Argon2_id, + Argon2Parameters.Version13, + Argon2Parameters.Argon2id, "0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659"); /* Version 0x10 (16) from reference C implementation https://github.com/P-H-C/phc-winner-argon2/tree/master/kats */ SpecsTest( - Argon2Parameters.Argon2_Version10, - Argon2Parameters.Argon2_d, + Argon2Parameters.Version10, + Argon2Parameters.Argon2d, "96a9d4e5a1734092c85e29f410a45914a5dd1f5cbf08b2670da68a0285abf32b"); SpecsTest( - Argon2Parameters.Argon2_Version10, - Argon2Parameters.Argon2_i, + Argon2Parameters.Version10, + Argon2Parameters.Argon2i, "87aeedd6517ab830cd9765cd8231abb2e647a5dee08f7c05e02fcb763335d0fd"); SpecsTest( - Argon2Parameters.Argon2_Version10, - Argon2Parameters.Argon2_id, + Argon2Parameters.Version10, + Argon2Parameters.Argon2id, "b64615f07789b66b645b67ee9ed3b377ae350b6bfcbb0fc95141ea8f322613c0"); } #endregion From 6940300ed5f6355578842b920ec134cc8cac6d0d Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Sat, 5 Oct 2024 19:10:17 +0200 Subject: [PATCH 34/37] Adapt PgpUtilities.DoMakeKeyFromPassPhrase to Argon2 Refactoring --- crypto/src/openpgp/PgpUtilities.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crypto/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index 559b70007..d1f5b7173 100644 --- a/crypto/src/openpgp/PgpUtilities.cs +++ b/crypto/src/openpgp/PgpUtilities.cs @@ -343,8 +343,8 @@ internal static KeyParameter DoMakeKeyFromPassPhrase(SymmetricKeyAlgorithmTag al if (s2k != null && s2k.Type == S2k.Argon2) { Argon2Parameters.Builder builder = - new Argon2Parameters.Builder(Argon2Parameters.Argon2_id) - .WithVersion(Argon2Parameters.Argon2_Version13) + new Argon2Parameters.Builder(Argon2Parameters.Argon2id) + .WithVersion(Argon2Parameters.Version13) .WithIterations(s2k.Passes) .WithMemoryPowOfTwo(s2k.MemorySizeExponent) .WithParallelism(s2k.Parallelism) From 1e57feb56f76b0e5f5afb167af9b26547c6ddf7a Mon Sep 17 00:00:00 2001 From: Peter Dettman Date: Wed, 10 Apr 2024 17:02:37 +0700 Subject: [PATCH 35/37] Add various fingerprint-related methods in OpenPgp --- crypto/src/openpgp/PgpPublicKey.cs | 6 +- crypto/src/openpgp/PgpPublicKeyRing.cs | 19 ++++-- crypto/src/openpgp/PgpPublicKeyRingBundle.cs | 72 ++++++++++++++------ crypto/src/openpgp/PgpSecretKeyRing.cs | 48 ++++++++++++- crypto/src/openpgp/PgpSecretKeyRingBundle.cs | 34 ++++----- crypto/test/src/openpgp/test/PGPRSATest.cs | 14 +++- crypto/test/src/openpgp/test/PgpEdDsaTest.cs | 6 +- 7 files changed, 151 insertions(+), 48 deletions(-) diff --git a/crypto/src/openpgp/PgpPublicKey.cs b/crypto/src/openpgp/PgpPublicKey.cs index 61be4b6a4..bd93264af 100644 --- a/crypto/src/openpgp/PgpPublicKey.cs +++ b/crypto/src/openpgp/PgpPublicKey.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.IO; using Org.BouncyCastle.Asn1.Cryptlib; @@ -572,6 +571,11 @@ public byte[] GetFingerprint() return Arrays.Clone(fingerprint); } + public bool HasFingerprint(byte[] fingerprint) + { + return Arrays.AreEqual(this.fingerprint, fingerprint); + } + /// /// Check if this key has an algorithm type that makes it suitable to use for encryption. /// diff --git a/crypto/src/openpgp/PgpPublicKeyRing.cs b/crypto/src/openpgp/PgpPublicKeyRing.cs index 46eecd726..f50dd915c 100644 --- a/crypto/src/openpgp/PgpPublicKeyRing.cs +++ b/crypto/src/openpgp/PgpPublicKeyRing.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; -using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.Collections; namespace Org.BouncyCastle.Bcpg.OpenPgp @@ -80,6 +79,18 @@ public virtual PgpPublicKey GetPublicKey(long keyId) return null; } + /// Return the public key with the passed in fingerprint if it is present. + public virtual PgpPublicKey GetPublicKey(byte[] fingerprint) + { + foreach (PgpPublicKey k in keys) + { + if (k.HasFingerprint(fingerprint)) + return k; + } + + return null; + } + /// Allows enumeration of all the public keys. /// An IEnumerable of PgpPublicKey objects. public virtual IEnumerable GetPublicKeys() @@ -239,17 +250,17 @@ public static PgpPublicKeyRing Join(PgpPublicKeyRing first, PgpPublicKeyRing sec public static PgpPublicKeyRing Join(PgpPublicKeyRing first, PgpPublicKeyRing second, bool joinTrustPackets, bool allowSubkeySigsOnNonSubkey) { - if (!Arrays.AreEqual(first.GetPublicKey().GetFingerprint(), second.GetPublicKey().GetFingerprint())) + if (!second.GetPublicKey().HasFingerprint(first.GetPublicKey().GetFingerprint())) throw new ArgumentException("Cannot merge certificates with differing primary keys."); var secondKeys = new HashSet(); - foreach (var key in second.GetPublicKeys()) + foreach (var key in second.keys) { secondKeys.Add(key.KeyId); } var merged = new List(); - foreach (var key in first.GetPublicKeys()) + foreach (var key in first.keys) { var copy = second.GetPublicKey(key.KeyId); if (copy != null) diff --git a/crypto/src/openpgp/PgpPublicKeyRingBundle.cs b/crypto/src/openpgp/PgpPublicKeyRingBundle.cs index 473d0ae5b..1940c979e 100644 --- a/crypto/src/openpgp/PgpPublicKeyRingBundle.cs +++ b/crypto/src/openpgp/PgpPublicKeyRingBundle.cs @@ -8,10 +8,10 @@ namespace Org.BouncyCastle.Bcpg.OpenPgp { - /// - /// Often a PGP key ring file is made up of a succession of master/sub-key key rings. - /// If you want to read an entire public key file in one hit this is the class for you. - /// + /// + /// Often a PGP key ring file is made up of a succession of master/sub-key key rings. + /// If you want to read an entire public key file in one hit this is the class for you. + /// public class PgpPublicKeyRingBundle { private readonly IDictionary m_pubRings; @@ -66,7 +66,7 @@ public int Count /// Allow enumeration of the public key rings making up this collection. public IEnumerable GetKeyRings() { - return CollectionUtilities.Proxy(m_pubRings.Values); + return CollectionUtilities.Proxy(KeyRings); } /// Allow enumeration of the key rings associated with the passed in userId. @@ -96,7 +96,7 @@ public IEnumerable GetKeyRings(string userID, bool matchPartia var compareInfo = CultureInfo.InvariantCulture.CompareInfo; var compareOptions = ignoreCase ? CompareOptions.OrdinalIgnoreCase : CompareOptions.Ordinal; - foreach (PgpPublicKeyRing pubRing in GetKeyRings()) + foreach (PgpPublicKeyRing pubRing in KeyRings) { foreach (string nextUserID in pubRing.GetPublicKey().GetUserIds()) { @@ -118,7 +118,7 @@ public IEnumerable GetKeyRings(string userID, bool matchPartia /// The ID of the public key to return. public PgpPublicKey GetPublicKey(long keyId) { - foreach (PgpPublicKeyRing pubRing in GetKeyRings()) + foreach (PgpPublicKeyRing pubRing in KeyRings) { PgpPublicKey pub = pubRing.GetPublicKey(keyId); if (pub != null) @@ -135,7 +135,7 @@ public PgpPublicKeyRing GetPublicKeyRing(long keyId) if (m_pubRings.TryGetValue(keyId, out var keyRing)) return keyRing; - foreach (PgpPublicKeyRing pubRing in GetKeyRings()) + foreach (PgpPublicKeyRing pubRing in KeyRings) { if (pubRing.GetPublicKey(keyId) != null) return pubRing; @@ -144,11 +144,39 @@ public PgpPublicKeyRing GetPublicKeyRing(long keyId) return null; } - /// - /// Return true if a key matching the passed in key ID is present, false otherwise. - /// - /// key ID to look for. - public bool Contains(long keyID) + /// Return the PGP public key associated with the given key fingerprint. + /// the public key fingerprint to match against. + public PgpPublicKey GetPublicKey(byte[] fingerprint) + { + foreach (PgpPublicKeyRing pubRing in KeyRings) + { + PgpPublicKey pub = pubRing.GetPublicKey(fingerprint); + if (pub != null) + return pub; + } + + return null; + } + + /// Return the public key ring which contains the key associated with the given key fingerprint. + /// + /// the public key fingerprint to match against. + public PgpPublicKeyRing GetPublicKeyRing(byte[] fingerprint) + { + foreach (PgpPublicKeyRing pubRing in KeyRings) + { + if (pubRing.GetPublicKey(fingerprint) != null) + return pubRing; + } + + return null; + } + + /// + /// Return true if a key matching the passed in key ID is present, false otherwise. + /// + /// key ID to look for. + public bool Contains(long keyID) { return GetPublicKey(keyID) != null; } @@ -170,14 +198,16 @@ public void Encode(Stream outStr) } } - /// - /// Return a new bundle containing the contents of the passed in bundle and - /// the passed in public key ring. - /// - /// The PgpPublicKeyRingBundle the key ring is to be added to. - /// The key ring to be added. - /// A new PgpPublicKeyRingBundle merging the current one with the passed in key ring. - /// If the keyId for the passed in key ring is already present. + private ICollection KeyRings => m_pubRings.Values; + + /// + /// Return a new bundle containing the contents of the passed in bundle and + /// the passed in public key ring. + /// + /// The PgpPublicKeyRingBundle the key ring is to be added to. + /// The key ring to be added. + /// A new PgpPublicKeyRingBundle merging the current one with the passed in key ring. + /// If the keyId for the passed in key ring is already present. public static PgpPublicKeyRingBundle AddPublicKeyRing(PgpPublicKeyRingBundle bundle, PgpPublicKeyRing publicKeyRing) { diff --git a/crypto/src/openpgp/PgpSecretKeyRing.cs b/crypto/src/openpgp/PgpSecretKeyRing.cs index a070aa132..ff644545f 100644 --- a/crypto/src/openpgp/PgpSecretKeyRing.cs +++ b/crypto/src/openpgp/PgpSecretKeyRing.cs @@ -3,7 +3,6 @@ using System.IO; using Org.BouncyCastle.Security; -using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.Collections; namespace Org.BouncyCastle.Bcpg.OpenPgp @@ -115,6 +114,38 @@ public PgpPublicKey GetPublicKey() return keys[0].PublicKey; } + /// Return the public key referred to by the passed in keyID if it is present. + public PgpPublicKey GetPublicKey(long keyID) + { + PgpSecretKey key = GetSecretKey(keyID); + if (key != null) + return key.PublicKey; + + foreach (PgpPublicKey k in extraPubKeys) + { + if (keyID == k.KeyId) + return k; + } + + return null; + } + + /// Return the public key with the passed in fingerprint if it is present. + public PgpPublicKey GetPublicKey(byte[] fingerprint) + { + PgpSecretKey key = GetSecretKey(fingerprint); + if (key != null) + return key.PublicKey; + + foreach (PgpPublicKey k in extraPubKeys) + { + if (k.HasFingerprint(fingerprint)) + return k; + } + + return null; + } + /** * Return any keys carrying a signature issued by the key represented by keyID. * @@ -165,6 +196,7 @@ public IEnumerable GetSecretKeys() return CollectionUtilities.Proxy(keys); } + /// Return the secret key referred to by the passed in keyID if it is present. public PgpSecretKey GetSecretKey(long keyId) { foreach (PgpSecretKey k in keys) @@ -176,6 +208,18 @@ public PgpSecretKey GetSecretKey(long keyId) return null; } + /// Return the secret key associated with the passed in fingerprint if it is present. + public PgpSecretKey GetSecretKey(byte[] fingerprint) + { + foreach (PgpSecretKey k in keys) + { + if (k.PublicKey.HasFingerprint(fingerprint)) + return k; + } + + return null; + } + /// /// Return an iterator of the public keys in the secret key ring that /// have no matching private key. At the moment only personal certificate data @@ -247,7 +291,7 @@ public static PgpSecretKeyRing CopyWithNewPassword( { var newKeys = new List(ring.keys.Count); - foreach (PgpSecretKey secretKey in ring.GetSecretKeys()) + foreach (PgpSecretKey secretKey in ring.keys) { if (secretKey.IsPrivateKeyEmpty) { diff --git a/crypto/src/openpgp/PgpSecretKeyRingBundle.cs b/crypto/src/openpgp/PgpSecretKeyRingBundle.cs index 695c882b7..fe9a2461a 100644 --- a/crypto/src/openpgp/PgpSecretKeyRingBundle.cs +++ b/crypto/src/openpgp/PgpSecretKeyRingBundle.cs @@ -8,10 +8,10 @@ namespace Org.BouncyCastle.Bcpg.OpenPgp { - /// - /// Often a PGP key ring file is made up of a succession of master/sub-key key rings. - /// If you want to read an entire secret key file in one hit this is the class for you. - /// + /// + /// Often a PGP key ring file is made up of a succession of master/sub-key key rings. + /// If you want to read an entire secret key file in one hit this is the class for you. + /// public class PgpSecretKeyRingBundle { private readonly IDictionary m_secretRings; @@ -66,7 +66,7 @@ public int Count /// Allow enumeration of the secret key rings making up this collection. public IEnumerable GetKeyRings() { - return CollectionUtilities.Proxy(m_secretRings.Values); + return CollectionUtilities.Proxy(KeyRings); } /// Allow enumeration of the key rings associated with the passed in userId. @@ -96,7 +96,7 @@ public IEnumerable GetKeyRings(string userID, bool matchPartia var compareInfo = CultureInfo.InvariantCulture.CompareInfo; var compareOptions = ignoreCase ? CompareOptions.OrdinalIgnoreCase : CompareOptions.Ordinal; - foreach (PgpSecretKeyRing secRing in GetKeyRings()) + foreach (PgpSecretKeyRing secRing in KeyRings) { foreach (string nextUserID in secRing.GetSecretKey().UserIds) { @@ -118,7 +118,7 @@ public IEnumerable GetKeyRings(string userID, bool matchPartia /// The ID of the secret key to return. public PgpSecretKey GetSecretKey(long keyId) { - foreach (PgpSecretKeyRing secRing in GetKeyRings()) + foreach (PgpSecretKeyRing secRing in KeyRings) { PgpSecretKey sec = secRing.GetSecretKey(keyId); if (sec != null) @@ -135,7 +135,7 @@ public PgpSecretKeyRing GetSecretKeyRing(long keyId) if (m_secretRings.TryGetValue(keyId, out var keyRing)) return keyRing; - foreach (PgpSecretKeyRing secretRing in GetKeyRings()) + foreach (PgpSecretKeyRing secretRing in KeyRings) { if (secretRing.GetSecretKey(keyId) != null) return secretRing; @@ -170,14 +170,16 @@ public void Encode(Stream outStr) } } - /// - /// Return a new bundle containing the contents of the passed in bundle and - /// the passed in secret key ring. - /// - /// The PgpSecretKeyRingBundle the key ring is to be added to. - /// The key ring to be added. - /// A new PgpSecretKeyRingBundle merging the current one with the passed in key ring. - /// If the keyId for the passed in key ring is already present. + private ICollection KeyRings => m_secretRings.Values; + + /// + /// Return a new bundle containing the contents of the passed in bundle and + /// the passed in secret key ring. + /// + /// The PgpSecretKeyRingBundle the key ring is to be added to. + /// The key ring to be added. + /// A new PgpSecretKeyRingBundle merging the current one with the passed in key ring. + /// If the keyId for the passed in key ring is already present. public static PgpSecretKeyRingBundle AddSecretKeyRing(PgpSecretKeyRingBundle bundle, PgpSecretKeyRing secretKeyRing) { diff --git a/crypto/test/src/openpgp/test/PGPRSATest.cs b/crypto/test/src/openpgp/test/PGPRSATest.cs index 6de95fbeb..56d761c16 100644 --- a/crypto/test/src/openpgp/test/PGPRSATest.cs +++ b/crypto/test/src/openpgp/test/PGPRSATest.cs @@ -325,7 +325,12 @@ private void FingerPrintTest() PgpPublicKey pubKey = pgpPub.GetPublicKey(); - if (!Arrays.AreEqual(pubKey.GetFingerprint(), Hex.Decode("4FFB9F0884266C715D1CEAC804A3BBFA"))) + byte[] expectedVersion3 = Hex.Decode("4FFB9F0884266C715D1CEAC804A3BBFA"); + if (!Arrays.AreEqual(pubKey.GetFingerprint(), expectedVersion3)) + { + Fail("version 3 fingerprint test failed"); + } + if (!pubKey.HasFingerprint(expectedVersion3)) { Fail("version 3 fingerprint test failed"); } @@ -337,10 +342,15 @@ private void FingerPrintTest() pubKey = pgpPub.GetPublicKey(); - if (!Arrays.AreEqual(pubKey.GetFingerprint(), Hex.Decode("3062363c1046a01a751946bb35586146fdf3f373"))) + byte[] expectedVersion4 = Hex.Decode("3062363c1046a01a751946bb35586146fdf3f373"); + if (!Arrays.AreEqual(pubKey.GetFingerprint(), expectedVersion4)) { Fail("version 4 fingerprint test failed"); } + if (!pubKey.HasFingerprint(expectedVersion4)) + { + Fail("version 4 fingerprint test failed"); + } } private void MixedTest( diff --git a/crypto/test/src/openpgp/test/PgpEdDsaTest.cs b/crypto/test/src/openpgp/test/PgpEdDsaTest.cs index f67d19a7f..c3cdd53f0 100644 --- a/crypto/test/src/openpgp/test/PgpEdDsaTest.cs +++ b/crypto/test/src/openpgp/test/PgpEdDsaTest.cs @@ -202,8 +202,10 @@ public override void PerformTest() PgpPublicKeyRing pubKeyRing = new PgpPublicKeyRing(aIn); - IsTrue(AreEqual(Hex.Decode("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"), - pubKeyRing.GetPublicKey().GetFingerprint())); + IsTrue(AreEqual(pubKeyRing.GetPublicKey().GetFingerprint(), + Hex.Decode("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"))); + IsTrue(pubKeyRing.GetPublicKey().HasFingerprint( + Hex.Decode("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"))); aIn = new ArmoredInputStream(new MemoryStream(Strings.ToByteArray(edDSASecretKey), false)); From ea6a5b32e615debcfa5ca81421dbffc7bdc03d46 Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 5 Dec 2024 18:00:53 +0100 Subject: [PATCH 36/37] followup changes --- crypto/src/bcpg/ArmoredOutputStream.cs | 6 +----- crypto/src/bcpg/PublicKeyEncSessionPacket.cs | 1 + crypto/src/crypto/generators/Argon2BytesGenerator.cs | 7 +------ crypto/src/openpgp/PgpPublicKey.cs | 5 ----- crypto/test/src/openpgp/test/PGPArmoredTest.cs | 4 ++-- 5 files changed, 5 insertions(+), 18 deletions(-) diff --git a/crypto/src/bcpg/ArmoredOutputStream.cs b/crypto/src/bcpg/ArmoredOutputStream.cs index 6c57181ba..a63200f1d 100644 --- a/crypto/src/bcpg/ArmoredOutputStream.cs +++ b/crypto/src/bcpg/ArmoredOutputStream.cs @@ -17,7 +17,6 @@ public class ArmoredOutputStream : BaseOutputStream { public static readonly string HeaderVersion = "Version"; - private readonly bool showVersion; private static readonly byte[] encodingTable = { @@ -368,10 +367,7 @@ public override void WriteByte(byte value) DoWrite(headerStart + type + headerTail + NewLine); - // https://www.rfc-editor.org/rfc/rfc9580#name-version-armor-header - // To minimize metadata, implementations SHOULD NOT emit this key and its corresponding value except - // for debugging purposes with explicit user consent. - if (showVersion && m_headers.TryGetValue(HeaderVersion, out var versionHeaders)) + if (m_headers.TryGetValue(HeaderVersion, out var versionHeaders)) { WriteHeaderEntry(HeaderVersion, versionHeaders[0]); } diff --git a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs index 9c35a9cd9..c3edf00bf 100644 --- a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs @@ -3,6 +3,7 @@ using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.IO; using Org.BouncyCastle.Crypto.Utilities; +using System; namespace Org.BouncyCastle.Bcpg { diff --git a/crypto/src/crypto/generators/Argon2BytesGenerator.cs b/crypto/src/crypto/generators/Argon2BytesGenerator.cs index 82d16a703..8e182f85e 100644 --- a/crypto/src/crypto/generators/Argon2BytesGenerator.cs +++ b/crypto/src/crypto/generators/Argon2BytesGenerator.cs @@ -150,12 +150,8 @@ private void Reset() private void FillMemoryBlocks() { - FillBlock filler = new FillBlock(); - Position position = new Position(); for (int pass = 0; pass < parameters.Iterations; ++pass) { - position.pass = pass; - for (int slice = 0; slice < Argon2SyncPoints; ++slice) { if (m_taskFactory == null || parameters.Parallelism <= 1) @@ -455,8 +451,6 @@ private static void Hash(ReadOnlySpan input, Span output) digest.BlockUpdate(outLenBytes, 0, outLenBytes.Length); digest.BlockUpdate(input, 0, input.Length); digest.DoFinal(outBuffer, 0); - - int halfLen = blake2bLength / 2, outPos = outOff; Array.Copy(outBuffer, 0, output, outPos, halfLen); #endif outPos += halfLen; @@ -776,3 +770,4 @@ internal Position(int pass, int slice, int lane) } } } + diff --git a/crypto/src/openpgp/PgpPublicKey.cs b/crypto/src/openpgp/PgpPublicKey.cs index f674403a7..bd93264af 100644 --- a/crypto/src/openpgp/PgpPublicKey.cs +++ b/crypto/src/openpgp/PgpPublicKey.cs @@ -576,11 +576,6 @@ public bool HasFingerprint(byte[] fingerprint) return Arrays.AreEqual(this.fingerprint, fingerprint); } - public bool HasFingerprint(byte[] fingerprint) - { - return Arrays.AreEqual(this.fingerprint, fingerprint); - } - /// /// Check if this key has an algorithm type that makes it suitable to use for encryption. /// diff --git a/crypto/test/src/openpgp/test/PGPArmoredTest.cs b/crypto/test/src/openpgp/test/PGPArmoredTest.cs index 195c1ca24..2cacf1366 100644 --- a/crypto/test/src/openpgp/test/PGPArmoredTest.cs +++ b/crypto/test/src/openpgp/test/PGPArmoredTest.cs @@ -128,7 +128,7 @@ private void VersionIsOptionalTest() { using (MemoryStream bOut = new MemoryStream()) { - using (ArmoredOutputStream aOut = new ArmoredOutputStream(bOut)) + using (ArmoredOutputStream aOut = new ArmoredOutputStream(bOut, addVersionHeader: false)) { aOut.Write(sample, 0, sample.Length); } @@ -146,7 +146,7 @@ private void VersionIsOptionalTest() using (MemoryStream bOut = new MemoryStream()) { - using (ArmoredOutputStream aOut = new ArmoredOutputStream(bOut, showVersion: true)) + using (ArmoredOutputStream aOut = new ArmoredOutputStream(bOut, addVersionHeader: true)) { aOut.Write(sample, 0, sample.Length); } From 8ad4e843536dc1ef84745ef290fd2cc52252b79c Mon Sep 17 00:00:00 2001 From: Fabrizio Tarizzo Date: Thu, 5 Dec 2024 18:34:36 +0100 Subject: [PATCH 37/37] BitStrength for RFC 9580 pubkey algorithms --- crypto/src/openpgp/PgpPublicKey.cs | 8 ++++++++ crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crypto/src/openpgp/PgpPublicKey.cs b/crypto/src/openpgp/PgpPublicKey.cs index bd93264af..4d6df9c25 100644 --- a/crypto/src/openpgp/PgpPublicKey.cs +++ b/crypto/src/openpgp/PgpPublicKey.cs @@ -217,6 +217,14 @@ private void Init() this.keyStrength = -1; // unknown } } + else if (key is Ed25519PublicBcpgKey || key is X25519PublicBcpgKey) + { + this.keyStrength = 256; + } + else if (key is Ed448PublicBcpgKey || key is X448PublicBcpgKey) + { + this.keyStrength = 448; + } } } diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs index 7677a2c27..27606cbe1 100644 --- a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -396,7 +396,7 @@ public void Version4Ed25519LegacyPubkeySampleTest() IsEquals(pubKey.Algorithm, PublicKeyAlgorithmTag.EdDsa_Legacy); IsEquals(pubKey.CreationTime.ToString("yyyyMMddHHmmss"), "20140819142827"); - + IsEquals(pubKey.BitStrength, 256); byte[] expectedFingerprint = Hex.Decode("C959BDBAFA32A2F89A153B678CFDE12197965A9A"); IsEquals((ulong)pubKey.KeyId, 0x8CFDE12197965A9A); IsTrue("wrong fingerprint", AreEqual(pubKey.GetFingerprint(), expectedFingerprint)); @@ -622,6 +622,7 @@ public void Version6Ed25519KeyPairCreationTest() IsEquals(keypair.PublicKey.Algorithm, PublicKeyAlgorithmTag.Ed25519); IsEquals(keypair.PublicKey.CreationTime.ToString("yyyyMMddHHmmss"), "20221130160803"); + IsEquals(keypair.PublicKey.BitStrength, 256); byte[] expectedFingerprint = Hex.Decode("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9"); IsEquals((ulong)keypair.KeyId, 0xCB186C4F0609A697); IsTrue("wrong master key fingerprint", AreEqual(keypair.PublicKey.GetFingerprint(), expectedFingerprint)); @@ -744,6 +745,7 @@ public void Version6Ed448KeyPairCreationTest() PgpKeyPair keypair = new PgpKeyPair(PublicKeyPacket.Version6, PublicKeyAlgorithmTag.Ed448, kp, now); IsEquals(keypair.PublicKey.Algorithm, PublicKeyAlgorithmTag.Ed448); IsEquals(keypair.PublicKey.CreationTime.ToString("yyyyMMddHHmmss"), now.ToString("yyyyMMddHHmmss")); + IsEquals(keypair.PublicKey.BitStrength, 448); long keyId = keypair.PublicKey.KeyId; byte[] fpr = keypair.PublicKey.GetFingerprint(); IsEquals(fpr.Length, 32);