OpenPGP

OpenPGP 是 PGP 和 GnuPG 背后的标准。它可以把文件和消息加密给一个或多个接收者, 对其签名,使接收方能够核实是谁写的、内容是否被改动,而且这一切都不需要证书颁发机构: 每个用户拥有一对密钥,并公开其中的公钥部分。它是加密电子邮件、签名的软件发布包、 软件包仓库,以及与合作伙伴和银行交换文件时所使用的格式。

 

sgcCrypto 单元以纯 Object Pascal 实现了 OpenPGP,不依赖任何外部 DLL,也不依赖 OpenSSL 和 Indy。 它们可以在 Win32 和 Win64 上从 Delphi 7 编译到 Delphi 13,这些单元也可以为 Linux64 编译。 所有操作都在内存中基于 TBytes 完成: armor 输出是以 TBytes 返回的 ASCII 文本, 辅助函数 sgcPGP_UTF8 和 sgcPGP_UTF8ToString 用于在字符串和字节之间转换。

 

版本 4 还是版本 6

 

支持两个版本的格式,二者都可以读取和写入。

 

版本 4(RFC 4880 和 RFC 6637) 是 GnuPG、Kleopatra、Thunderbird 以及几乎所有已部署工具目前使用的版本。 只要对方运行 GnuPG,或者您无法控制对方的软件,就应选择它。消息使用 SEIPD v1 保护 (CFB 模式的 AES 加上 MDC 完整性校验)。

 

版本 6(RFC 9580)是该标准的 当前修订版。密钥原生使用 Ed25519/X25519 或 Ed448/X448,私钥使用 Argon2 保护, 消息使用 SEIPD v2,这是真正的 AEAD 构造(默认为 OCB)。当所有参与方 都支持 RFC 9580 时选择它,例如在您自己开发的两个应用程序之间。 GnuPG 2.4 不能读取 v6 密钥。

 

您不必为每条消息单独选择。加密时,只有在每个接收者都支持 SEIPD v2(v6 密钥)时才会使用它。 只要有一个接收者使用 v4 密钥,消息就会按 SEIPD v1 写入,因此混合接收者 列表依然可以被 GnuPG 读取。

 

支持的功能

 

领域 支持内容
格式 OpenPGP v4(RFC 4880、RFC 6637,兼容 GnuPG)和 v6(RFC 9580)
公钥算法 RSA 2048/3072/4096、Ed25519、Ed448(仅 v6)、X25519、X448、NIST P-256/P-384/P-521 上的 ECDSA 和 ECDH、Brainpool 曲线上的 ECDSA 和 ECDH(仅导入)
对称密码 AES-128、AES-192、AES-256
加密数据 SEIPD v1(CFB 加 MDC)、使用 OCB、EAX 或 GCM 的 SEIPD v2 AEAD
会话密钥 公钥(PKESK v3 和 v6)与口令(SKESK v4 和 v6)
密钥保护(S2K) 迭代加盐 S2K、Argon2 S2K
压缩 ZIP(原始 DEFLATE)和 ZLIB
签名 分离式、内联(单遍)和明文签名,二进制和文本模式
编码 ASCII armor 和二进制
GnuPG 2.4 LibrePGP 读取 OCB 加密消息(数据包 20)和 v5 SKESK 数据包,但从不写入

 

单元

 

单元 内容
sgcCrypto_PGP 高级 API:加密、解密、签名、验证、结果记录
sgcCrypto_PGP_Cert 可传输密钥、密钥环和密钥生成:TsgcPGPKey、 TsgcPGPKeyRing、sgcPGP_GenerateKey
sgcCrypto_PGP_Message 消息数据包:会话密钥、加密数据、字面数据和压缩数据
sgcCrypto_PGP_Signature 签名数据包,创建与验证
sgcCrypto_PGP_Keys 密钥数据包和私钥保护
sgcCrypto_PGP_Packets 数据包封装、ASCII armor、S2K、常量和辅助函数
sgcCrypto_Modes AES-OCB 和 AES-EAX,供 SEIPD v2 使用(参见对称加密)
sgcCrypto_Deflate DEFLATE 和 ZLIB 压缩(参见WinZip AE-2 归档加密)

 

