In this blog post I am going to discuss about Digital signature and PGP encryption for txt/xml data. This is generally required for payment processing files being sent out of D365 FO.
As a pre-requisite, you must have a PGP encrypted public key in format as
-----BEGIN PGP PUBLIC KEY BLOCK-----
key content
-----END PGP PUBLIC KEY BLOCK-----
, a private key (if you don't have, then generate it using kleopatra tool) in format as
-----BEGIN PGP ARMORED FILE-----
key content
-----END PGP ARMORED FILE-----
and a passphrase for private key.
These keys and passphrase must be securely stored. I have stored it over Azure key vault and is using D365 Key vault parameters to retrieve that using azure client app. (for Dev box, Same is possible via txt files stored locally too, Or as a resource file for Dev and other environments, Or as a txt file over azure storage account).
Although its the safest way to use azure key vault, but there is a minor problem as to how the whole key is stored. Generally key must include special characters '\r\n' (Not directly visible unless read via code or using tools such as notepad++), when pasting it over key vault it ignores the new lines and considers whole data as a single line. To overcome this, do use the line ending characters before pasting it to key vault as
-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: PGP Command Line v10.1.0 (Build 52) (Linux)\r\n\r\nkey content\r\n-----END PGP PUBLIC KEY BLOCK----
Once you are finalized with all these, lets proceed for code.
We will start with the PGP helper class/
using Org.BouncyCastle.Bcpg.OpenPgp; using Org.BouncyCastle.Security; using Org.BouncyCastle.Bcpg; class PGPHelper { public System.IO.Stream EncryptFile(System.IO.Stream inputFile, Str publicKeyString) { PgpPublicKey publicKey = this.ReadPublicKey(publicKeyString); System.IO.Stream outputStream = new System.IO.MemoryStream(); PgpEncryptedDataGenerator encryptedData = new PgpEncryptedDataGenerator(Org.BouncyCastle.Bcpg.SymmetricKeyAlgorithmTag::Cast5, true, new SecureRandom()); encryptedData.AddMethod(publicKey); int bufferSize = 1 << 16; ArmoredOutputStream armoredStream = new ArmoredOutputStream(outputStream); System.IO.Stream outStream = encryptedData.Open(armoredStream, bufferSize); //PgpCompressedDataGenerator compressedData = new PgpCompressedDataGenerator(Org.BouncyCastle.Bcpg.CompressionAlgorithmTag::Uncompressed); //var compressedOut = compressedData.Open(outStream); System.Byte[] buffer = new System.Byte[bufferSize](); int len = inputFile.Read(buffer, 0, bufferSize); if (len > 0) { outStream.write(buffer, 0, len); } armoredStream.Close(); outputStream.Position = 0; return outputStream; } public System.IO.Stream SignFile(System.IO.Stream inputFile, Str privateKeyString, Str passphrase) { PgpPrivateKey privateKey = this.ReadPrivateKey(privateKeyString, passphrase); System.IO.Stream outputStream = new System.IO.MemoryStream(); PgpSignatureGenerator pgpSignatureGenerator = new PgpSignatureGenerator(privateKey.PublicKeyPacket.Algorithm, Org.BouncyCastle.Bcpg.HashAlgorithmTag::Sha256); pgpSignatureGenerator.InitSign(0, privateKey); PgpLiteralDataGenerator literalData = new PgpLiteralDataGenerator(); System.Char format = Global::toanytype('b'); var literalOut = literalData.Open(outputStream, format, "filename", inputFile.Length, DateTimeUtil::utcNow()); var signatureOut = pgpSignatureGenerator.GenerateOnePassVersion(false); signatureOut.Encode(literalOut); inputFile.CopyTo(literalOut); var signature = pgpSignatureGenerator.Generate(); signature.Encode(literalOut); literalOut.Close(); outputStream.Position = 0; return outputStream; } private PgpPublicKey ReadPublicKey(Str publicKeyString) { System.Byte[] getpublicKeyByte = System.Text.Encoding::UTF8.GetBytes(publicKeyString); System.IO.Stream publicKeyStream = new System.IO.MemoryStream(getpublicKeyByte); System.IO.Stream decodedStream = PgpUtilities::GetDecoderStream(publicKeyStream); PgpPublicKeyRingBundle pgpPub = new PgpPublicKeyRingBundle(decodedStream); PgpPublicKeyRing kRing; System.Collections.IEnumerator kRingenumerator = pgpPub.GetKeyRings().GetEnumerator(); while (kRingenumerator.MoveNext()) { kRing = kRingenumerator.Current; PgpPublicKey k; System.Collections.IEnumerator kenumerator = kRing.GetPublicKeys().GetEnumerator(); while (kenumerator.MoveNext()) { k = kenumerator.Current; if (k.IsEncryptionKey) { return k; } } } throw error('No encryption key found in public key ring.'); } private PgpPrivateKey ReadPrivateKey(Str privateKeyString, Str passphrase) { System.Byte[] getprivateKeyByte = System.Text.Encoding::UTF8.GetBytes(privateKeyString); System.Byte[] getpassphraseByte = System.Text.Encoding::UTF8.GetBytes(passphrase); System.IO.Stream privateKeyStream = new System.IO.MemoryStream(getprivateKeyByte); System.IO.Stream decodedStream = PgpUtilities::GetDecoderStream(privateKeyStream); PgpSecretKeyRingBundle pgpSec = new PgpSecretKeyRingBundle(decodedStream); PgpSecretKey secretKey; PgpSecretKeyRing kRing; System.Collections.IEnumerator kRingenumerator = pgpSec.GetKeyRings().GetEnumerator(); while (kRingenumerator.MoveNext()) { kRing = kRingenumerator.Current; PgpSecretKey k; System.Collections.IEnumerator kenumerator = kRing.GetSecretKeys().GetEnumerator(); while (kenumerator.MoveNext()) { k = kenumerator.Current; if (k.IsSigningKey) { secretKey = k; break; } } if (secretKey != null) { break; } } if (secretKey == null) { throw error('No signing key found in secret key ring.'); } System.Char[] passPhraseChar = System.Text.Encoding::UTF8.GetChars(getpassphraseByte); PgpPrivateKey privateKey = secretKey.ExtractPrivateKey(passPhraseChar); return privateKey; } }
internal final class RunnableClass1 { public static void main(Args _args) { KeyVaultParameters keyVaultParameters; KeyVaultCertificateTable certTable; //FinTechDotNetLib.PgpHelper pgpHelper = new FinTechDotNetLib.PgpHelper(); PGPHelper pgpHelper = new PGPHelper(); System.IO.Stream encryptedStream; System.IO.Stream signedStream; select keyVaultParameters where keyVaultParameters.Name = 'keyVaulttest'; System.IO.Stream streamName = new System.IO.MemoryStream(System.Text.Encoding::UTF8.GetBytes(System.IO.File::ReadAllText(@"D:\Test.xml"))); if (keyVaultParameters.DigitalSignature) //This is custom No/Yes field { str privateKey = KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('privatekey').RecId); str formattedPrivateKey; formattedPrivateKey = strReplace(privateKey, "\\r\\n", '\r\n'); signedStream = pgpHelper.SignFile(streamName, formattedPrivateKey, KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('passphrase').RecId)); } if (keyVaultParameters.DigitalSignature && keyVaultParameters.Encryption) //These are custom No/Yes field { str publicKey = KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('publickey').RecId); str formattedPublicKey; formattedPublicKey = strReplace(publicKey, "\\r\\n", '\r\n'); encryptedStream = pgpHelper.EncryptFile(signedStream, formattedPublicKey); } else if (keyVaultParameters.Encryption && !keyVaultParameters.DigitalSignature) //These are custom No/Yes field { str publicKey = KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('publickey').RecId); str formattedPublicKey; formattedPublicKey = strReplace(publicKey, "\\r\\n", '\r\n'); encryptedStream = pgpHelper.EncryptFile(streamName, formattedPublicKey); } if (keyVaultParameters.Encryption) { System.IO.StreamReader streamReader = new System.IO.StreamReader(encryptedStream); info (streamReader.ReadToEnd()); } else { System.IO.StreamReader streamReader = new System.IO.StreamReader(encryptedStream); info (streamReader.ReadToEnd()); } } }
using System; using System.Linq; using System.Text; using System.IO; using Azure.Identity; using Azure.Security.KeyVault.Secrets; using Org.BouncyCastle.Bcpg.OpenPgp; using Org.BouncyCastle.Security; namespace FinTechDotNetLib { public class PgpHelper { public Stream EncryptFile(Stream inputFile, string publicKeyString) { PgpPublicKey publicKey = ReadPublicKey(publicKeyString); MemoryStream outputStream = new MemoryStream(); PgpEncryptedDataGenerator encryptedDataGenerator = new PgpEncryptedDataGenerator(Org.BouncyCastle.Bcpg.SymmetricKeyAlgorithmTag.Cast5, true, new SecureRandom()); encryptedDataGenerator.AddMethod(publicKey); using (Org.BouncyCastle.Bcpg.ArmoredOutputStream armoredOut = new Org.BouncyCastle.Bcpg.ArmoredOutputStream(outputStream)) { // Open the encrypted stream using (Stream encryptedOut = encryptedDataGenerator.Open(armoredOut, new byte[1 << 16])) { //PgpCompressedDataGenerator compressedDataGenerator = new PgpCompressedDataGenerator(Org.BouncyCastle.Bcpg.CompressionAlgorithmTag.Uncompressed); //Stream compressedOut = compressedDataGenerator.Open(encryptedOut); // Write the input file data to the encrypted stream byte[] buffer = new byte[1 << 16]; int len; while ((len = inputFile.Read(buffer, 0, buffer.Length)) > 0) { encryptedOut.Write(buffer, 0, len); } } } outputStream.Position = 0; return outputStream; } public Stream SignFile(Stream inputFile, string privateKeyString, string passphrase) { PgpPrivateKey privateKey = ReadPrivateKey(privateKeyString, passphrase); MemoryStream outputStream = new MemoryStream(); PgpSignatureGenerator signatureGenerator = new PgpSignatureGenerator(Org.BouncyCastle.Bcpg.PublicKeyAlgorithmTag.RsaGeneral, Org.BouncyCastle.Bcpg.HashAlgorithmTag.Sha256); signatureGenerator.InitSign(PgpSignature.BinaryDocument, privateKey); PgpLiteralDataGenerator literalDataGenerator = new PgpLiteralDataGenerator(); Stream literalOut = literalDataGenerator.Open(outputStream, PgpLiteralData.Binary, "filename", inputFile.Length, DateTime.UtcNow); byte[] buffer = new byte[1 << 16]; int len; while ((len = inputFile.Read(buffer, 0, buffer.Length)) > 0) { literalOut.Write(buffer, 0, len); signatureGenerator.Update(buffer, 0, len); } //signatureGenerator.Generate().Encode(literalOut); PgpSignature signature = signatureGenerator.Generate(); // Write the signature to the output stream using (Stream signatureStream = new MemoryStream()) { signature.Encode(signatureStream); signatureStream.Position = 0; signatureStream.CopyTo(outputStream); } literalOut.Close(); outputStream.Position = 0; return outputStream; } private PgpPublicKey ReadPublicKey(string publicKeyString) { using (Stream keyIn = new MemoryStream(Encoding.UTF8.GetBytes(publicKeyString))) using (Stream inputStream = PgpUtilities.GetDecoderStream(keyIn)) { PgpPublicKeyRingBundle publicKeyRingBundle = new PgpPublicKeyRingBundle(inputStream); foreach (PgpPublicKeyRing keyRing in publicKeyRingBundle.GetKeyRings()) { foreach (PgpPublicKey key in keyRing.GetPublicKeys()) { if (key.IsEncryptionKey) { return key; } } } } throw new ArgumentException("Can't find encryption key in key ring."); } private PgpPrivateKey ReadPrivateKey(string privateKeyString, string passphrase) { using (Stream keyIn = new MemoryStream(Encoding.UTF8.GetBytes(privateKeyString))) using (Stream inputStream = PgpUtilities.GetDecoderStream(keyIn)) { PgpSecretKeyRingBundle secretKeyRingBundle = new PgpSecretKeyRingBundle(inputStream); foreach (PgpSecretKeyRing keyRing in secretKeyRingBundle.GetKeyRings()) { foreach (PgpSecretKey key in keyRing.GetSecretKeys()) { if (key.IsSigningKey) { PgpPrivateKey privateKey = key.ExtractPrivateKey(passphrase.ToCharArray()); if (privateKey != null) { return privateKey; } } } } } throw new ArgumentException("Can't find signing key in key ring."); } } }