公開鍵(非対称)暗号は、共有秘密の代わりに鍵ペアを使用します。秘密鍵が署名したものは、対応する公開鍵で検証でき、公開鍵に対して封印されたものは、対応する秘密鍵でしか開くことができません。公開鍵は、敵対者を含む誰に渡しても、何も弱めることはありません。
RSA
鍵生成は大きな素数を探索するため実際に時間がかかり、2048 ビット鍵では数秒を要します。UI スレッドで実行しないでください。暗号化には OAEP を、署名には PSS を使用してください。どちらも PKCS#1 v1.5 にはないランダム化要素を含んでいます。PKCS#1 v1.5 の署名/検証は、それを指定する形式(JWS のRS256が一般的なケースです)と相互運用するためにsgcCrypto_RSAで提供されています。新規の用途では PSS を優先してください。
function sgcRSA_GenerateKey(aBits: Integer): TsgcRSAPrivateKey;
function sgcRSA_PublicKey(const aPrivateKey: TsgcRSAPrivateKey): TsgcRSAPublicKey;
function sgcRSA_OAEP_Encrypt(const aKey: TsgcRSAPublicKey; const aPlain, aLabel: TBytes; aHash: TsgcRSAHash): TBytes;
function sgcRSA_OAEP_Decrypt(const aKey: TsgcRSAPrivateKey; const aCipher, aLabel: TBytes; aHash: TsgcRSAHash; out aPlain: TBytes): Boolean;
function sgcRSA_PSS_Sign(const aKey: TsgcRSAPrivateKey; const aData: TBytes; aHash: TsgcRSAHash; aSaltLen: Integer = -1): TBytes;
function sgcRSA_PSS_Verify(const aKey: TsgcRSAPublicKey; const aData, aSignature: TBytes; aHash: TsgcRSAHash; aSaltLen: Integer = -1): Boolean;
var
oKey: TsgcRSAPrivateKey;
oSignature: TBytes;
begin
oKey := sgcRSA_GenerateKey(2048);
oSignature := sgcRSA_PSS_Sign(oKey, oData, rhSHA256);
if sgcRSA_PSS_Verify(sgcRSA_PublicKey(oKey), oData, oSignature, rhSHA256) then
{ valid };
end;
PKCS#1 v1.5 暗号化も提供されています。Java がRSA/NONE/PKCS1Paddingと呼ぶモードです。これは、それを義務付ける形式と相互運用するためだけに存在しており、それ以外の目的はありません。v1.5 パディングはまさに Bleichenbacher 攻撃が狙う対象であるため、新規の用途では OAEP が引き続き正しい選択です。平文の長さはモジュラスサイズから 11 バイトを引いた値に制限されており、2048 ビット鍵では 245 バイトです。
function sgcRSA_PKCS1_Encrypt(const aKey: TsgcRSAPublicKey; const aPlain: TBytes): TBytes;
function sgcRSA_PKCS1_Decrypt(const aKey: TsgcRSAPrivateKey; const aCipher: TBytes; out aPlain: TBytes): Boolean;
sgcRSA_PKCS1_Decryptは、あらゆる種類のデコード失敗に対してFalseを返し、そのすべてに対して同じ経路をたどります。これらを区別できてしまうことこそが、パディングオラクルが必要とする情報だからです。
OAEP と MGF1 ハッシュの分離
OAEP は2か所でハッシュを使用します。ラベルに対して、そして MGF1 マスク生成関数の内部で、それぞれ使われますが、両者は同じである必要はありません。ここに Java の落とし穴があります。SunJCE はOAEPWithSHA256AndMGF1Paddingを、ダイジェストには SHA-256、MGF1 には依然として SHA-1 を使うものとして解釈しますが、Bouncy Castle は同じ文字列を両方とも SHA-256 として解釈するため、一方が生成した暗号文をもう一方は復号できません。5 パラメータのオーバーロードは、この2つのハッシュを独立して固定できるため、Java 側と通信する際に必要になることがあります。4 パラメータの形式は変更されておらず、両方にaHashを渡します。
function sgcRSA_OAEP_Encrypt(const aKey: TsgcRSAPublicKey; const aPlain, aLabel: TBytes; aHash, aMGFHash: TsgcRSAHash): TBytes; overload;
function sgcRSA_OAEP_Decrypt(const aKey: TsgcRSAPrivateKey; const aCipher, aLabel: TBytes; aHash, aMGFHash: TsgcRSAHash; out aPlain: TBytes): Boolean; overload;
var
oKey: TsgcRSAPrivateKey;
oCipher: TBytes;
begin
{ SunJCE OAEPWithSHA256AndMGF1Padding: SHA-256 label hash, MGF1 on SHA-1. }
oCipher := sgcRSA_OAEP_Encrypt(sgcRSA_PublicKey(oKey), oPlain, nil, rhSHA256, rhSHA1);
end;
ハッシュの不一致は、破損した暗号文と見分けがつきません。したがって、誤ったaMGFHashを渡した場合も、単なるFalseが返るだけで、どちらが誤っているかを示す手がかりは何もありません。両者のピアは事前にこれについて合意しておく必要があります。
鍵は PKCS#1 および SubjectPublicKeyInfo の DER 形式、そして PEM 形式にエクスポートできます。
function sgcRSA_ExportPrivateKeyPEM(const aKey: TsgcRSAPrivateKey): string;
function sgcRSA_ExportPublicKeyPEM(const aKey: TsgcRSAPublicKey): string;
PKCS#8 はもう一方の秘密鍵コンテナであり、BEGIN PRIVATE KEYとして書き出される形式で、Java や .NET が期待するものです。上記の一対の関数は、代わりに古い PKCS#1 のBEGIN RSA PRIVATE KEYを出力します。どちらのコンテナも暗号化されていないため、返ってくるのは鍵材料そのものであり、ログやバックアップが届く場所に置いてはいけません。
function sgcRSA_ExportPrivateKeyPKCS8DER(const aKey: TsgcRSAPrivateKey): TBytes;
function sgcRSA_ExportPrivateKeyPKCS8PEM(const aKey: TsgcRSAPrivateKey): string;
RSA 鍵のインポート
これらが存在するまで、TsgcRSAPrivateKeyはsgcRSA_GenerateKeyからしか得られず、ディスクに保存した鍵はまったく使用できませんでした。インポートルーチンは、両方の秘密鍵コンテナ、PKCS#1 のRSAPrivateKeyと PKCS#8 のPrivateKeyInfo、そして両方の公開鍵エンコーディング、PKCS#1 のRSAPublicKeyと X.509 の SubjectPublicKeyInfo を、DER または PEM のいずれの形式でも読み取ります。暗号化された PKCS#8、つまりBEGIN ENCRYPTED PRIVATE KEY形式はサポートされておらず、Falseを返します。CRT パラメータはファイルに含まれていない場合には再計算されるため、インポートした鍵も全速力で動作します。
function sgcRSA_ImportPrivateKeyDER(const aDER: TBytes; out aKey: TsgcRSAPrivateKey): Boolean;
function sgcRSA_ImportPrivateKeyPEM(const aPEM: string; out aKey: TsgcRSAPrivateKey): Boolean;
function sgcRSA_ImportPublicKeyDER(const aDER: TBytes; out aKey: TsgcRSAPublicKey): Boolean;
function sgcRSA_ImportPublicKeyPEM(const aPEM: string; out aKey: TsgcRSAPublicKey): Boolean;
var
oKey: TsgcRSAPrivateKey;
oSignature: TBytes;
begin
if sgcRSA_ImportPrivateKeyPEM(Memo1.Text, oKey) then
oSignature := sgcRSA_PSS_Sign(oKey, oData, rhSHA256);
end;
証明書は公開鍵そのものではないため、sgcRSA_ImportPublicKeyDERには証明書自体ではなく、証明書から取り出した SubjectPublicKeyInfo を渡してください。sgcRSA_CheckPrivateKeyは、他所から得られたものの内部的な整合性、すなわち n = p*q であること、e*d が 1 と合同であること、そして CRT パラメータが一致することを検証します。
Ed25519、Ed448(署名)
EdDSA のノンスは秘密鍵とメッセージから決定論的に導出されるため、署名に乱数生成器はいっさい関与せず、ノンスの重複によって鍵が漏れる可能性もありません。これは(後述する)ECDSA とは対照的です。sgcCrypto_Ed25519は、sgcWebSockets の他の場所で WebAuthn の EdDSA クレデンシャルタイプが使用している検証/署名のプリミティブです。呼び出し元が既に 32 バイトの公開鍵を知っていること(WebAuthn はクレデンシャルからこれを渡します)を前提としており、独立した鍵生成や公開鍵導出の呼び出しは提供していません。sgcCrypto_Ed448は汎用の署名器で、Ed25519 の 128 ビットに対しておよそ 224 ビットのセキュリティを持ちますが、その分鍵と署名は大きくなり、署名も遅くなります。
{ Ed25519: caller supplies the 32-byte seed and, to verify, the 32-byte public key. }
function sgcEd25519_Sign(const aPrivateSeed, aMessage: TBytes): TBytes;
function sgcEd25519_Verify(const aPublicKey, aMessage, aSignature: TBytes): Boolean;
{ Ed448: 57-byte keys, 114-byte signatures, optional context string. }
procedure sgcEd448_GenerateKeyPair(out aPrivateKey, aPublicKey: TBytes);
function sgcEd448_Sign(const aPrivateKey, aMessage, aContext: TBytes): TBytes;
function sgcEd448_Verify(const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;
X25519、X448(鍵合意)
モンゴメリ曲線上の Diffie-Hellman です。双方が鍵ペアを生成して公開鍵を交換し、それぞれが自分の秘密鍵と相手の公開鍵から同じ共有秘密を計算します。何かを暗号化するために使用する前に、生の出力は必ず KDF(パスワードハッシュ化 & 鍵導出の HKDF を参照)に通してください。生の ECDH の出力自体は、そのままでは鍵として適していません。X448 は、X25519 の 128 ビットに対しておよそ 224 ビットのセキュリティを持ちますが、その分鍵は大きくなり、計算量もおよそ3倍になります。
function sgcX25519_PublicKey(const aPrivateKey: TBytes): TBytes;
function sgcX25519_SharedSecret(const aPrivateKey, aPeerPublicKey: TBytes; out aSecret: TBytes): Boolean;
function sgcX448_PublicKey(const aPrivateKey: TBytes): TBytes;
function sgcX448_SharedSecret(const aPrivateKey, aPeerPublicKey: TBytes; out aSecret: TBytes): Boolean;
var
oAlicePriv, oBobPriv, oAlicePub, oBobPub, oSecretA, oSecretB: TBytes;
begin
oAlicePriv := sgcRandomBytes(32);
oBobPriv := sgcRandomBytes(32);
oAlicePub := sgcX25519_PublicKey(oAlicePriv);
oBobPub := sgcX25519_PublicKey(oBobPriv);
sgcX25519_SharedSecret(oAlicePriv, oBobPub, oSecretA);
sgcX25519_SharedSecret(oBobPriv, oAlicePub, oSecretB);
{ oSecretA = oSecretB }
end;
SharedSecretは、低位数の相手公開鍵、つまり自分の秘密鍵に関わらず共有秘密をすべてゼロにしてしまう公開鍵の小さな集合に対してはFalseを返します。出力を無条件に信頼せず、常に戻り値を確認してください。
ECIES(sealed box 暗号化)
X25519、HKDF-SHA256、XChaCha20-Poly1305 を1回の呼び出しにまとめ、事前の共有秘密なしで受信者の公開鍵にメッセージを暗号化します。aContextは、封印されたメッセージを特定の目的やプロトコルの手順に結び付けます。異なるコンテキストで開こうとすると失敗するため、あるやり取りで封印されたメッセージが別のやり取りに再利用されることを防ぎます。
procedure sgcECIES_GenerateKeyPair(out aPrivateKey, aPublicKey: TBytes);
function sgcECIES_Seal(const aRecipientPublicKey, aPlain, aAAD, aContext: TBytes): TBytes;
function sgcECIES_Open(const aPrivateKey, aSealed, aAAD, aContext: TBytes; out aPlain: TBytes): Boolean;
名前付き曲線上の ECDSA / ECDH
2つの独立したユニットが、実装する曲線ファミリーごとに分かれて short Weierstrass 曲線をカバーしています。
sgcCrypto_ECは NIST 曲線(P-256、P-384、P-521)をカバーしており、PEM 鍵と JWS 署名、つまり JWT ライブラリが期待する形(ES256など)を中心に設計されています。
sgcCrypto_ECCurvesは secp256k1(Bitcoin および Ethereum の曲線)と、RFC 5639 の brainpoolP256r1 / P384r1 / P512r1 曲線をカバーしており、PEM ではなく生の鍵と署名バイト列を直接扱います。
sgcCrypto_ECCurvesのTsgcECCurveは、現在は4種類ではなく7種類の曲線を持ちます。eccSecp256k1、3種類の Brainpool 曲線、そして NIST P-256、P-384、P-521 に対応するeccP256、eccP384、eccP521です。この NIST の3曲線は、これまでsgcCrypto_ECの PEM および JWS API 経由でしか利用できなかったため、P-256 鍵を生のバイト列として保持しているアプリケーションは、いったん PEM を作成する必要がありました。以下の生鍵の生成、署名、検証、ECDH、圧縮の各呼び出しは、現在は7種類すべてをカバーしています。用途が JWS であればsgcCrypto_ECを、鍵材料が既にバイト列であればsgcCrypto_ECCurvesを選んでください。
両方のユニットとも、TsgcECCurveという名前の型と、sgcEC_GenerateKeyPairという名前のルーチンを宣言していますが、それぞれのユニットで意味が異なります。同じuses句に両方のユニットを追加して、いずれかの名前を修飾なしで参照しないでください。ひとつのユニットで本当にどちらも必要な場合は、ユニット名で修飾してください(sgcCrypto_ECCurves.sgcEC_GenerateKeyPair)。
{ sgcCrypto_ECCurves: secp256k1 and Brainpool, RFC 6979 deterministic nonces,
low-S normalized signatures (BIP 62). }
procedure sgcEC_GenerateKeyPair(aCurve: TsgcECCurve; out aPrivateKey, aPublicKey: TBytes);
function sgcECDSA_SignHash(aCurve: TsgcECCurve; const aPrivateKey, aHash: TBytes; aHashKind: TsgcECHash = echSHA256): TBytes;
function sgcECDSA_VerifyHash(aCurve: TsgcECCurve; const aPublicKey, aHash, aSignature: TBytes): Boolean;
function sgcECDH_SharedSecret(aCurve: TsgcECCurve; const aPrivateKey, aPeerPublicKey: TBytes; out aSecret: TBytes): Boolean;
function sgcEC_ValidatePublicKey(aCurve: TsgcECCurve; const aPublicKey: TBytes): Boolean;
function sgcEC_Compress(aCurve: TsgcECCurve; const aPublicKey: TBytes): TBytes;
function sgcEC_Decompress(aCurve: TsgcECCurve; const aCompressed: TBytes): TBytes;
ここでの ECDSA 署名は RFC 6979 の決定論的ノンスを使用しています。ノンスは乱数生成器から取得するのではなく、秘密鍵とメッセージから導出されます。これにより、実運用で ECDSA の秘密鍵が失われる最も一般的な原因、つまり重複した、あるいは部分的に予測可能なノンスを排除しています。署名には Bitcoin(BIP 62)や Ethereum が要求する low-S 正規化が施されていますが、検証はどちらの形式も受け付けます。
DER エンコードされた ECDSA 署名
1つの署名に2種類のエンコーディングがあり、どちらを選ぶかを誤ることは、この分野で最もよくある統合上のバグのひとつです。生のペア R || S(それぞれが曲線のフィールドサイズの半分)は JOSE や WebAuthn が使う形式です。DER のSEQUENCE { r, s }は X.509、CMS、TLS が使う形式です。上記のsgcECDSA_SignHashは生の形式を生成し、以下の呼び出しは DER 形式を生成・受理し、2つの変換関数は再署名することなく両者を変換します。
{ sgcCrypto_ECCurves: raw keys, a digest the caller has already computed. }
function sgcECDSA_SignDER(aCurve: TsgcECCurve; const aPrivateKey, aHash: TBytes; aHashKind: TsgcECHash): TBytes; overload;
function sgcECDSA_VerifyDER(aCurve: TsgcECCurve; const aPublicKey, aHash, aDERSignature: TBytes): Boolean; overload;
function sgcECDSA_RawToDER(aCurve: TsgcECCurve; const aRaw: TBytes): TBytes;
function sgcECDSA_DERToRaw(aCurve: TsgcECCurve; const aDER: TBytes; out aRaw: TBytes): Boolean;
{ sgcCrypto_EC: PEM keys, hashes aData itself with the curve SHA. }
function sgcECDSA_SignDER(const aPrivateKeyPEM: string; const aData: TBytes; aCurveBits: Integer): TBytes; overload;
function sgcECDSA_VerifyDER(const aPublicPoint, aData, aDERSignature: TBytes; aCurveBits: Integer): Boolean; overload;
var
oRaw, oDER: TBytes;
begin
oRaw := sgcECDSA_SignHash(eccP256, oPrivateKey, oDigest, echSHA256);
oDER := sgcECDSA_RawToDER(eccP256, oRaw);
end;
生鍵形式におけるaHashKindは、RFC 6979 ノンスの裏にある HMAC を選ぶだけであり、何かをハッシュ化してくれるわけではありません。特に理由がなければsgcEC_DefaultHash(aCurve)を渡してください。sgcECDSA_RawToDERは、そのペアがちょうど2つのフィールドサイズの半分になっていない場合に空の配列を返し、sgcECDSA_DERToRawは不正な形式の DER、または曲線に収まらない値に対してFalseを返すため、どちらも信頼できない入力に対して安全に使用できます。
EC 鍵のエクスポートとインポート
3種類のコンテナがあり、それらを区別するのは PEM のラベルです。SEC1 のECPrivateKeyはBEGIN EC PRIVATE KEY、PKCS#8 のPrivateKeyInfoはBEGIN PRIVATE KEY、X.509 の SubjectPublicKeyInfo はBEGIN PUBLIC KEYです。Java と .NET は PKCS#8 を求め、OpenSSL やほとんどのコマンドラインツールはどちらも読み取れます。どちらの秘密鍵形式も暗号化されていないため、得られる結果はスカラー値そのものと同じくらい秘密性が高いものです。
function sgcEC_ExportPrivateKeySEC1DER(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): TBytes;
function sgcEC_ExportPrivateKeyPKCS8DER(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): TBytes;
function sgcEC_ExportSubjectPublicKeyInfo(aCurve: TsgcECCurve; const aPublicKey: TBytes): TBytes;
function sgcEC_ExportPrivateKeySEC1PEM(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): string;
function sgcEC_ExportPrivateKeyPKCS8PEM(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): string;
function sgcEC_ExportPublicKeyPEM(aCurve: TsgcECCurve; const aPublicKey: TBytes): string;
function sgcEC_ImportPrivateKeyDER(const aDER: TBytes; out aCurve: TsgcECCurve; out aPrivateKey, aPublicKey: TBytes): Boolean;
function sgcEC_ImportPrivateKeyPEM(const aPEM: string; out aCurve: TsgcECCurve; out aPrivateKey, aPublicKey: TBytes): Boolean;
function sgcEC_ImportPublicKeyDER(const aDER: TBytes; out aCurve: TsgcECCurve; out aPublicKey: TBytes): Boolean;
function sgcEC_ImportPublicKeyPEM(const aPEM: string; out aCurve: TsgcECCurve; out aPublicKey: TBytes): Boolean;
var
oCurve: TsgcECCurve;
oPrivateKey, oPublicKey: TBytes;
begin
if sgcEC_ImportPrivateKeyPEM(Memo1.Text, oCurve, oPrivateKey, oPublicKey) then
Memo2.Text := sgcEC_ExportPrivateKeyPKCS8PEM(oCurve, oPrivateKey, oPublicKey);
end;
インポートルーチンはファイルから曲線を読み取るため、aCurveは渡されるのではなくoutパラメータとして返されます。このユニットが実装していない曲線は、バグではなくネゴシエーションの失敗であるため、これらの関数は例外を発生させることなくFalseを返します。これにより、信頼できない入力に対しても安全に使用できます。公開点は、ファイルにそれが含まれていない場合はスカラー値から再計算され、返す前に曲線に対して検証されます。エクスポートルーチンは逆に例外を発生させます。曲線に一致しない鍵材料は入力ではなく呼び出し元のバグだからです。
これらのコンテナのそれぞれに書き込まれる、ドット区切りの名前付き曲線 OID は直接利用可能であり、エクスポートおよびインポートルーチンが内部で使用しているのと同じものです。
function sgcEC_CurveOID(aCurve: TsgcECCurve): string;
function sgcEC_CurveFromOID(const aDotted: string; out aCurve: TsgcECCurve): Boolean;
常に相手の公開鍵を検証してください
ECDH、ECDSA の検証、ECIES のいずれであっても、自分で生成していない公開鍵を使用する前には、sgcEC_ValidatePublicKeyで、それが実際に曲線上の点であることを検証してください。曲線上にない点は、それを処理する演算を通じて自分自身の秘密鍵のビットを漏らす可能性があります。
Schnorr 署名(BIP-340)
secp256k1 専用であり、Taproot、Nostr、Lightning が使用しているものです。上記の ECDSA とそのまま置き換えられるものではありません。2つの点が異なるためです。公開鍵は x 座標のみの 32 バイトで、y 座標は偶数であることが暗黙の前提となっているため、BIP-340 鍵は、このページの他の箇所で生成される 33 バイトの圧縮形式や 65 バイトの非圧縮形式とは互換性がありません。また、チャレンジは公開鍵をノンスおよびメッセージと一緒にハッシュ化するため、ECDSA 署名にはない形で、署名が鍵に結び付けられます。
function sgcSchnorr_PublicKey(const aPrivateKey: TBytes): TBytes;
function sgcSchnorr_Sign(const aPrivateKey, aMessage, aAuxRand: TBytes): TBytes;
function sgcSchnorr_Verify(const aPublicKey, aMessage, aSignature: TBytes): Boolean;
function sgcSchnorr_TaggedHash(const aTag: string; const aData: TBytes): TBytes;
var
oPrivateKey, oPublicKey, oSignature: TBytes;
begin
oPrivateKey := sgcRandomBytes(32);
oPublicKey := sgcSchnorr_PublicKey(oPrivateKey); { 32 bytes, x-only }
oSignature := sgcSchnorr_Sign(oPrivateKey, oMessage, sgcRandomBytes(32));
if sgcSchnorr_Verify(oPublicKey, oMessage, oSignature) then
{ valid };
end;
aAuxRandは、ノンス導出に混ぜ込まれる 32 バイトの補助的なランダム性です。これはノンスそのものではなく、フォールト攻撃に対する多層防御であり、すべてゼロであってもこの方式は安全なままです。空の配列は 32 バイトのゼロとして扱われるため、BIP-340 のテストベクターを正確に再現できます。実運用では新鮮なランダム性を渡してください。sgcSchnorr_TaggedHashが公開されているのは、BIP-341 と BIP-342 が同じ構成の上にさらにタグを構築するためです。