충분히 큰 양자 컴퓨터는 공개 키 암호화 페이지에 있는 모든 곡선에서의 RSA, X25519/X448, ECDSA/ECDH를 깨뜨릴 것입니다: 그 어떤 수학적 근거도 Shor 알고리즘을 견디지 못합니다. 이 문제는 그런 기계가 오늘 존재해야만 오늘의 문제가 되는 것이 아닙니다. 공격자는 지금 암호화된 트래픽을 기록해 두었다가 그런 기계가 등장하면 복호화할 수 있으며, 이는 흔히 "지금 수집하고 나중에 복호화한다(harvest now, decrypt later)"라고 불리는 전략입니다. 며칠이 아니라 수년간 기밀을 유지해야 하는 모든 것은 이미 포스트 양자 키 교환으로 옮겨가야 합니다.
NIST는 세 가지 방식을 표준화했으며, 모두 이 라이브러리에 구현되어 있고, 모두 SHAKE(해싱 및 메시지 인증 참조) 외에는 아무것도 사용하지 않습니다. 그래서 이들의 안전성은 오직 해시가 견고한지에만 달려 있으며, 깨질 수 있는 격자 이론이나 수 이론 가정이 없습니다.
| 방식 | 목적 | 표준 | 단위 |
| ML-KEM | 키 캡슐화, Diffie-Hellman을 대체 | FIPS 203 | sgcCrypto_MLKEM |
| ML-DSA | 서명, 격자 기반, 기본 선택 | FIPS 204 | sgcCrypto_MLDSA |
| SLH-DSA | 서명, 해시 기반만 사용, 보수적인 대안 | FIPS 205 | sgcCrypto_SLHDSA |
ML-KEM과 하이브리드 구성 (권장)
ML-KEM은 Diffie-Hellman 교환을 대체합니다: 송신자는 수신자의 공개 키로 공유 비밀을 캡슐화하고, 수신자는 개인 키로 이를 디캡슐화하며, 양쪽 모두 같은 32바이트를 얻게 됩니다. 세 가지 매개변수 집합이 있으며, 클수록 보안 여유와 키 크기가 커집니다.
procedure sgcMLKEM_GenerateKeyPair(aParams: TsgcMLKEMParams; out aPublicKey, aPrivateKey: TBytes);
procedure sgcMLKEM_Encapsulate(aParams: TsgcMLKEMParams; const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcMLKEM_Decapsulate(aParams: TsgcMLKEMParams; const aPrivateKey, aCiphertext: TBytes): TBytes;
var
oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
sgcMLKEM_GenerateKeyPair(mlkem768, oPub, oPriv);
sgcMLKEM_Encapsulate(mlkem768, oPub, oCiphertext, oSecretA); { sender }
oSecretB := sgcMLKEM_Decapsulate(mlkem768, oPriv, oCiphertext); { recipient }
{ oSecretA = oSecretB }
end;
FIPS 203 디캡슐화는 절대 실패하지 않습니다.
손상되었거나 악의적인 암호문이 오류를 일으키지 않고, 대신 개인 키에 내장된 암묵적 거부 값에서 유도된 다른 비밀을 조용히 반환합니다. 대신 오류를 반환하면 공격자에게 개인 키를 탐색할 수 있는 복호화 오라클을 넘겨주는 셈이 되므로, 표준은 의도적으로 이를 피합니다. 유도된 비밀이 무엇을 보호하든, 그 불일치를 스스로 감지해야 합니다. 예를 들어 이후 단계에서 AEAD 태그 검증이 실패하는 식입니다.
오늘 실제로 배포해야 할 것은 하이브리드 구성입니다: X25519와 ML-KEM-768을 결합해 공유 비밀이 둘 모두에 의존하도록 합니다. 암호 분석의 발전으로 ML-KEM이 언젠가 깨지더라도 안전하며, 양자 컴퓨터가 X25519를 깨뜨리더라도 안전합니다. 공격자는 둘 다 동시에 깨뜨려야 합니다.
procedure sgcHybrid_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcHybrid_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcHybrid_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;
X-Wing과 TLS 1.3 하이브리드 그룹
sgcCrypto_MLKEM_Hybrid에는 세 가지 하이브리드 계열이 들어 있습니다. 위의 sgcHybrid_ 함수들은 원래의 sgcCrypto 형식이며 호환성을 위해 유지됩니다. X-Wing은 X25519와 ML-KEM-768을 결합한 표준화된 범용 하이브리드 KEM이며, TLS 밖의 새 코드에서는 이것을 사용하십시오. 세 번째 계열은 TLS 1.3 하이브리드 키 셰어를 만드는 용도로만 쓰입니다.
X-Wing(draft-connolly-cfrg-xwing-kem-10)은 비밀을 하나만 저장합니다. 개인 키는 32바이트 시드이며, 모든 연산이 이를 확장해 필요한 ML-KEM-768과 X25519 자료를 만들어 냅니다. 공개 키는 1216바이트, 암호문은 1120바이트, 공유 비밀은 32바이트입니다. 캡슐화는 FIPS 203 캡슐화 키 검사를 수행하며, 공개 키가 이 검사를 통과하지 못하면 오류를 발생시킵니다.
procedure sgcXWing_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcXWing_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcXWing_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;
var
oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
sgcXWing_GenerateKeyPair(oPub, oPriv);
sgcXWing_Encapsulate(oPub, oCiphertext, oSecretA); { sender }
if sgcXWing_Decapsulate(oPriv, oCiphertext, oSecretB) then
{ oSecretA = oSecretB };
end;
RFC 10024의 TLS 1.3 하이브리드 키 셰어는 X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024이며, IANA 코드포인트는 각각 0x11EC, 0x11EB, 0x11ED입니다. 이들은 단독으로 사용할 수 있는 KEM이 아닙니다. 공유 비밀은 두 절반을 단순히 이어 붙인 값이며, TLS가 이를 핸드셰이크 트랜스크립트와 함께 자체 키 스케줄에 넣기 때문에만 안전합니다. 그러므로 TLS 키 셰어를 만드는 용도로만 사용하십시오. 네이티브 TLS 엔진은 기본적으로 X25519MLKEM768을 가장 먼저 제안하므로, 이를 사용하는 클라이언트나 서버는 추가 코드 없이 이미 포스트 양자 키 교환을 협상합니다.
이 하이브리드 그룹은 TLS 1.3에만 있습니다. 네이티브 엔진은 TLS 1.2도 구사하며, 피어가 TLS 1.3을 할 수 없어 TLS 1.2로 대체되는 연결은 X25519, secp256r1, secp384r1 중 고전적인 그룹을 사용하므로 포스트 양자 보호가 없습니다. 키 교환이 항상 포스트 양자여야 한다면, 클라이언트의 TLSOptions.Version이나 서버의 SSLOptions.Version을 tls1_3으로 설정하고, NativeTLS_Options.Groups에 하이브리드 그룹만 나열하십시오.
포스트 양자 TLS 데모인 Demos\18.Cryptography\08.PostQuantum_TLS에는 TLS 1.3과 TLS 1.2, TLS 1.3 전용, TLS 1.2 전용이라는 선택지를 가진 TLS 버전 선택기와 암호 제품군 선택기가 있으며, 상태 표시줄에 협상된 버전, 그룹, 암호 제품군이 표시됩니다. 서버 데모는 ML-DSA-44 인증서로 서명할 수 있는데, TLS 1.2에는 ML-DSA 서명 방식이 없으므로 이 인증서는 TLS 1.3 클라이언트만 지원합니다.
type
TsgcTLSHybridGroup = (thgX25519MLKEM768, thgSecP256r1MLKEM768, thgSecP384r1MLKEM1024);
function sgcTLSHybrid_GroupID(aGroup: TsgcTLSHybridGroup): Word;
function sgcTLSHybrid_GroupName(aGroup: TsgcTLSHybridGroup): string;
function sgcTLSHybrid_Available(aGroup: TsgcTLSHybridGroup): Boolean;
procedure sgcTLSHybrid_ClientKeyShare(aGroup: TsgcTLSHybridGroup; out aKeyShare, aPrivateState: TBytes);
function sgcTLSHybrid_ServerKeyShare(aGroup: TsgcTLSHybridGroup; const aClientKeyShare: TBytes;
out aKeyShare, aSharedSecret: TBytes): Boolean;
function sgcTLSHybrid_ClientSharedSecret(aGroup: TsgcTLSHybridGroup;
const aPrivateState, aServerKeyShare: TBytes; out aSharedSecret: TBytes): Boolean;
클라이언트는 aPrivateState를 비밀로 유지하고, 이를 그대로 sgcTLSHybrid_ClientSharedSecret에 전달한 뒤 지워야 합니다. 그 내부 구조는 API의 일부가 아닙니다. 두 Boolean 함수는 피어가 보낸 값이 올바르지 않으면 모든 출력을 비운 채 False를 반환합니다. 길이가 잘못된 키 셰어, FIPS 203 검사를 통과하지 못하는 ML-KEM 캡슐화 키, 곡선 위에 있지 않은 점, 전부 0인 X25519 결과가 여기에 해당합니다. RFC 10024는 이 모든 경우에 illegal_parameter 경고와 함께 연결을 중단하도록 요구합니다.
X25519 위에 만들어진 모든 것, 즉 sgcHybrid_ 함수들과 X-Wing, 그리고 X25519MLKEM768 그룹은 SGC_CRYPTO_FIPS 빌드에서 컴파일에서 제외됩니다. FIPS 모드를 참조하십시오. 두 SecP 그룹은 그대로 남는데, P-256, P-384, ML-KEM 모두 승인된 알고리즘이기 때문입니다. 그룹이 있다고 가정하지 말고, 제안하거나 수락하기 전에 sgcTLSHybrid_Available을 호출하십시오.
ML-DSA (서명에 권장)
서명은 마스킹 벡터를 뽑아 서명을 만들려 시도하며, 시도가 키에 대한 정보를 유출할 것 같으면 다시 시작합니다. 예상 시도 횟수는 적지만 고정되어 있지 않으므로, 서명에 걸리는 시간이 일정하지 않습니다. 이는 이 방식에 내재된 특성이며 키에 대한 사이드 채널이 아닙니다. 서명은 큽니다: Ed25519의 64바이트에 비해 ML-DSA-44는 2420바이트이고, 공개 키도 32바이트에 비해 1312바이트이므로, 크기에 민감한 프로토콜에 도입하기 전에 이 차이를 감안해 여유를 두어야 합니다.
procedure sgcMLDSA_GenerateKeyPair(aParams: TsgcMLDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcMLDSA_Sign(aParams: TsgcMLDSAParams; const aPrivateKey, aMessage, aContext: TBytes): TBytes;
function sgcMLDSA_Verify(aParams: TsgcMLDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;
var
oPub, oPriv, oSignature, oEmpty: TBytes;
begin
sgcMLDSA_GenerateKeyPair(mldsa44, oPub, oPriv);
oSignature := sgcMLDSA_Sign(mldsa44, oPriv, oMessage, oEmpty);
if sgcMLDSA_Verify(mldsa44, oPub, oMessage, oSignature, oEmpty) then
{ valid };
end;
SLH-DSA (보수적인 대안)
SLH-DSA는 오직 해시 함수에만 의존합니다: 앞으로 어느 시점에도 깨질 격자 문제가 없습니다. 이 보장에는 대가가 따릅니다. 서명은 가장 작은 SLH-DSA-SHAKE-128s의 7856바이트에서 가장 큰 SLH-DSA-SHAKE-256f의 49856바이트까지 다양하며, s(작은) 매개변수 집합으로 서명하면 수십만 번의 해시 연산을 거치며 수십 초가 걸릴 수 있습니다. f(빠른) 집합은 서명 크기가 대략 두 배인 대신 약 1초에서 7초 사이에 서명이 끝납니다. 검증은 모든 매개변수 집합에서 빠르며, 이는 펌웨어나 코드 서명처럼 서명은 드물게 하고 검증은 자주 하는 용도에 대화형 프로토콜보다 더 적합합니다.
procedure sgcSLHDSA_GenerateKeyPair(aParams: TsgcSLHDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcSLHDSA_Sign(aParams: TsgcSLHDSAParams; const aPrivateKey, aMessage, aContext: TBytes;
aDeterministic: Boolean = True): TBytes;
function sgcSLHDSA_Verify(aParams: TsgcSLHDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;
키 형식: SubjectPublicKeyInfo, PKCS#8, PEM
FIPS 알고리즘이 다루는 원시 바이트 문자열은 저장 형식이 아니며, 다른 어느 구현도 이를 읽지 못합니다. 키를 디스크에 기록하거나, 인증서에 넣거나, 다른 라이브러리에 전달하려면 표준 컨테이너로 내보내십시오. 공개 키는 X.509 SubjectPublicKeyInfo, 개인 키는 PKCS#8 OneAsymmetricKey, 그리고 둘 중 어느 쪽이든 PEM 아몬(RFC 7468)으로 감싸며, 라벨은 PUBLIC KEY와 PRIVATE KEY입니다. 세 방식 모두 동일한 여덟 개의 함수를 제공하며, 각각 그 인코딩을 정의하는 RFC를 따릅니다.
| 방식 | 컨테이너 RFC | DER 함수 | PEM 함수 |
| ML-DSA | RFC 9881 | sgcMLDSA_ExportSubjectPublicKeyInfo, sgcMLDSA_ImportSubjectPublicKeyInfo, sgcMLDSA_ExportPrivateKeyInfo, sgcMLDSA_ImportPrivateKeyInfo | sgcMLDSA_ExportPublicKeyPEM, sgcMLDSA_ImportPublicKeyPEM, sgcMLDSA_ExportPrivateKeyPEM, sgcMLDSA_ImportPrivateKeyPEM |
| ML-KEM | RFC 9935 | sgcMLKEM_ExportSubjectPublicKeyInfo, sgcMLKEM_ImportSubjectPublicKeyInfo, sgcMLKEM_ExportPrivateKeyInfo, sgcMLKEM_ImportPrivateKeyInfo | sgcMLKEM_ExportPublicKeyPEM, sgcMLKEM_ImportPublicKeyPEM, sgcMLKEM_ExportPrivateKeyPEM, sgcMLKEM_ImportPrivateKeyPEM |
| SLH-DSA | RFC 9909 | sgcSLHDSA_ExportSubjectPublicKeyInfo, sgcSLHDSA_ImportSubjectPublicKeyInfo, sgcSLHDSA_ExportPrivateKeyInfo, sgcSLHDSA_ImportPrivateKeyInfo | sgcSLHDSA_ExportPublicKeyPEM, sgcSLHDSA_ImportPublicKeyPEM, sgcSLHDSA_ExportPrivateKeyPEM, sgcSLHDSA_ImportPrivateKeyPEM |
알고리즘 식별자는 매개변수가 없는 상태로 NIST 객체 식별자를 담고 있으므로, 매개변수 집합은 키와 함께 이동하고 가져오기할 때 다시 반환됩니다. sgcMLDSA_OID, sgcMLKEM_OID, sgcSLHDSA_OID가 점으로 구분된 문자열을 돌려주며, 이에 대응하는 ParamsFromOID 함수들은 그 반대로 매핑합니다.
ML-DSA와 ML-KEM의 세 가지 개인 키 형식.
내보내기 함수는 sgcCrypto_DER에 선언된 TsgcPQCPrivateKeyFormat 형식의 aFormat 매개변수를 받습니다. 기본값인 pqkfSeed는 시드만 기록하며, ML-DSA는 32바이트, ML-KEM은 64바이트인 d || z입니다. 이는 두 RFC가 모두 권장하는 형식이며 단연 가장 작습니다. pqkfExpanded는 확장된 FIPS 개인 키를 기록합니다. pqkfBoth는 둘 다 기록하며, 확장된 키가 시드를 확장한 결과가 아니면 오류를 발생시킵니다. 이는 두 RFC가 잘못된 키라고 부르는 경우입니다.
확장된 키에서 나중에 시드를 복구할 수는 없으므로, 권장 형식으로 저장할 생각이라면 키를 생성할 때 sgcMLDSA_GenerateKeyPairAndSeed나 sgcMLKEM_GenerateKeyPairAndSeed로 시드를 함께 받으십시오. SLH-DSA는 개인 키 형식이 SK.seed || SK.prf || PK.seed || PK.root 하나뿐이므로, 그 내보내기는 형식 매개변수를 받지 않습니다.
function sgcMLDSA_ExportPrivateKeyPEM(aParams: TsgcMLDSAParams; const aSeed, aPrivateKey: TBytes;
aFormat: TsgcPQCPrivateKeyFormat = pqkfSeed): string;
function sgcMLDSA_ImportPrivateKeyPEM(const aPEM: string; out aParams: TsgcMLDSAParams;
out aSeed, aPrivateKey, aPublicKey: TBytes): Boolean;
var
vParams: TsgcMLDSAParams;
oSeed, oPub, oPriv: TBytes;
vPublicPEM, vPrivatePEM: string;
begin
sgcMLDSA_GenerateKeyPairAndSeed(mldsa65, oSeed, oPub, oPriv);
vPublicPEM := sgcMLDSA_ExportPublicKeyPEM(mldsa65, oPub);
vPrivatePEM := sgcMLDSA_ExportPrivateKeyPEM(mldsa65, oSeed, oPriv, pqkfSeed);
{ reading it back: the parameter set comes from the key itself }
if sgcMLDSA_ImportPrivateKeyPEM(vPrivatePEM, vParams, oSeed, oPriv, oPub) then
{ vParams = mldsa65, and oPriv and oPub are the expansion of oSeed };
end;
가져오기는 검증하며, 그냥 신뢰하지 않습니다.
모든 가져오기 함수는 오류를 발생시키는 대신 False를 반환합니다. 잘못된 DER, 뒤에 남은 데이터, 알 수 없는 객체 식별자, 없어야 할 자리에 있는 매개변수, 그리고 길이가 매개변수 집합과 맞지 않는 키에서 모두 False를 반환합니다. 구조 외에도, ML-KEM 공개 키는 FIPS 203 계수 검사를 통과해야 하고, 확장된 ML-KEM 개인 키는 해시 검사와 쌍 일관성 검사를 통과해야 하며, 확장된 ML-DSA 개인 키는 공개 키로 다시 구성되어 저장된 t0과 tr이 둘 다 그와 일치해야 합니다. 개인 키와 일치하지 않는 내장 공개 키는 거부됩니다. 개인 키가 시드를 담고 있으면 세 출력이 모두 채워져 돌아오므로, 호출자가 따로 확장할 필요가 없습니다.
sgcSLHDSA_ImportPrivateKeyInfo와 sgcSLHDSA_ImportPrivateKeyPEM은 기본값이 True인 aVerify 매개변수를 추가로 받으며, 이 매개변수는 개인 시드에서 PK.root를 다시 계산하여 저장된 루트가 다른 키를 거부합니다. 이 재구성은 키 생성만큼 비용이 들며, s 매개변수 집합에서는 수 초가 걸립니다. 따라서 자체 애플리케이션이 생성했거나 이미 신뢰하는 키에만 False를 전달하십시오. False일 때도 객체 식별자와 길이, 구조는 여전히 검사됩니다.
이들은 라이브러리의 나머지 부분이 읽는 컨테이너입니다. PKCS#8 ML-DSA 개인 키는 JWT 클라이언트가 서명에 사용하는 것이고, SubjectPublicKeyInfo는 JWT 서버가 검증에 사용하는 것이며, 둘 다 인증서 및 PKI가 인증서나 인증서 서명 요청에 넣는 것입니다.
매개변수 집합 선택
모든 크기 조회 함수(sgcMLKEM_PublicKeySize, sgcMLDSA_SignatureSize, sgcSLHDSA_SignatureSize와 이에 대응하는 PrivateKeySize)는 연산 자체와 동일한 매개변수 열거형을 받으므로, 프로토콜은 어떤 암호화 연산도 수행하기 전에 버퍼 크기를 정할 수 있습니다:
| 매개변수 집합 | 공개 키 | 개인 키 | 암호문 / 서명 |
| ML-KEM-512 | 800 | 1632 | 768 |
| ML-KEM-768 | 1184 | 2400 | 1088 |
| ML-KEM-1024 | 1568 | 3168 | 1568 |
| ML-DSA-44 | 1312 | 2560 | 2420 |
| ML-DSA-65 | 1952 | 4032 | 3309 |
| ML-DSA-87 | 2592 | 4896 | 4627 |
| SLH-DSA-SHAKE-128s | 32 | 64 | 7856 |
| SLH-DSA-SHAKE-128f | 32 | 64 | 17088 |
| SLH-DSA-SHAKE-192s | 48 | 96 | 16224 |
| SLH-DSA-SHAKE-192f | 48 | 96 | 35664 |
| SLH-DSA-SHAKE-256s | 64 | 128 | 29792 |
| SLH-DSA-SHAKE-256f | 64 | 128 | 49856 |
모든 크기는 바이트 단위입니다. ML-KEM 공유 비밀은 매개변수 집합과 관계없이 항상 32바이트입니다(sgcMLKEM_SharedSecretSize).