大多数应用程序只需要在 uses 子句中加入 sgcCrypto_PGP、 sgcCrypto_PGP_Cert 和 sgcCrypto_PGP_Packets。 这些单元属于密码学包,并且在 FIPS 构建中被排除(SGC_CRYPTO_FIPS, 参见FIPS 模式)。

 

密钥管理

 

sgcPGP_GenerateKey 会创建一个主签名密钥和一个加密子密钥,用自签名绑定用户 ID,并返回一个 TsgcPGPKey。 口令在保存时保护私钥;返回的 对象已经处于解锁状态,可以立即签名和解密。口令为空时, 私钥将不受保护地保存。aExpirationSeconds 设置从创建时起算的密钥有效期,0 表示永不过期。

 

TsgcPGPKeyType 版本 4 版本 6
pgpkEd25519EdDSA Ed25519 + ECDH Curve25519Ed25519 + X25519
pgpkEd448不可用Ed448 + X448
pgpkRSA2048RSA 2048不可用(RFC 9580 的最小值为 3072)
pgpkRSA3072RSA 3072RSA 3072
pgpkRSA4096RSA 4096RSA 4096
pgpkNISTP256ECDSA + ECDH P-256ECDSA + ECDH P-256
pgpkNISTP384ECDSA + ECDH P-384ECDSA + ECDH P-384
pgpkNISTP521ECDSA + ECDH P-521ECDSA + ECDH P-521

 


function sgcPGP_GenerateKey(const aUserID, aPassphrase: string;
  aKeyType: TsgcPGPKeyType = pgpkEd25519; aVersion: Integer = 4;
  aExpirationSeconds: Cardinal = 0): TsgcPGPKey;

var
  oAlice: TsgcPGPKey;
  vPublic, vPrivate: string;
begin
  oAlice := sgcPGP_GenerateKey('Alice <alice@example.com>', 'alice passphrase',
    pgpkEd25519, 4);
  try
    vPublic := oAlice.SaveToArmored(False);  { share this one }
    vPrivate := oAlice.SaveToArmored(True);  { keep this one, protected by the passphrase }
    WriteLn(oAlice.FingerprintHex + ' ' + oAlice.PrimaryUserID);
  finally
    oAlice.Free;
  end;
end;

 

如果从文件加载的密钥在保存时使用了口令,则它处于锁定状态。 在用它签名之前先调用 Unlock; 口令错误时它返回 False。 Protect 会用新口令重新加密私钥材料,修改口令就是这样完成的。 v4 密钥使用 AES-256 和迭代加盐 S2K 保护;v6 密钥使用 OCB 模式的 AES-256 和 Argon2 S2K。

 


var
  oKey: TsgcPGPKey;
begin
  oKey := TsgcPGPKey.Create;
  try
    oKey.LoadFromBytes(sgcPGP_UTF8(vPrivate));  { armored or binary }
    if not oKey.Unlock('alice passphrase') then
      raise Exception.Create('wrong passphrase');
    oKey.Protect('new passphrase');
    vPrivate := oKey.SaveToArmored(True);
  finally
    oKey.Free;
  end;
end;

 

TsgcPGPKeyRing 可以容纳任意数量的密钥并拥有它们:传给 Add 的密钥会随密钥环一起释放,因此不要自行释放它。LoadFromFile 和 LoadFromBytes 接受包含一个或多个密钥的 armor 或二进制数据(GnuPG 导出的 整个密钥环也可以),并追加到已加载的内容之后。查找方式:

 

 

每个密钥都提供 Version、 FingerprintHex、 KeyIDHex、 PrimaryUserID、 HasSecret、 IsValid、 IsRevoked、 IsExpired 和 ExpirationTime, 以及它的用户 ID 和子密钥。自签名、子密钥绑定、有效期 和吊销都会在加载时检查,已吊销或已过期的密钥绝不会 被选用于加密或签名。

 

