diff --git a/crypto/src/bcpg/AeadAlgorithmTag.cs b/crypto/src/bcpg/AeadAlgorithmTag.cs index 632f888381..f02bc90aee 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/AeadEncDataPacket.cs b/crypto/src/bcpg/AeadEncDataPacket.cs index 78e54fcd08..13dd281643 100644 --- a/crypto/src/bcpg/AeadEncDataPacket.cs +++ b/crypto/src/bcpg/AeadEncDataPacket.cs @@ -19,11 +19,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) + : base(bcpgIn, PacketTag.ReservedAeadEncryptedData) { m_version = bcpgIn.RequireByte(); - if (m_version != 1) + if (m_version != Version1) throw new ArgumentException("wrong AEAD packet version: " + m_version); m_algorithm = (SymmetricKeyAlgorithmTag)bcpgIn.RequireByte(); @@ -36,9 +38,9 @@ public AeadEncDataPacket(BcpgInputStream bcpgIn) public AeadEncDataPacket(SymmetricKeyAlgorithmTag algorithm, AeadAlgorithmTag aeadAlgorithm, int chunkSize, byte[] iv) - : base(null) + : base(null, PacketTag.ReservedAeadEncryptedData) { - m_version = 1; + m_version = Version1; m_algorithm = algorithm; m_aeadAlgorithm = aeadAlgorithm; m_chunkSize = (byte)chunkSize; @@ -69,5 +71,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 new file mode 100644 index 0000000000..e88334ab2e --- /dev/null +++ b/crypto/src/bcpg/AeadInputStream.cs @@ -0,0 +1,191 @@ +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 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, + KeyParameter secretKey, + byte[] iv, + 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); + + 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 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, + AeadUtils.CreateNonce(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 = AeadUtils.CreateLastBlockAAData(isV5StyleAead, aaData, chunkIndex, totalBytes); + AeadParameters aeadParams = new AeadParameters( + secretKey, + 8 * tagLen, + AeadUtils.CreateNonce(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/AeadOutputStream.cs b/crypto/src/bcpg/AeadOutputStream.cs new file mode 100644 index 0000000000..8d49718661 --- /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 new file mode 100644 index 0000000000..0f2adce49f --- /dev/null +++ b/crypto/src/bcpg/AeadUtils.cs @@ -0,0 +1,167 @@ +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 +{ + public sealed class AeadUtils + { + /// + /// 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) + { + case AeadAlgorithmTag.Eax: + return 16; + case AeadAlgorithmTag.Ocb: + return 15; + case AeadAlgorithmTag.Gcm: + return 12; + default: + throw new ArgumentException($"Invalid AEAD algorithm tag: {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. + /// + /// AEAD algorithm identifier + /// length of the auth tag + /// Thrown when aeadAlgorithmTag is unknown/invalid + 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. + ///
+ /// m+n-8 bytes of concatenated message key and IV + /// symmetric cipher algorithm + /// AEAD algorithm + /// 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); + + messageKey = new byte[keyLen]; + iv = new byte[ivLen]; + + Array.Copy(messageKeyAndIv, messageKey, messageKey.Length); + 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) + { + 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/ArmoredOutputStream.cs b/crypto/src/bcpg/ArmoredOutputStream.cs index 0f4d410b3b..a63200f1dc 100644 --- a/crypto/src/bcpg/ArmoredOutputStream.cs +++ b/crypto/src/bcpg/ArmoredOutputStream.cs @@ -239,21 +239,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: diff --git a/crypto/src/bcpg/BcpgInputStream.cs b/crypto/src/bcpg/BcpgInputStream.cs index 623696ceb8..578723c516 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; @@ -119,9 +119,9 @@ public Packet ReadPacket() throw new IOException("invalid header encountered"); bool newPacket = (hdr & 0x40) != 0; - PacketTag tag = 0; + PacketTag tag = PacketTag.Reserved; // TODO[pgp] Is the length field supposed to support full uint range? - int bodyLen; + int bodyLen = 0; bool partial = false; if (newPacket) @@ -170,56 +170,65 @@ public Packet ReadPacket() switch (tag) { - case PacketTag.Reserved: - return new InputStreamPacket(objStream); - case PacketTag.PublicKeyEncryptedSession: - return new PublicKeyEncSessionPacket(objStream); - case PacketTag.Signature: - return new SignaturePacket(objStream); - case PacketTag.SymmetricKeyEncryptedSessionKey: - return new SymmetricKeyEncSessionPacket(objStream); - case PacketTag.OnePassSignature: - return new OnePassSignaturePacket(objStream); - case PacketTag.SecretKey: - return new SecretKeyPacket(objStream); - case PacketTag.PublicKey: - return new PublicKeyPacket(objStream); - case PacketTag.SecretSubkey: - return new SecretSubkeyPacket(objStream); - case PacketTag.CompressedData: - return new CompressedDataPacket(objStream); - case PacketTag.SymmetricKeyEncrypted: - return new SymmetricEncDataPacket(objStream); - case PacketTag.Marker: - return new MarkerPacket(objStream); - case PacketTag.LiteralData: - return new LiteralDataPacket(objStream); - case PacketTag.Trust: - return new TrustPacket(objStream); - case PacketTag.UserId: - return new UserIdPacket(objStream); - case PacketTag.UserAttribute: - return new UserAttributePacket(objStream); - case PacketTag.PublicSubkey: - return new PublicSubkeyPacket(objStream); - case PacketTag.SymmetricEncryptedIntegrityProtected: - return new SymmetricEncIntegrityPacket(objStream); - case PacketTag.ModificationDetectionCode: - return new ModDetectionCodePacket(objStream); - case PacketTag.Experimental1: - case PacketTag.Experimental2: - case PacketTag.Experimental3: - case PacketTag.Experimental4: - return new ExperimentalPacket(tag, objStream); - default: - throw new IOException("unknown packet type encountered: " + tag); + case PacketTag.Reserved: + return new InputStreamPacket(objStream, PacketTag.Reserved); + case PacketTag.PublicKeyEncryptedSession: + return new PublicKeyEncSessionPacket(objStream); + case PacketTag.Signature: + return new SignaturePacket(objStream); + case PacketTag.SymmetricKeyEncryptedSessionKey: + return new SymmetricKeyEncSessionPacket(objStream); + case PacketTag.OnePassSignature: + return new OnePassSignaturePacket(objStream); + case PacketTag.SecretKey: + return new SecretKeyPacket(objStream); + case PacketTag.PublicKey: + return new PublicKeyPacket(objStream); + case PacketTag.SecretSubkey: + return new SecretSubkeyPacket(objStream); + case PacketTag.CompressedData: + return new CompressedDataPacket(objStream); + case PacketTag.SymmetricKeyEncrypted: + return new SymmetricEncDataPacket(objStream); + case PacketTag.Marker: + return new MarkerPacket(objStream); + case PacketTag.LiteralData: + return new LiteralDataPacket(objStream); + case PacketTag.Trust: + return new TrustPacket(objStream); + case PacketTag.UserId: + return new UserIdPacket(objStream); + case PacketTag.UserAttribute: + return new UserAttributePacket(objStream); + case PacketTag.PublicSubkey: + return new PublicSubkeyPacket(objStream); + case PacketTag.SymmetricEncryptedIntegrityProtected: + 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: + case PacketTag.Experimental2: + case PacketTag.Experimental3: + case PacketTag.Experimental4: + return new ExperimentalPacket(tag, objStream); + default: + throw new IOException("unknown packet type encountered: " + tag); } } public PacketTag SkipMarkerPackets() + { + return SkipMarkerAndPaddingPackets(); + } + + public PacketTag SkipMarkerAndPaddingPackets() { PacketTag tag; - while ((tag = NextPacketTag()) == PacketTag.Marker) + while ((tag = NextPacketTag()) == PacketTag.Marker || tag == PacketTag.Padding) { ReadPacket(); } @@ -243,7 +252,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 b066621d83..7f72066a1e 100644 --- a/crypto/src/bcpg/CompressedDataPacket.cs +++ b/crypto/src/bcpg/CompressedDataPacket.cs @@ -6,8 +6,9 @@ public class CompressedDataPacket { private readonly CompressionAlgorithmTag m_algorithm; - internal CompressedDataPacket(BcpgInputStream bcpgIn) - : base(bcpgIn) + internal CompressedDataPacket( + BcpgInputStream bcpgIn) + : base(bcpgIn, PacketTag.CompressedData) { m_algorithm = (CompressionAlgorithmTag)bcpgIn.RequireByte(); } diff --git a/crypto/src/bcpg/ContainedPacket.cs b/crypto/src/bcpg/ContainedPacket.cs index ca39fd985c..812e36aa66 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/Ed25519PublicBCPGKey.cs b/crypto/src/bcpg/Ed25519PublicBCPGKey.cs new file mode 100644 index 0000000000..8c23d82180 --- /dev/null +++ b/crypto/src/bcpg/Ed25519PublicBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class Ed25519PublicBcpgKey + : OctetArrayBcpgKey + { + // https://www.rfc-editor.org/rfc/rfc9580#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 0000000000..67e5497387 --- /dev/null +++ b/crypto/src/bcpg/Ed25519SecretBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class Ed25519SecretBcpgKey + : OctetArrayBcpgKey + { + // https://www.rfc-editor.org/rfc/rfc9580#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 0000000000..102fbf7a5f --- /dev/null +++ b/crypto/src/bcpg/Ed448PublicBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class Ed448PublicBcpgKey + : OctetArrayBcpgKey + { + // https://www.rfc-editor.org/rfc/rfc9580#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 0000000000..489692cf3f --- /dev/null +++ b/crypto/src/bcpg/Ed448SecretBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class Ed448SecretBcpgKey + : OctetArrayBcpgKey + { + // https://www.rfc-editor.org/rfc/rfc9580#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/ExperimentalPacket.cs b/crypto/src/bcpg/ExperimentalPacket.cs index 3d413db099..dea370b599 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 d4946ebf26..97d61e5282 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 0b0aab6716..7e44420468 100644 --- a/crypto/src/bcpg/LiteralDataPacket.cs +++ b/crypto/src/bcpg/LiteralDataPacket.cs @@ -10,8 +10,9 @@ public class LiteralDataPacket private readonly byte[] m_fileName; private readonly long m_modDate; - internal LiteralDataPacket(BcpgInputStream bcpgIn) - : base(bcpgIn) + internal LiteralDataPacket( + BcpgInputStream bcpgIn) + : base(bcpgIn, PacketTag.LiteralData) { m_format = bcpgIn.RequireByte(); diff --git a/crypto/src/bcpg/MarkerPacket.cs b/crypto/src/bcpg/MarkerPacket.cs index f2c4507935..b5a8e9639f 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 ae8283aef5..56cd8d7cad 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/OctetArrayBCPGKey.cs b/crypto/src/bcpg/OctetArrayBCPGKey.cs new file mode 100644 index 0000000000..73d0d34a4f --- /dev/null +++ b/crypto/src/bcpg/OctetArrayBCPGKey.cs @@ -0,0 +1,47 @@ +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/OnePassSignaturePacket.cs b/crypto/src/bcpg/OnePassSignaturePacket.cs index 37d0b45864..3898117e3a 100644 --- a/crypto/src/bcpg/OnePassSignaturePacket.cs +++ b/crypto/src/bcpg/OnePassSignaturePacket.cs @@ -1,3 +1,7 @@ +using Org.BouncyCastle.Bcpg.OpenPgp; +using Org.BouncyCastle.Crypto.Utilities; +using Org.BouncyCastle.Utilities; +using System; using System.IO; namespace Org.BouncyCastle.Bcpg @@ -6,40 +10,129 @@ 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; - - internal OnePassSignaturePacket( + 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 int nested; + + // 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.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.rfc-editor.org/rfc/rfc9580#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.RequireByte(); + if (version != Version3 && version != Version6) + { + throw new UnsupportedPacketVersionException($"unsupported OpenPGP One Pass Signature packet version: {version}"); + } + sigType = bcpgIn.RequireByte(); - hashAlgorithm = (HashAlgorithmTag)bcpgIn.RequireByte(); - keyAlgorithm = (PublicKeyAlgorithmTag)bcpgIn.RequireByte(); - keyId = (long)StreamUtilities.RequireUInt64BE(bcpgIn); + hashAlgorithm = (HashAlgorithmTag) bcpgIn.RequireByte(); + keyAlgorithm = (PublicKeyAlgorithmTag) bcpgIn.RequireByte(); + + if (version == Version3) + { + keyId = (long)StreamUtilities.RequireUInt64BE(bcpgIn); + } + 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.RequireByte(); - } + EnforceConstraints(); + } + + /// + /// Create a Version 3 OPS Packet + /// + /// + /// + /// + /// + /// public OnePassSignaturePacket( int sigType, HashAlgorithmTag hashAlgorithm, PublicKeyAlgorithmTag keyAlgorithm, long keyId, bool isNested) - { - this.version = 3; + : base(PacketTag.OnePassSignature) + { + 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; } } @@ -61,17 +154,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/bcpg/Packet.cs b/crypto/src/bcpg/Packet.cs index 964102a715..ffca0bf939 100644 --- a/crypto/src/bcpg/Packet.cs +++ b/crypto/src/bcpg/Packet.cs @@ -1,8 +1,36 @@ +using System; + namespace Org.BouncyCastle.Bcpg { // TODO Add packet tag at this level (see bc-java), and IsCritical property 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/PacketTags.cs b/crypto/src/bcpg/PacketTags.cs index 5a53d4e957..43590e5f9c 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.rfc-editor.org/rfc/rfc9580#padding-packet) Experimental1 = 60, // Private or Experimental Values Experimental2 = 61, diff --git a/crypto/src/bcpg/PaddingPacket.cs b/crypto/src/bcpg/PaddingPacket.cs new file mode 100644 index 0000000000..d91ed9b287 --- /dev/null +++ b/crypto/src/bcpg/PaddingPacket.cs @@ -0,0 +1,52 @@ +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; + +namespace Org.BouncyCastle.Bcpg +{ + public class PaddingPacket + : ContainedPacket + { + 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); + } + + 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/bcpg/PublicKeyAlgorithmTags.cs b/crypto/src/bcpg/PublicKeyAlgorithmTags.cs index a309b65ae4..07ac3c2602 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 Reserved by RFC 9580 + AEDH = 23, + AEDSA = 24, + + // https://www.rfc-editor.org/rfc/rfc9580 + X25519 = 25, + X448 = 26, + Ed25519 = 27, + Ed448 = 28, + Experimental_1 = 100, Experimental_2 = 101, Experimental_3 = 102, diff --git a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs index c56b2bd70e..c3edf00bfe 100644 --- a/crypto/src/bcpg/PublicKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/PublicKeyEncSessionPacket.cs @@ -2,6 +2,8 @@ using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.IO; +using Org.BouncyCastle.Crypto.Utilities; +using System; namespace Org.BouncyCastle.Bcpg { @@ -9,47 +11,131 @@ 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; // 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.RequireByte(); - keyId = (long)StreamUtilities.RequireUInt64BE(bcpgIn); - algorithm = (PublicKeyAlgorithmTag)bcpgIn.RequireByte(); + switch (version) + { + case Version3: + keyId = (long)StreamUtilities.RequireUInt64BE(bcpgIn); + break; + case Version6: + int keyInfoLength = bcpgIn.RequireByte(); + if (keyInfoLength == 0) + { + // anonymous recipient + keyVersion = 0; + keyFingerprint = Array.Empty(); + keyId = 0; + } + else + { + keyVersion = bcpgIn.RequireByte(); + keyFingerprint = new byte[keyInfoLength - 1]; + bcpgIn.ReadFully(keyFingerprint); + + 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.RequireByte(); - switch ((PublicKeyAlgorithmTag) algorithm) + switch (algorithm) { - case PublicKeyAlgorithmTag.RsaEncrypt: - case PublicKeyAlgorithmTag.RsaGeneral: - data = new byte[][]{ new MPInteger(bcpgIn).GetEncoded() }; - break; - case PublicKeyAlgorithmTag.ElGamalEncrypt: - case PublicKeyAlgorithmTag.ElGamalGeneral: - MPInteger p = new MPInteger(bcpgIn); - MPInteger g = new MPInteger(bcpgIn); - data = new byte[][]{ - p.GetEncoded(), - g.GetEncoded(), - }; - break; - case PublicKeyAlgorithmTag.ECDH: - data = new byte[][]{ Streams.ReadAll(bcpgIn) }; - break; - default: - throw new IOException("unknown PGP public key algorithm encountered"); + case PublicKeyAlgorithmTag.RsaEncrypt: + case PublicKeyAlgorithmTag.RsaGeneral: + data = new byte[][]{ new MPInteger(bcpgIn).GetEncoded() }; + break; + case PublicKeyAlgorithmTag.ElGamalEncrypt: + case PublicKeyAlgorithmTag.ElGamalGeneral: + MPInteger p = new MPInteger(bcpgIn); + MPInteger g = new MPInteger(bcpgIn); + data = new byte[][]{ + p.GetEncoded(), + g.GetEncoded(), + }; + break; + case PublicKeyAlgorithmTag.ECDH: + data = new byte[][]{ Streams.ReadAll(bcpgIn) }; + break; + case PublicKeyAlgorithmTag.X25519: + case PublicKeyAlgorithmTag.X448: + // 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; + 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) - { - this.version = 3; + : base(PacketTag.PublicKeyEncryptedSession) + { + this.version = Version3; this.keyId = keyId; this.algorithm = algorithm; this.data = new byte[data.Length][]; @@ -59,6 +145,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; } @@ -69,7 +181,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; } } @@ -81,20 +208,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/bcpg/PublicKeyPacket.cs b/crypto/src/bcpg/PublicKeyPacket.cs index a0f1baee5b..6c6add125b 100644 --- a/crypto/src/bcpg/PublicKeyPacket.cs +++ b/crypto/src/bcpg/PublicKeyPacket.cs @@ -9,26 +9,49 @@ namespace Org.BouncyCastle.Bcpg public class PublicKeyPacket : ContainedPacket //, PublicKeyAlgorithmTag { - private int version; - private long time; - private int validDays; - private PublicKeyAlgorithmTag algorithm; - private IBcpgKey key; - - internal PublicKeyPacket( - 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; + + public const int DefaultVersion = Version4; + + 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) + : this(bcpgIn, PacketTag.PublicKey) + { + } + + protected PublicKeyPacket( + BcpgInputStream bcpgIn, + PacketTag tag) + : base(tag) { version = bcpgIn.RequireByte(); time = StreamUtilities.RequireUInt32BE(bcpgIn); - if (version <= 3) + if (version <= Version3) { validDays = StreamUtilities.RequireUInt16BE(bcpgIn); } algorithm = (PublicKeyAlgorithmTag)bcpgIn.RequireByte(); + 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: @@ -52,8 +75,49 @@ 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"); + } + } + + + /// Construct a public key packet. + public PublicKeyPacket( + int version, + 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; + this.algorithm = algorithm; + this.key = key; + + if (version == Version5 || version == Version6) + { + v6KeyLen = ((BcpgObject)key).GetEncoded().Length; } } @@ -62,11 +126,8 @@ public PublicKeyPacket( PublicKeyAlgorithmTag algorithm, DateTime time, IBcpgKey key) + :this(DefaultVersion, algorithm, time, key) { - this.version = 4; - this.time = DateTimeUtilities.DateTimeToUnixMs(time) / 1000L; - this.algorithm = algorithm; - this.key = key; } public virtual int Version @@ -102,12 +163,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/PublicSubkeyPacket.cs b/crypto/src/bcpg/PublicSubkeyPacket.cs index 0e1065b720..0e70790a17 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, PacketTag.PublicSubkey) + { + } + + /// Construct a version 4 public subkey packet. public PublicSubkeyPacket( PublicKeyAlgorithmTag algorithm, DateTime time, IBcpgKey key) - : base(algorithm, time, key) + : base(DefaultVersion, algorithm, time, key, PacketTag.PublicSubkey) { } diff --git a/crypto/src/bcpg/S2k.cs b/crypto/src/bcpg/S2k.cs index d68dc38282..28cb12c8b5 100644 --- a/crypto/src/bcpg/S2k.cs +++ b/crypto/src/bcpg/S2k.cs @@ -1,6 +1,7 @@ using System; using System.IO; - +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.IO; @@ -15,75 +16,123 @@ 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; - 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) + internal S2k( + Stream inStr) { type = StreamUtilities.RequireByte(inStr); - algorithm = (HashAlgorithmTag)StreamUtilities.RequireByte(inStr); - // - // if this happens we have a dummy-S2k packet. - // - if (type != GnuDummyS2K) + switch (type) { - if (type != 0) - { - iv = new byte[8]; + case Simple: + algorithm = (HashAlgorithmTag)StreamUtilities.RequireByte(inStr); + break; + + case Salted: + algorithm = (HashAlgorithmTag)StreamUtilities.RequireByte(inStr); + iv = new byte[8]; StreamUtilities.RequireBytes(inStr, iv); + break; - if (type == 3) - { - itCount = inStr.ReadByte(); - } - } - } - else - { - //inStr.ReadByte(); // G - //inStr.ReadByte(); // N - //inStr.ReadByte(); // U - //protectionMode = inStr.ReadByte(); // protection mode - uint GNU_ = StreamUtilities.RequireUInt32BE(inStr); - protectionMode = (byte)GNU_; + case SaltedAndIterated: + algorithm = (HashAlgorithmTag)StreamUtilities.RequireByte(inStr); + iv = new byte[8]; + StreamUtilities.RequireBytes(inStr, iv); + itCount = StreamUtilities.RequireByte(inStr); + break; + + case Argon2: + iv = new byte[16]; + StreamUtilities.RequireBytes(inStr, iv); + passes = StreamUtilities.RequireByte(inStr); + parallelism = StreamUtilities.RequireByte(inStr); + memorySizeExponent = StreamUtilities.RequireByte(inStr); + break; + + case GnuDummyS2K: + algorithm = (HashAlgorithmTag)StreamUtilities.RequireByte(inStr); + //inStr.ReadByte(); // G + //inStr.ReadByte(); // N + //inStr.ReadByte(); // U + //protectionMode = inStr.ReadByte(); // protection mode + uint GNU_ = StreamUtilities.RequireUInt32BE(inStr); + protectionMode = (byte)GNU_; + 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; + } + + /// 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; } @@ -113,30 +162,183 @@ 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); + switch (type) + { + 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; + + case SaltedAndIterated: + bcpgOut.WriteByte((byte)type); + bcpgOut.WriteByte((byte)algorithm); + bcpgOut.Write(iv); + bcpgOut.WriteByte((byte)itCount); + break; - if (type != GnuDummyS2K) + 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}"); + } + } + + /// + /// Parameters for Argon2 S2K + /// Sect. 3.7.1.4 of RFC 9580 + /// + 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()) { - if (type != 0) + } + + + /// + /// 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) { - bcpgOut.Write(iv); + 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 (type == 3) + if (parallelism < 1) { - bcpgOut.WriteByte((byte) itCount); + 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.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"); + } + 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()); } - else + + /// + /// 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) { - bcpgOut.WriteByte((byte) 'G'); - bcpgOut.WriteByte((byte) 'N'); - bcpgOut.WriteByte((byte) 'U'); - bcpgOut.WriteByte((byte) protectionMode); + byte[] salt = new byte[16]; + secureRandom.NextBytes(salt); + return salt; } } } diff --git a/crypto/src/bcpg/SecretKeyPacket.cs b/crypto/src/bcpg/SecretKeyPacket.cs index e048a1195a..235604b23a 100644 --- a/crypto/src/bcpg/SecretKeyPacket.cs +++ b/crypto/src/bcpg/SecretKeyPacket.cs @@ -1,6 +1,6 @@ -using System.IO; - using Org.BouncyCastle.Utilities; +using System; +using System.IO; namespace Org.BouncyCastle.Bcpg { @@ -8,19 +8,74 @@ namespace Org.BouncyCastle.Bcpg public class SecretKeyPacket : ContainedPacket //, PublicKeyAlgorithmTag { - public const int UsageNone = 0x00; - public const int UsageChecksum = 0xff; - public const int UsageSha1 = 0xfe; - private PublicKeyPacket pubKeyPacket; + /// + /// 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. + /// + /// + /// 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. + /// + /// + /// 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 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; + + + 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 aeadAlgorithm; + + 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) { @@ -31,23 +86,53 @@ internal SecretKeyPacket( pubKeyPacket = new PublicKeyPacket(bcpgIn); } - s2kUsage = bcpgIn.RequireByte(); + int version = pubKeyPacket.Version; + s2kUsage = bcpgIn.RequireByte(); + + if (version == PublicKeyPacket.Version6 && s2kUsage != UsageNone) + { + // TODO: Use length to parse unknown parameters + int conditionalParameterLength = bcpgIn.RequireByte(); + } - if (s2kUsage == UsageChecksum || s2kUsage == UsageSha1) + if (HasS2KSpecifier) { encAlgorithm = (SymmetricKeyAlgorithmTag)bcpgIn.RequireByte(); - s2k = new S2k(bcpgIn); } else { encAlgorithm = (SymmetricKeyAlgorithmTag)s2kUsage; - } + } + + if (s2kUsage == UsageAead) + { + aeadAlgorithm = (AeadAlgorithmTag)bcpgIn.RequireByte(); + } + + if (HasS2KSpecifier) + { + if (version == PublicKeyPacket.Version6) + { + // TODO: Use length to parse unknown S2Ks + int s2kLen = bcpgIn.RequireByte(); + } + s2k = new S2k(bcpgIn); + } + if (s2kUsage == UsageAead) + { + 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]; } @@ -59,15 +144,28 @@ internal SecretKeyPacket( } } - secKeyData = bcpgIn.ReadAll(); + secKeyData = bcpgIn.ReadAll(); + } + + + public SecretKeyPacket( + PublicKeyPacket pubKeyPacket, + SymmetricKeyAlgorithmTag encAlgorithm, + S2k s2k, + byte[] iv, + byte[] secKeyData) + : this(pubKeyPacket, encAlgorithm, s2k, iv, secKeyData, PacketTag.SecretKey) + { } - public SecretKeyPacket( + protected SecretKeyPacket( PublicKeyPacket pubKeyPacket, SymmetricKeyAlgorithmTag encAlgorithm, S2k s2k, byte[] iv, - byte[] secKeyData) + byte[] secKeyData, + PacketTag tag) + :base(tag) { this.pubKeyPacket = pubKeyPacket; this.encAlgorithm = encAlgorithm; @@ -86,13 +184,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; @@ -102,12 +213,62 @@ 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, 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; + 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 AeadAlgorithm + { + get { return aeadAlgorithm; } + } + + public int S2kUsage { get { return s2kUsage; } } @@ -132,31 +293,73 @@ public byte[] GetSecretKeyData() return secKeyData; } - public byte[] GetEncodedContents() + internal byte[] GetAAData() { - MemoryStream bOut = new MemoryStream(); - using (var pOut = new BcpgOutputStream(bOut)) + // HKDF Info used for key derivation in UsageAead + return new byte[] { - pOut.Write(pubKeyPacket.GetEncodedContents()); - pOut.WriteByte((byte)s2kUsage); + (byte)(0xC0 | (byte)Tag), + (byte)pubKeyPacket.Version, + (byte)encAlgorithm, + (byte)aeadAlgorithm + }; + } - if (s2kUsage == UsageChecksum || s2kUsage == UsageSha1) + private byte[] EncodeConditionalParameters() + { + using (MemoryStream conditionalParameters = new MemoryStream()) + { + 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 2d405aec27..5fb249a254 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,11 +30,23 @@ public SecretSubkeyPacket( S2k s2k, byte[] iv, byte[] secKeyData) - : base(pubKeyPacket, encAlgorithm, s2kUsage, s2k, iv, secKeyData) + : base(pubKeyPacket, encAlgorithm, s2kUsage, s2k, iv, secKeyData, PacketTag.SecretSubkey) { } - 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, PacketTag.SecretSubkey) + { + } + + public override void Encode(BcpgOutputStream bcpgOut) { bcpgOut.WritePacket(PacketTag.SecretSubkey, GetEncodedContents()); } diff --git a/crypto/src/bcpg/SignaturePacket.cs b/crypto/src/bcpg/SignaturePacket.cs index a9e805f2d0..8149c11758 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; @@ -14,23 +14,69 @@ 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; + + public const int DefaultVersion = Version4; + + private readonly int version; + private readonly int signatureType; + private long creationTime; + private long keyId; + private bool keyIdAlreadySet = false; + 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; + 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.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) + { + throw new IOException("V6 signatures MUST NOT include an IssuerKeyId subpacket"); + } + keyId = issuerKeyId.KeyId; + keyIdAlreadySet = true; + } + } + + internal SignaturePacket(BcpgInputStream bcpgIn) + :base(PacketTag.Signature) { version = bcpgIn.RequireByte(); - if (version == 3 || version == 2) + if (version == Version2 || version == Version3) { // int l = bcpgIn.RequireByte(); @@ -41,13 +87,22 @@ internal SignaturePacket(BcpgInputStream bcpgIn) keyAlgorithm = (PublicKeyAlgorithmTag)bcpgIn.RequireByte(); hashAlgorithm = (HashAlgorithmTag)bcpgIn.RequireByte(); } - else if (version == 4) + else if (version >= Version4 && version <= Version6) { signatureType = bcpgIn.RequireByte(); keyAlgorithm = (PublicKeyAlgorithmTag)bcpgIn.RequireByte(); hashAlgorithm = (HashAlgorithmTag)bcpgIn.RequireByte(); - int hashedLength = StreamUtilities.RequireUInt16BE(bcpgIn); + int hashedLength; + + if (version == Version6) + { + hashedLength = (int)StreamUtilities.RequireUInt32BE(bcpgIn); + } + else + { + hashedLength = StreamUtilities.RequireUInt16BE(bcpgIn); + } byte[] hashed = new byte[hashedLength]; bcpgIn.ReadFully(hashed); @@ -69,17 +124,25 @@ 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()); } } - int unhashedLength = StreamUtilities.RequireUInt16BE(bcpgIn); + int unhashedLength; + + if (version == Version6) + { + unhashedLength = (int)StreamUtilities.RequireUInt32BE(bcpgIn); + } + else + { + unhashedLength = StreamUtilities.RequireUInt16BE(bcpgIn); + } + byte[] unhashed = new byte[unhashedLength]; bcpgIn.ReadFully(unhashed); @@ -96,10 +159,7 @@ internal SignaturePacket(BcpgInputStream bcpgIn) foreach (var p in unhashedData) { - if (p is IssuerKeyId issuerKeyId) - { - keyId = issuerKeyId.KeyId; - } + CheckIssuerSubpacket(p); } } else @@ -112,7 +172,24 @@ internal SignaturePacket(BcpgInputStream bcpgIn) fingerprint = new byte[2]; bcpgIn.ReadFully(fingerprint); - switch (keyAlgorithm) + if (version == Version6) + { + int saltSize = bcpgIn.ReadByte(); + + if (saltSize != PgpUtilities.GetSaltSize(hashAlgorithm)) + { + // 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.rfc-editor.org/rfc/rfc9580#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); + } + + switch (keyAlgorithm) { case PublicKeyAlgorithmTag.RsaGeneral: case PublicKeyAlgorithmTag.RsaSign: @@ -132,6 +209,20 @@ internal SignaturePacket(BcpgInputStream bcpgIn) MPInteger ecS = new MPInteger(bcpgIn); signature = new MPInteger[2]{ ecR, ecS }; break; + + case PublicKeyAlgorithmTag.Ed25519: + // 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.rfc-editor.org/rfc/rfc9580#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); @@ -142,7 +233,7 @@ internal SignaturePacket(BcpgInputStream bcpgIn) } } - /** + /** * Generate a version 4 signature packet. * * @param signatureType @@ -162,7 +253,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, null, null, signature) { } @@ -184,7 +275,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; } @@ -199,6 +290,56 @@ 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; this.signatureType = signatureType; @@ -207,16 +348,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; @@ -226,6 +368,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 @@ -235,15 +382,21 @@ 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() { - if (version == 3) + return GetSignatureTrailer(Array.Empty()); + } + + public byte[] GetSignatureTrailer(byte[] additionalMetadata) + { + if (version == Version3) { long time = creationTime / 1000L; @@ -253,40 +406,93 @@ public byte[] GetSignatureTrailer() return trailer; } - MemoryStream sOut = new MemoryStream(); + 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((byte)Version); - sOut.WriteByte((byte)SignatureType); - sOut.WriteByte((byte)KeyAlgorithm); - sOut.WriteByte((byte)HashAlgorithm); + SignatureSubpacket[] hashed = GetHashedSubPackets(); + for (int i = 0; i != hashed.Length; i++) + { + hashed[i].Encode(sOut); + } - // Mark position an reserve two bytes for length - long lengthPosition = sOut.Position; - sOut.WriteByte(0x00); - sOut.WriteByte(0x00); + ushort dataLength = Convert.ToUInt16(sOut.Position - lengthPosition - 2); + if (version == Version6) + { + dataLength -= 2; + } - SignatureSubpacket[] hashed = GetHashedSubPackets(); - for (int i = 0; i != hashed.Length; i++) - { - hashed[i].Encode(sOut); - } + uint hDataLength = Convert.ToUInt32(sOut.Position); - ushort dataLength = Convert.ToUInt16(sOut.Position - lengthPosition - 2); - uint hDataLength = Convert.ToUInt32(sOut.Position); + // 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. - 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 )); + 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); + } + } - // Reset position and fill in length - sOut.Position = lengthPosition; - sOut.WriteByte((byte)(dataLength >> 8)); - sOut.WriteByte((byte)(dataLength )); + sOut.WriteByte((byte)Version); + sOut.WriteByte(0xff); - return sOut.ToArray(); + 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 )); + + // 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; @@ -299,6 +505,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. @@ -306,7 +517,7 @@ public byte[] GetSignatureTrailer() public byte[] GetSignatureBytes() { if (signatureEncoding != null) - return (byte[])signatureEncoding.Clone(); + return Arrays.Clone(signatureEncoding); MemoryStream bOut = new MemoryStream(); @@ -342,7 +553,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); @@ -350,11 +561,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 { @@ -363,6 +574,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); @@ -377,10 +594,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/SignatureSubpacketTags.cs b/crypto/src/bcpg/SignatureSubpacketTags.cs index 0574c274b7..082d668393 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/SymmetricEncDataPacket.cs b/crypto/src/bcpg/SymmetricEncDataPacket.cs index 17ee55bb73..013a7fd39e 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 fe09ba7d95..bbac8c1ebd 100644 --- a/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs +++ b/crypto/src/bcpg/SymmetricEncIntegrityPacket.cs @@ -1,14 +1,87 @@ +using Org.BouncyCastle.Utilities; +using System; +using System.IO; + namespace Org.BouncyCastle.Bcpg { public class SymmetricEncIntegrityPacket : InputStreamPacket { - internal readonly int m_version; + /// + /// Version 3 SEIPD packet. + /// + /// + public const int Version1 = 1; + /// + /// Version 2 SEIPD packet. + /// + /// + public const int Version2 = 2; + + private readonly int m_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) + internal SymmetricEncIntegrityPacket( + BcpgInputStream bcpgIn) + : base(bcpgIn, PacketTag.SymmetricEncryptedIntegrityProtected) { m_version = bcpgIn.RequireByte(); + if (m_version == Version2) + { + cipherAlgorithm = (SymmetricKeyAlgorithmTag)bcpgIn.RequireByte(); + aeadAlgorithm = (AeadAlgorithmTag)bcpgIn.RequireByte(); + chunkSize = bcpgIn.RequireByte(); + + salt = new byte[32]; + if (bcpgIn.Read(salt, 0, 32) != salt.Length) + { + throw new IOException("Premature end of stream."); + } + } + } + + public int Version + { + get { return m_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(Version, cipherAlgorithm, aeadAlgorithm, chunkSize); + } + + internal static byte[] CreateAAData(int version, SymmetricKeyAlgorithmTag cipherAlgorithm, AeadAlgorithmTag aeadAlgorithm, int chunkSize) + { + return new byte[]{ + 0xC0 | (byte)PacketTag.SymmetricEncryptedIntegrityProtected, + (byte)version, + (byte)cipherAlgorithm, + (byte)aeadAlgorithm, + (byte)chunkSize + }; } } } diff --git a/crypto/src/bcpg/SymmetricKeyAlgorithmTags.cs b/crypto/src/bcpg/SymmetricKeyAlgorithmTags.cs index e05a486163..abd00fa218 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/bcpg/SymmetricKeyEncSessionPacket.cs b/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs index 569deec265..0a4a96a6c8 100644 --- a/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs +++ b/crypto/src/bcpg/SymmetricKeyEncSessionPacket.cs @@ -1,3 +1,5 @@ +using Org.BouncyCastle.Utilities; +using System; using System.IO; namespace Org.BouncyCastle.Bcpg @@ -8,32 +10,123 @@ namespace Org.BouncyCastle.Bcpg public class SymmetricKeyEncSessionPacket : ContainedPacket { - private int version; - private SymmetricKeyAlgorithmTag encAlgorithm; - private S2k s2k; + /// + /// 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; - public SymmetricKeyEncSessionPacket(BcpgInputStream bcpgIn) + private readonly byte[] s2kBytes; + private readonly AeadAlgorithmTag aeadAlgorithm; + private readonly byte[] iv; + + public SymmetricKeyEncSessionPacket( + BcpgInputStream bcpgIn) + :base(PacketTag.SymmetricKeyEncryptedSessionKey) { version = bcpgIn.RequireByte(); - encAlgorithm = (SymmetricKeyAlgorithmTag)bcpgIn.RequireByte(); - s2k = new S2k(bcpgIn); + switch (version) + { + case Version4: + encAlgorithm = (SymmetricKeyAlgorithmTag)bcpgIn.RequireByte(); + s2k = new S2k(bcpgIn); + secKeyData = bcpgIn.ReadAll(); + break; + + case Version5: + case Version6: + // 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.RequireByte(); + aeadAlgorithm = (AeadAlgorithmTag)bcpgIn.RequireByte(); + + 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; } + + /// + /// 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 */ @@ -42,6 +135,11 @@ public SymmetricKeyAlgorithmTag EncAlgorithm get { return encAlgorithm; } } + public AeadAlgorithmTag AeadAlgorithm + { + get { return aeadAlgorithm; } + } + /** * @return S2k */ @@ -66,21 +164,69 @@ public int Version get { return version; } } - public override void Encode(BcpgOutputStream bcpgOut) + internal byte[] GetAAData() + { + return CreateAAData(Version, EncAlgorithm, AeadAlgorithm); + } + + internal static byte[] CreateAAData(int version, SymmetricKeyAlgorithmTag encAlgorithm, AeadAlgorithmTag aeadAlgorithm) { - MemoryStream bOut = new MemoryStream(); - using (var pOut = new BcpgOutputStream(bOut)) + return new byte[] { - pOut.Write((byte)version, (byte)encAlgorithm); - pOut.WriteObject(s2k); + 0xC0 | (byte)PacketTag.SymmetricKeyEncryptedSessionKey, + (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/bcpg/TrustPacket.cs b/crypto/src/bcpg/TrustPacket.cs index ad227ddc88..4d143ebccf 100644 --- a/crypto/src/bcpg/TrustPacket.cs +++ b/crypto/src/bcpg/TrustPacket.cs @@ -7,11 +7,13 @@ public class TrustPacket private readonly byte[] m_levelAndTrustAmount; public TrustPacket(BcpgInputStream bcpgIn) + :base(PacketTag.Trust) { m_levelAndTrustAmount = bcpgIn.ReadAll(); } public TrustPacket(int trustCode) + : base(PacketTag.Trust) { m_levelAndTrustAmount = new byte[]{ (byte)trustCode }; } diff --git a/crypto/src/bcpg/UserAttributePacket.cs b/crypto/src/bcpg/UserAttributePacket.cs index e976f1215f..8d1261f700 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 7d3d3a846f..86ae6d7b35 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/bcpg/X25519PublicBCPGKey.cs b/crypto/src/bcpg/X25519PublicBCPGKey.cs new file mode 100644 index 0000000000..edb8f67e8e --- /dev/null +++ b/crypto/src/bcpg/X25519PublicBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class X25519PublicBcpgKey + : OctetArrayBcpgKey + { + // https://www.rfc-editor.org/rfc/rfc9580#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 0000000000..cbdf8abd58 --- /dev/null +++ b/crypto/src/bcpg/X25519SecretBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class X25519SecretBcpgKey + : OctetArrayBcpgKey + { + // https://www.rfc-editor.org/rfc/rfc9580#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 0000000000..c6f370bb12 --- /dev/null +++ b/crypto/src/bcpg/X448PublicBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class X448PublicBcpgKey + : OctetArrayBcpgKey + { + // https://www.rfc-editor.org/rfc/rfc9580#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 0000000000..16bfab2f5d --- /dev/null +++ b/crypto/src/bcpg/X448SecretBCPGKey.cs @@ -0,0 +1,19 @@ +namespace Org.BouncyCastle.Bcpg +{ + public sealed class X448SecretBcpgKey + : OctetArrayBcpgKey + { + // https://www.rfc-editor.org/rfc/rfc9580#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/bcpg/sig/Features.cs b/crypto/src/bcpg/sig/Features.cs index a04d2cf9da..df80bde32e 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 }; diff --git a/crypto/src/crypto/generators/Argon2BytesGenerator.cs b/crypto/src/crypto/generators/Argon2BytesGenerator.cs index d9e4fc3d3e..8e182f85e0 100644 --- a/crypto/src/crypto/generators/Argon2BytesGenerator.cs +++ b/crypto/src/crypto/generators/Argon2BytesGenerator.cs @@ -770,3 +770,4 @@ internal Position(int pass, int slice, int lane) } } } + diff --git a/crypto/src/openpgp/PgpEncryptedDataGenerator.cs b/crypto/src/openpgp/PgpEncryptedDataGenerator.cs index ac847ddb6b..2c55469530 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; @@ -22,9 +22,10 @@ public class PgpEncryptedDataGenerator { private BcpgOutputStream pOut; private CipherStream cOut; + private AeadOutputStream aeadOut; private IBufferedCipher c; - private bool withIntegrityPacket; - private bool oldFormat; + private readonly bool withIntegrityPacket; + private readonly bool oldFormat; private DigestStream digestOut; private abstract class EncMethod @@ -32,24 +33,37 @@ private abstract class EncMethod { protected byte[] sessionInfo; protected SymmetricKeyAlgorithmTag encAlgorithm; + protected AeadAlgorithmTag aeadAlgorithm; protected KeyParameter key; + protected byte[] aeadIv; - 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; + private readonly int skeskVersion; internal PbeMethod( SymmetricKeyAlgorithmTag encAlgorithm, + AeadAlgorithmTag aeadAlgorithm, S2k s2k, - KeyParameter key) + KeyParameter key, + int skeskVersion) + : base(PacketTag.SymmetricKeyEncryptedSessionKey) { this.encAlgorithm = encAlgorithm; + this.aeadAlgorithm = aeadAlgorithm; this.s2k = s2k; this.key = key; + this.skeskVersion = skeskVersion; } public KeyParameter GetKey() @@ -57,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); + } - this.sessionInfo = c.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); + + 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); } @@ -85,11 +147,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( @@ -105,6 +170,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; @@ -268,6 +413,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); } @@ -289,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); } @@ -297,22 +461,25 @@ public override void Encode(BcpgOutputStream pOut) private readonly List methods = new List(); private readonly SymmetricKeyAlgorithmTag defAlgorithm; + private readonly AeadAlgorithmTag defAeadAlgorithm; private readonly SecureRandom rand; - public PgpEncryptedDataGenerator( + 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) + :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. @@ -321,12 +488,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. @@ -334,13 +497,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. @@ -351,13 +509,57 @@ 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; + } + + + 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. @@ -392,7 +594,49 @@ 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, defAeadAlgorithm, s2k, PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase), skeskVersion)); + } + + /// 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, + defAeadAlgorithm, + s2k, + PgpUtilities.DoMakeKeyFromPassPhrase(defAlgorithm, s2k, rawPassPhrase, clearPassPhrase), + skeskVersion + )); } /// Add a public key encrypted session key to the encrypted object. @@ -408,7 +652,13 @@ public void AddMethod(PgpPublicKey key, bool sessionKeyObfuscation) throw new ArgumentException("passed in key not an encryption key!"); } - methods.Add(new PubMethod(key, sessionKeyObfuscation)); + if (pkeskVersion == PublicKeyEncSessionPacket.Version6 + && (key.Algorithm == PublicKeyAlgorithmTag.ElGamalEncrypt || key.Algorithm == PublicKeyAlgorithmTag.ElGamalGeneral)) + { + 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)); } private void AddCheckSum( @@ -431,9 +681,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; } @@ -454,7 +717,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"); @@ -467,29 +730,31 @@ private Stream Open( if (methods.Count == 1) { - if (methods[0] is PbeMethod pbeMethod) + if (methods[0] is PbeMethod pbeMethod && skeskVersion == SymmetricKeyEncSessionPacket.Version4) { - key = pbeMethod.GetKey(); + // 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]); } @@ -513,6 +778,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"); @@ -627,6 +949,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/PgpKeyPair.cs b/crypto/src/openpgp/PgpKeyPair.cs index 9cf78fa6fb..bfa2e44c21 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.KeyId, pub.PublicKeyPacket, 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 a04ebc7dfe..6ef7af816f 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(); @@ -274,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); } /// @@ -300,7 +299,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 +345,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); @@ -399,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/PgpLiteralData.cs b/crypto/src/openpgp/PgpLiteralData.cs index 92fafe657a..f9abb5ed48 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/PgpObjectFactory.cs b/crypto/src/openpgp/PgpObjectFactory.cs index 068b851542..3dabd5e5ab 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/PgpOnePassSignature.cs b/crypto/src/openpgp/PgpOnePassSignature.cs index c14e72bf77..c7505b072b 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) @@ -55,6 +52,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) { @@ -147,7 +149,23 @@ public void Update(ReadOnlySpan input) /// Verify the calculated signature against the passed in PgpSignature. public bool Verify(PgpSignature pgpSig) { - byte[] trailer = pgpSig.GetSignatureTrailer(); + 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.rfc-editor.org/rfc/rfc9580#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; + } + // Additional metadata for v5 signatures + byte[] trailer = pgpSig.GetSignatureTrailer(additionalMetadata); sig.BlockUpdate(trailer, 0, trailer.Length); @@ -159,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; } } @@ -174,7 +197,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(); diff --git a/crypto/src/openpgp/PgpPadding.cs b/crypto/src/openpgp/PgpPadding.cs new file mode 100644 index 0000000000..0fd6019299 --- /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/src/openpgp/PgpPbeEncryptedData.cs b/crypto/src/openpgp/PgpPbeEncryptedData.cs index 1c3432d892..c4bca5b20c 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.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) + { + 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.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) + { + 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,96 @@ 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(); + 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( + 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); } - } + + } } diff --git a/crypto/src/openpgp/PgpPrivateKey.cs b/crypto/src/openpgp/PgpPrivateKey.cs index 61487a5b25..57b3bdca61 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 7ee57d4366..1e0a92c231 100644 --- a/crypto/src/openpgp/PgpPublicKey.cs +++ b/crypto/src/openpgp/PgpPublicKey.cs @@ -25,6 +25,41 @@ namespace Org.BouncyCastle.Bcpg.OpenPgp public class PgpPublicKey : PgpObject { + private const byte v4FingerprintPreamble = 0x99; + 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); @@ -32,16 +67,14 @@ public class PgpPublicKey public static byte[] CalculateFingerprint(PublicKeyPacket publicPk) { IBcpgKey key = publicPk.Key; - IDigest digest; + IDigest digest = CreateDigestForFingerprint(publicPk.Version); - if (publicPk.Version <= 3) + if (publicPk.Version <= PublicKeyPacket.Version3) { RsaPublicBcpgKey rK = (RsaPublicBcpgKey)key; try { - digest = PgpUtilities.CreateDigest(HashAlgorithmTag.MD5); - UpdateDigest(digest, rK.Modulus); UpdateDigest(digest, rK.PublicExponent); } @@ -54,13 +87,27 @@ public static byte[] CalculateFingerprint(PublicKeyPacket publicPk) { try { + digest.Update(FingerprintPreamble(publicPk.Version)); + byte[] kBytes = publicPk.GetEncodedContents(); - digest = PgpUtilities.CreateDigest(HashAlgorithmTag.Sha1); + if (publicPk.Version == PublicKeyPacket.Version4) + { + digest.Update((byte)(kBytes.Length >> 8)); + digest.Update((byte)kBytes.Length); + } + else if (publicPk.Version == PublicKeyPacket.Version5 || publicPk.Version == PublicKeyPacket.Version6) + { + 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 +153,7 @@ private void Init() this.fingerprint = CalculateFingerprint(publicPk); - if (publicPk.Version <= 3) + if (publicPk.Version <= PublicKeyPacket.Version3) { RsaPublicBcpgKey rK = (RsaPublicBcpgKey) key; @@ -115,7 +162,14 @@ private void Init() } else { - this.keyId = (long)Pack.BE_To_UInt64(fingerprint, fingerprint.Length - 8); + 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) { @@ -163,9 +217,23 @@ 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; + } } } + + public PgpPublicKey(PublicKeyAlgorithmTag algorithm, AsymmetricKeyParameter pubKey, DateTime time) + :this(PublicKeyPacket.DefaultVersion, algorithm, pubKey, time) + { + } + /// /// Create a PgpPublicKey from the passed in lightweight one. /// @@ -178,7 +246,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)); @@ -218,44 +286,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>(); @@ -401,7 +497,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); } @@ -480,7 +576,7 @@ public long KeyId /// The fingerprint of the public key public byte[] GetFingerprint() { - return (byte[]) fingerprint.Clone(); + return Arrays.Clone(fingerprint); } public bool HasFingerprint(byte[] fingerprint) @@ -509,6 +605,8 @@ public bool IsEncryptionKey case PublicKeyAlgorithmTag.ElGamalGeneral: case PublicKeyAlgorithmTag.RsaEncrypt: case PublicKeyAlgorithmTag.RsaGeneral: + case PublicKeyAlgorithmTag.X25519: + case PublicKeyAlgorithmTag.X448: return true; default: return false; @@ -641,8 +739,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) @@ -1312,4 +1427,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/PgpPublicKeyEncryptedData.cs b/crypto/src/openpgp/PgpPublicKeyEncryptedData.cs index 6459732156..402351bdaf 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.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. + 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.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) + { + 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,82 @@ 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.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. + // * 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(); + + // 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, length); + + 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( + 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( @@ -95,7 +215,35 @@ public Stream GetDataStream( if (!ConfirmCheckSum(sessionData)) throw new PgpKeyValidationException("key checksum failed"); - SymmetricKeyAlgorithmTag symmAlg = (SymmetricKeyAlgorithmTag)sessionData[0]; + if (keyData.Version == PublicKeyEncSessionPacket.Version6) + { + // V6 PKESK + V2 SEIPD + try + { + return GetDataStreamSeipdVersion2(sessionData, (SymmetricEncIntegrityPacket)encData); + } + catch (PgpException) + { + throw; + } + catch (Exception e) + { + throw new PgpException("Exception starting decryption", e); + } + } + + 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 +275,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 +346,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 RFC 9580 for the description of + // the key derivation algorithm for X25519 and X448 + // 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]; + + 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/src/openpgp/PgpSecretKey.cs b/crypto/src/openpgp/PgpSecretKey.cs index 184621b5c1..57b3609488 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; @@ -25,6 +26,8 @@ public class PgpSecretKey private readonly SecretKeyPacket secret; private readonly PgpPublicKey pub; + #region "Internal constructors" + internal PgpSecretKey( SecretKeyPacket secret, PgpPublicKey pub) @@ -33,13 +36,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) { @@ -110,31 +129,75 @@ 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"); } try { - MemoryStream bOut = new MemoryStream(); - BcpgOutputStream pOut = new BcpgOutputStream(bOut); + byte[] keyData = secKey.GetEncoded(); - pOut.WriteObject(secKey); + if (encAlgorithm == SymmetricKeyAlgorithmTag.Null) + { + s2kUsage = SecretKeyPacket.UsageNone; + } + + // 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 + // 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(); - byte[] keyData = bOut.ToArray(); - byte[] checksumData = Checksum(useSha1, keyData, keyData.Length); + 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 @@ -152,17 +215,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); } } } @@ -176,6 +235,98 @@ 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, + keyPair.PublicKey.Version > PublicKeyPacket.Version4 ? HashAlgorithmTag.Sha256 : 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). @@ -232,22 +383,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), - 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). @@ -307,97 +442,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), - encAlgorithm, rawPassPhrase, clearPassPhrase, useSha1, rand, true) - { - } - - private static PgpPublicKey CertifiedPublicKey( - int certificationLevel, - PgpKeyPair keyPair, - string id, - PgpSignatureSubpacketVector hashedPackets, - PgpSignatureSubpacketVector unhashedPackets) - { - 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); - - 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) - { - 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); - - 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, @@ -455,6 +499,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; @@ -485,6 +531,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 { @@ -545,7 +597,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); @@ -777,11 +857,33 @@ 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"); } - return new PgpPrivateKey(KeyId, pubPk, privateKey); + return new PgpPrivateKey(pub, privateKey); } catch (PgpException) { @@ -978,7 +1080,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]; @@ -998,13 +1107,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/src/openpgp/PgpSignature.cs b/crypto/src/openpgp/PgpSignature.cs index d6ffc0f747..b543bb9d2e 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) { @@ -207,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, @@ -222,10 +238,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(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); } @@ -300,7 +331,8 @@ public bool VerifyCertification( PgpPublicKey pubKey) { if (SignatureType != KeyRevocation - && SignatureType != SubkeyRevocation) + && SignatureType != SubkeyRevocation + && SignatureType != DirectKey) { throw new InvalidOperationException("signature is not a key signature"); } @@ -321,7 +353,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); } @@ -332,10 +369,21 @@ public byte[] GetSignatureTrailer() return sigPck.GetSignatureTrailer(); } - /// - /// Return true if the signature has either hashed or unhashed subpackets. - /// - public bool HasSubpackets + public byte[] GetSignatureTrailer(byte[] additionalMetadata) + { + // Additional metadata for v5 signatures + return sigPck.GetSignatureTrailer(additionalMetadata); + } + + public byte[] GetSignatureSalt() + { + return sigPck.GetSignatureSalt(); + } + + /// + /// Return true if the signature has either hashed or unhashed subpackets. + /// + public bool HasSubpackets { get { @@ -514,8 +562,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, @@ -523,9 +576,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 7ff771997a..b5c71ff940 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; @@ -14,17 +15,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,13 +50,24 @@ 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.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. + 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); @@ -60,7 +76,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 +88,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 +221,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.rfc-editor.org/rfc/rfc9580#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 +250,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.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) + { + 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 +335,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 +495,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 +516,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/src/openpgp/PgpUtilities.cs b/crypto/src/openpgp/PgpUtilities.cs index fa04f5f463..d1f5b7173f 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; @@ -26,18 +27,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,11 +107,37 @@ 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); } } + /// + /// Returns the V6 signature salt size for a hash algorithm. + /// https://www.rfc-editor.org/rfc/rfc9580#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)) @@ -157,11 +188,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( @@ -235,7 +273,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) { @@ -297,7 +340,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.Argon2id) + .WithVersion(Argon2Parameters.Version13) + .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) @@ -547,6 +607,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/data/openpgp/big-pkesk-aead-msg.asc b/crypto/test/data/openpgp/big-pkesk-aead-msg.asc new file mode 100644 index 0000000000..7380c636e8 --- /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 RFC 9580 Appendix A.3 +Comment: generated with gosop 2.0.0-alpha +Comment: Session key CFB73D46CF7C13B7535227BEDB5B2D8B4023C5B... +Comment: Session key ...58289D19CF2C33B0DB388B0B6 + +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 +-----END PGP MESSAGE----- \ No newline at end of file diff --git a/crypto/test/data/openpgp/big-skesk-aead-msg.asc b/crypto/test/data/openpgp/big-skesk-aead-msg.asc new file mode 100644 index 0000000000..5a00b879c9 --- /dev/null +++ b/crypto/test/data/openpgp/big-skesk-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 + +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 +sKZt+8/TOYGzbCH/2g== +-----END PGP MESSAGE----- \ 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 71196edaef..de1ce16523 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" + + "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\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/PGPArmoredTest.cs b/crypto/test/src/openpgp/test/PGPArmoredTest.cs index 9fe5da6edd..2cacf1366d 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, addVersionHeader: false)) + { + 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, addVersionHeader: 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 a6f03c06f4..e41c1f19cf 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() diff --git a/crypto/test/src/openpgp/test/PaddingPacketTest.cs b/crypto/test/src/openpgp/test/PaddingPacketTest.cs new file mode 100644 index 0000000000..8c607d7b93 --- /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 diff --git a/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs new file mode 100644 index 0000000000..27606cbe17 --- /dev/null +++ b/crypto/test/src/openpgp/test/PgpCryptoRefreshTest.cs @@ -0,0 +1,1348 @@ +using NUnit.Framework; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +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 PgpCryptoRefreshTest + : SimpleTest + { + // 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.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.rfc-editor.org/rfc/rfc9580#name-sample-version-6-certificat + 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.rfc-editor.org/rfc/rfc9580#name-sample-version-6-secret-key + 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.rfc-editor.org/rfc/rfc9580#name-sample-locked-version-6-sec + 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.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" + + "2azJAAAAAGk2IHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usJ9BvuAqo" + + "/FvLFuGWMbKAdA+epq7V4HOtAPlBWmU8QOd6aud+aSunHQaaEJ+iTFjP2OMW0KBr" + + "NK2ay45cX1IVAQ=="); + + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-inline-signed-messag + private readonly byte[] v6SampleInlineSignedMessage = Base64.Decode( + "xEYGAQobIHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usyxhsTwYJppfk" + + "1S36bHIrDB8eJ8GKVnCPZSXsJ7rZrMkBy0p1AAAAAABXaGF0IHdlIG5lZWQgZnJv" + + "bSB0aGUgZ3JvY2VyeSBzdG9yZToKCi0gdG9mdQotIHZlZ2V0YWJsZXMKLSBub29k" + + "bGVzCsKYBgEbCgAAACkFgmOYo2MiIQbLGGxPBgmml+TVLfpscisMHx4nwYpWcI9l" + + "JewnutmsyQAAAABpNiB2SV9QIYiQ9/Xi7jwYIlFPcFAPVR2G5ckh5ATjSlP7rCfQ" + + "b7gKqPxbyxbhljGygHQPnqau1eBzrQD5QVplPEDnemrnfmkrpx0GmhCfokxYz9jj" + + "FtCgazStmsuOXF9SFQE="); + + // Sample AEAD encryption and decryption - V6 SKESK + V2 SEIPD + // 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( + "w0AGHgcBCwMIpa5XnR/F2Cv/aSJPkZmTs1Bvo7WaanPP+MXvxfQcV/tU4cImgV14" + + "KPX5LEVOtl6+AKtZhsaObnxV0mkCBwEGn/kOOzIZZPOkKRPI3MZhkyUBUifvt+rq" + + "pJ8EwuZ0F11KPSJu1q/LnKmsEiwUcOEcY9TAqyQcapOK1Iv5mlqZuQu6gyXeYQR1" + + "QCWKt5Wala0FHdqW6xVDHf719eIlXKeCYVRuM5o="); + + // 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( + "wz8GHQcCCwMIVqKY0vXjZFP/z8xcEWZO2520JZDX3EawckG2EsOBLP/76gDyNHsl" + + "ZBEj+IeuYNT9YU4IN9gZ02zSaQIHAgYgpmH3MfyaMDK1YjMmAn46XY21dI6+/wsM" + + "WRDQns3WQf+f04VidYA1vEl1TOG/P/+n2tCjuBBPUTPPQqQQCoPu9MobSAGohGv0" + + "K82nyM6dZeIS8wHLzZj9yt5pSod61CRzI/boVw=="); + + // 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( + "wzwGGgcDCwMI6dOXhbIHAAj/tC58SD70iERXyzcmubPbn/d25fTZpAlS4kRymIUa" + + "v/91Jt8t1VRBdXmneZ/SaQIHAwb8uUSQvLmLvcnRBsYJAmaUD3LontwhtVlrFXax" + + "Ae0Pn/xvxtZbv9JNzQeQlm5tHoWjAFN4TLHYtqBpnvEhVaeyrWJYUxtXZR/Xd3kS" + + "+pXjXZtAIW9ppMJI2yj/QzHxYykHOZ5v+Q=="); + + // 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( + "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=="); + + // 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( + "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(); + + #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 PgpPbeEncryptedData", 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() + { + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-version-4-ed25519leg + PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v4Ed25519LegacyPubkeySample); + PgpPublicKey pubKey = pubRing.GetPublicKey(); + + 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)); + } + + [Test] + public void Version4Ed25519LegacyCreateTest() + { + // create a v4 EdDsa_Legacy Pubkey with the same key material and creation datetime as the test vector + // 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")); + 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.rfc-editor.org/rfc/rfc9580#name-sample-version-4-ed25519lega + PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v4Ed25519LegacyPubkeySample); + PgpPublicKey pubKey = pubRing.GetPublicKey(); + + PgpObjectFactory factory = new PgpObjectFactory(v4Ed25519LegacySignatureSample); + PgpSignatureList sigList = factory.NextPgpObject() as PgpSignatureList; + 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[] 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] + public void Version6CertificateParsingTest() + { + /* + * 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 + * 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)); + + // 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]; + 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)); + + // 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()) + { + using (BcpgOutputStream bs = new BcpgOutputStream(ms, newFormatOnly: true)) + { + pubRing.Encode(bs); + } + + byte[] encoded = ms.ToArray(); + IsTrue(AreEqual(encoded, v6Certificate)); + } + } + + [Test] + public void Version6PublicKeyCreationTest() + { + /* + * Create a v6 Ed25519 pubkey with the same key material and creation datetime as the test vector + * 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"); + 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)); + + VerifyEncodedSignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes(v6SampleCleartextSignedMessage), + pubKey); + + VerifyEncodedSignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes("wrongdata"), + pubKey, + shouldFail: true); + } + + [Test] + public void Version6UnlockedSecretKeyParsingTest() + { + /* + * 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 + * 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); + + SignVerifyRoundtrip(signingKey, emptyPassphrase); + + // encryption key + PgpSecretKey encryptionKey = secretKeys[1]; + IsEquals(encryptionKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X25519); + IsEquals(encryptionKey.PublicKey.KeyId, 0x12C83F1E706F6308); + + // Encode-Decode roundtrip + using (MemoryStream ms = new MemoryStream()) + { + using (BcpgOutputStream bs = new BcpgOutputStream(ms, newFormatOnly: true)) + { + secretKeyRing.Encode(bs); + } + + byte[] encoded = ms.ToArray(); + IsTrue(AreEqual(encoded, v6UnlockedSecretKey)); + } + + // 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()); + PgpSignature 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] + public void Version6Ed25519KeyPairCreationTest() + { + /* + * Create a v6 Ed25519 keypair with the same key material and creation datetime as the test vector + * 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); + 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"); + 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)); + + + VerifyEncodedSignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes(v6SampleCleartextSignedMessage), + keypair.PublicKey); + + VerifyEncodedSignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes("wrongdata"), + keypair.PublicKey, + shouldFail: true); + + // 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, + keypair, + uid, + SymmetricKeyAlgorithmTag.Null, + Array.Empty(), + false, + hashed, + null, + rand); + + // 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(); + // 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(); + 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(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)); + + // Sign-Verify roundtrip + SignVerifyRoundtrip(pgpseckey, emptyPassphrase); + + // 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)); + + 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, sign-verify, encrypt-decrypt 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")); + IsEquals(keypair.PublicKey.BitStrength, 448); + long keyId = keypair.PublicKey.KeyId; + byte[] fpr = keypair.PublicKey.GetFingerprint(); + IsEquals(fpr.Length, 32); + + // encode-decode roundtrip + string uid = "Alice "; + PgpKeyRingGenerator keyRingGen = new PgpKeyRingGenerator( + PgpSignature.PositiveCertification, + keypair, + uid, + SymmetricKeyAlgorithmTag.Null, + Array.Empty(), + false, + null, + null, + rand); + + // 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(); + // 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(); + 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(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)); + + // Sign-Verify roundtrip + SignVerifyRoundtrip(pgpseckey, emptyPassphrase); + + // Encrypt-Decrypt roundtrip + PubkeyEncryptDecryptRoundtrip( + Encoding.UTF8.GetBytes("Hello, World!"), + false, + 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.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. + */ + + 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.KeyEncryptionAlgorithm, SymmetricKeyAlgorithmTag.Aes256); + IsEquals(signingKey.KeyEncryptionAeadAlgorithm, AeadAlgorithmTag.Ocb); + 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); + IsEquals(encryptionKey.KeyEncryptionAeadAlgorithm, AeadAlgorithmTag.Ocb); + IsEquals(encryptionKey.PublicKey.Algorithm, PublicKeyAlgorithmTag.X25519); + IsEquals(encryptionKey.PublicKey.KeyId, 0x12C83F1E706F6308); + + // encrypt-decrypt + PubkeyEncryptDecryptRoundtrip( + Encoding.UTF8.GetBytes("Hello, World!"), + false, + false, + encryptionKey.PublicKey, + encryptionKey.ExtractPrivateKey(passphrase.ToCharArray())); + + // Encode-Decode roundtrip + 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() + { + // https://www.rfc-editor.org/rfc/rfc9580#name-sample-cleartext-signed-mes + PgpPublicKeyRing pubRing = new PgpPublicKeyRing(v6Certificate); + PgpPublicKey pubKey = pubRing.GetPublicKey(); + + VerifyEncodedSignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes(v6SampleCleartextSignedMessage), + pubKey); + + VerifyEncodedSignature( + v6SampleCleartextSignedMessageSignature, + Encoding.UTF8.GetBytes("wrongdata"), + pubKey, + shouldFail: true); + } + + [Test] + public void Version6SampleInlineSignedMessageVerifySignatureTest() + { + // https://www.rfc-editor.org/rfc/rfc9580#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); + } + + [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, // 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!"); + 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); + }); + } + + 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); + }); + } + + /* + * 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-skesk-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 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 + }; + + 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); + }); + } + + // 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); + } + + + [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 + * RFC 9580 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)); + } + } + } + + // encrypt-decrypt roundtrip + { + 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); + + // 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); + } + } + + public override string Name => "PgpCryptoRefreshTest"; + + public override void PerformTest() + { + Version4Ed25519LegacyPubkeySampleTest(); + Version4Ed25519LegacySignatureSampleTest(); + Version4Ed25519LegacyCreateTest(); + Version6CertificateParsingTest(); + Version6PublicKeyCreationTest(); + Version6Ed25519KeyPairCreationTest(); + Version6Ed448KeyPairCreationTest(); + Version6UnlockedSecretKeyParsingTest(); + Version6LockedSecretKeyParsingTest(); + Version6SampleCleartextSignedMessageVerifySignatureTest(); + Version6SampleInlineSignedMessageVerifySignatureTest(); + 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 new file mode 100644 index 0000000000..f3791c6558 --- /dev/null +++ b/crypto/test/src/openpgp/test/PgpInteroperabilityTestSuite.cs @@ -0,0 +1,818 @@ +using NUnit.Framework; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.Utilities.Test; +using System; +using System.IO; +using System.Linq; +using System.Runtime.Serialization.Formatters; +using System.Text; + +namespace Org.BouncyCastle.Bcpg.OpenPgp.Tests +{ + [TestFixture] + public class PgpInteroperabilityTestSuite + : SimpleTest + { + // 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" + + "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=="); + + // 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( + "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" + + "NEJd3XZRzaXZE2aAMQ=="); + + private static readonly byte[] bobSecretkey = Base64.Decode( + "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"); + + // 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( + "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" + ); + + // 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" + + "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 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" + + "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 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) + { + 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 MultiplePkeskTest() + { + // Encrypt-Decrypt roundtrip with multiple keys: the plaintext + // "Hello World :)" is encrypted with the X25519 sample 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" + + "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 RFC 9580 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() + { + // Verify Inline Signature with multiple keys: + // 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 + 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 RFC 9580 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 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 :)"); + 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 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 :("); + 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)); + } + + [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.rfc-editor.org/rfc/rfc9580#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(); + + MultipleDetachedSignatureTest(); + GenerateAndVerifyMultipleDetachedSignatureTest(); + + Version5KeyParsingTest(); + Version5InlineSignatureTest(); + } + } +} diff --git a/crypto/test/src/openpgp/test/PgpKeyRingTest.cs b/crypto/test/src/openpgp/test/PgpKeyRingTest.cs index 821a7f2952..6b2cc3e704 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"); }