加密与解密

 

sgcPGP_Encrypt 将数据加密给一个或多个公钥。传入签名者时,会在加密消息内部对数据签名, 这与 gpg --sign --encrypt 产生的结果相同。签名者必须已解锁。数据默认使用 ZIP 压缩;传入 CSGC_PGP_COMP_ZLIB 或 CSGC_PGP_COMP_NONE 可更改。当每个接收者的首选项中都列出 AES-256 时使用 AES-256 (SEIPD v2 始终如此),否则使用 AES-128。

 


function sgcPGP_Encrypt(const aData: TBytes;
  const aRecipients: array of TsgcPGPKey; aSigner: TsgcPGPKey = nil;
  aArmor: Boolean = True; aCompression: Integer = CSGC_PGP_COMP_ZIP;
  const aFileName: string = ''): TBytes;

var
  oPublicRing: TsgcPGPKeyRing;
  oBob: TsgcPGPKey;
  oEncrypted: TBytes;
begin
  oPublicRing := TsgcPGPKeyRing.Create;
  try
    oPublicRing.LoadFromFile('public-keys.asc');
    oBob := oPublicRing.FindByUserID('bob@example.com');
    if oBob = nil then
      raise Exception.Create('Bob is not in the keyring');
    { oAlice is an unlocked secret key, used as signer }
    oEncrypted := sgcPGP_Encrypt(sgcPGP_UTF8('Hello Bob'), [oBob], oAlice);
    WriteLn(sgcPGP_UTF8ToString(oEncrypted));  { -----BEGIN PGP MESSAGE----- }
  finally
    oPublicRing.Free;
  end;
end;

 

如果使用共享密码而不是密钥,请使用 sgcPGP_EncryptSymmetric。 当 aVersion = 4 时,它写入 v4 SKESK 和 SEIPD v1,gpg --decrypt 可以读取。当为 6 时,它写入带 Argon2 的 v6 SKESK 和使用 AES-256 OCB 的 SEIPD v2。

 


function sgcPGP_EncryptSymmetric(const aData: TBytes; const aPassphrase: string;
  aArmor: Boolean = True; aVersion: Integer = 4;
  aCompression: Integer = CSGC_PGP_COMP_ZIP;
  const aFileName: string = ''): TBytes;

  oEncrypted := sgcPGP_EncryptSymmetric(sgcPGP_UTF8('Hello'), 'shared passphrase');

 

sgcPGP_Decrypt 同时处理这两种方式,armor 或二进制均可。aSecretKeys 保存要尝试的私钥;aPassphrase 用于解锁它们,同时也会被当作消息密码尝试。已锁定的密钥 在私有副本上解锁,因此传入的密钥环永远不会被修改。 aVerifyKeys 保存用于检查内嵌签名的公钥,可以为 nil。无法解密的消息会引发 EsgcPGPError。

 


function sgcPGP_Decrypt(const aData: TBytes; aSecretKeys: TsgcPGPKeyRing;
  const aPassphrase: string; aVerifyKeys: TsgcPGPKeyRing;
  out aResult: TsgcPGPDecryptResult): TBytes;

var
  oSecretRing: TsgcPGPKeyRing;
  oResult: TsgcPGPDecryptResult;
  oData: TBytes;
begin
  oSecretRing := TsgcPGPKeyRing.Create;
  try
    oSecretRing.LoadFromFile('bob-secret.asc');
    try
      oData := sgcPGP_Decrypt(oEncrypted, oSecretRing, 'bob passphrase',
        oPublicRing, oResult);
      WriteLn(sgcPGP_UTF8ToString(oData));
      if oResult.Verify.Valid then
        WriteLn('signed by ' + oResult.Verify.SignerUserID)
      else if oResult.Verify.Signed then
        WriteLn('signature not valid: ' + oResult.Verify.Status);
    except
      on E: EsgcPGPError do
        WriteLn('cannot decrypt: ' + E.Message);
    end;
  finally
    oSecretRing.Free;
  end;
end;

 

TsgcPGPDecryptResult 记录描述了被解密的内容:

 

字段 含义
FileName字面数据包中保存的文件名,可能为空
Date字面数据的日期,Unix 时间
Format字面数据格式字节:'b' 为二进制,'t' 或 'u' 为文本
Encrypted输入已加密时为 True,仅签名的消息为 False
SymAlgo对称算法 ID(7、8、9 分别对应 AES-128、AES-192、AES-256)
AEADAlgoAEAD 模式 ID(1 EAX,2 OCB,3 GCM),SEIPD v1 为 0
RecipientKeyID解密该消息的私钥的密钥 ID,使用口令时为空
Verify针对内嵌签名的 TsgcPGPVerifyResult,见下文

 

签名与验证

 

三种签名涵盖了常见情形:

 

 

签名者必须已解锁。aTextMode 使用规范的 CRLF 行尾对文本签名,因此文件在 Windows 和 Linux 之间移动后, 签名仍然可以验证通过。

 


function sgcPGP_Sign(const aData: TBytes; aSigner: TsgcPGPKey;
  aDetached: Boolean = True; aArmor: Boolean = True; aTextMode: Boolean = False;
  const aFileName: string = ''): TBytes;
function sgcPGP_ClearSign(const aText: string; aSigner: TsgcPGPKey): string;
function sgcPGP_Verify(const aSignedMessage: TBytes; aKeys: TsgcPGPKeyRing;
  out aData: TBytes; out aResult: TsgcPGPVerifyResult): Boolean;
function sgcPGP_VerifyDetached(const aData, aSignature: TBytes;
  aKeys: TsgcPGPKeyRing; out aResult: TsgcPGPVerifyResult): Boolean;

var
  oData, oSignature, oSigned, oOut: TBytes;
  oVerify: TsgcPGPVerifyResult;
  vClear: string;
begin
  oData := sgcPGP_UTF8('release 1.0');

  { detached }
  oSignature := sgcPGP_Sign(oData, oAlice);
  if sgcPGP_VerifyDetached(oData, oSignature, oPublicRing, oVerify) then
    WriteLn('valid, signed by ' + oVerify.SignerUserID);

  { inline }
  oSigned := sgcPGP_Sign(oData, oAlice, False);
  if sgcPGP_Verify(oSigned, oPublicRing, oOut, oVerify) then
    WriteLn(sgcPGP_UTF8ToString(oOut));

  { cleartext }
  vClear := sgcPGP_ClearSign('Hello', oAlice);
  if sgcPGP_Verify(sgcPGP_UTF8(vClear), oPublicRing, oOut, oVerify) then
    WriteLn('valid cleartext signature');
end;

 

TsgcPGPVerifyResult 记录报告找到的第一个有效签名,如果没有有效签名,则报告 第一个签名:

 

字段 含义
Signed消息至少带有一个签名时为 True
Valid签名已通过密钥环中某个有效密钥验证时为 True
SignerKeyID, SignerFingerprint进行签名的密钥(或子密钥)的十六进制密钥 ID 和指纹
SignerUserID签名者的主用户 ID
SignatureTime签名创建时间,Unix 时间
HashAlgo哈希算法 ID,sgcPGP_HashName 给出其名称
Status有效时为空,否则为原因:未签名、未找到签名者密钥、签名错误、签名时签名者密钥无效、签名已过期、签名格式错误、意外的签名类型

 

来自不在密钥环中的密钥的签名永远不会有效: Status 为 signer key not found, 签名者字段保持为空。只应信任 Valid, 并且仅限于您自己放入密钥环的密钥。

 

与 GnuPG 的互操作性

 

与 GnuPG 交换数据时请使用 v4 密钥。armor 文件可以双向原样使用。 GnuPG 一侧的典型命令:

 


gpg --import alice-public.asc
gpg --armor --export bob@example.com > bob-public.asc
gpg --armor --export-secret-keys bob@example.com > bob-secret.asc

gpg --decrypt message.asc
gpg --armor --sign --encrypt --recipient alice@example.com file.txt
gpg --armor --symmetric file.txt

gpg --verify file.txt.asc file.txt
gpg --verify clearsigned.asc
gpg --armor --detach-sign file.txt
gpg --clearsign note.txt

 

GnuPG 导出的密钥可以用 TsgcPGPKeyRing.LoadFromFile 加载, 包括由口令保护的私钥。GnuPG 2.4 会在其密钥上标记 LibrePGP OCB 特性,并可能使用数据包 20(OCB 加密数据)或 v5 SKESK 数据包向它们加密。这些消息可以被读取 和解密,但绝不会被写入:在此为 v4 密钥加密的数据始终使用 SEIPD v1,所有 GnuPG 版本都能读取。已 转移到智能卡的私钥在 GnuPG 导出中显示为不含 私钥材料的存根;它们可以加载,但不能签名或解密。

 

安全说明

 

 

限制

 

 

API 参考

 

名称 单元 用途
sgcPGP_EncryptsgcCrypto_PGP加密给公钥,可选签名
sgcPGP_EncryptSymmetricsgcCrypto_PGP使用口令加密,v4 或 v6
sgcPGP_DecryptsgcCrypto_PGP解密并验证内嵌签名
sgcPGP_SignsgcCrypto_PGP分离式或内联签名
sgcPGP_ClearSignsgcCrypto_PGP明文签名
sgcPGP_VerifysgcCrypto_PGP验证内联或明文签名的消息
sgcPGP_VerifyDetachedsgcCrypto_PGP验证分离式签名
TsgcPGPDecryptResult, TsgcPGPVerifyResultsgcCrypto_PGP结果记录
sgcPGPMaxDecompressedSizesgcCrypto_PGP解压限制,0 表示不限制
sgcPGP_GenerateKeysgcCrypto_PGP_Cert生成 v4 或 v6 密钥
TsgcPGPKeysgcCrypto_PGP_Cert带用户 ID 和子密钥的单个密钥:LoadFromBytes, SaveToBytes, SaveToArmored, Unlock, Protect
TsgcPGPKeyRingsgcCrypto_PGP_Cert拥有密钥的列表:Add, Remove, Clear, LoadFromBytes, LoadFromFile, SaveToArmored, FindByID, FindByUserID, FindKeyPacket
TsgcPGPKeyTypesgcCrypto_PGP_Cert用于生成的密钥算法
sgcPGP_Armor, sgcPGP_Dearmor, sgcPGP_IsArmoredsgcCrypto_PGP_PacketsASCII armor 编码和解码
sgcPGP_UTF8, sgcPGP_UTF8ToStringsgcCrypto_PGP_Packets字符串与 UTF-8 字节之间的相互转换
sgcPGPMaxArgon2EncodedM, sgcPGPMaxArgon2Passes, sgcPGPMaxArgon2LanessgcCrypto_PGP_PacketsArgon2 S2K 限制
CSGC_PGP_COMP_NONE, CSGC_PGP_COMP_ZIP, CSGC_PGP_COMP_ZLIBsgcCrypto_PGP_Packets加密时的压缩方式
EsgcPGPErrorsgcCrypto_PGP_Packets发生任何 OpenPGP 错误时引发的异常

 


function sgcPGP_Armor(const aData: TBytes; aType: TsgcPGPArmorType;
  aWithChecksum: Boolean = True): string;
function sgcPGP_Dearmor(const aText: string;
  out aType: TsgcPGPArmorType): TBytes;
{ TsgcPGPArmorType = (pgpaMessage, pgpaPublicKey, pgpaPrivateKey, pgpaSignature) }

 

演示程序

 

Demos\18.Cryptography\09.OpenPGP 文件夹(sgcOpenPGP.dpr) 展示了本页中 API 的实际用法:密钥生成、加密和 解密、签名与验证。