비고
일부 정보는 상용 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적이거나 묵시적인 보증도 하지 않습니다. 이 항목에 설명된 기능은 Windows Insider Preview시험판 버전에서 사용할 수 있습니다.
이 문서에서는 Microsoft의 CNG API를 사용하여 모듈Lattice-Based 키 캡슐화 메커니즘(ML-KEM) 알고리즘을 사용하여 키 교환을 수행하기 위한 엔드투엔드 워크플로를 구현하는 가이드를 제공합니다.
BCrypt를 사용하는 샘플 ML-KEM 키 캡슐화 및 탈캡슐화 코드
ML-KEM 키 교환에 사용되는 양자 후 알고리즘이며 FIPS 203에서 표준화됩니다. 키 교환은 TLS와 같은 보안 프로토콜의 중요한 부분으로, 클라이언트와 서버가 연결을 협상하고 키 자료를 만들고 공유하여 인터넷을 통해 전송된 메시지를 암호화하고 암호 해독합니다. KEM에서 키 쌍 생성 프로세스에서 생성된 키를 캡슐화 키와 디캡슐화 키라고 합니다. 캡슐화 키는 공개 키이며 누구나 비밀 키(이 작업을 수행하는 당사자용)와 암호 텍스트를 생성하는 키 캡슐화 작업을 수행하는 데 사용할 수 있습니다. 암호 텍스트는 키 캡슐화 프로세스 중에 캡슐화 당사자가 얻은 것과 동일한 공유 비밀 키를 복구하기 위해 프라이빗 키 소유자가 키 캡슐화 작업에 대한 입력으로 제공됩니다. 이 예제에서는 가상 TLS 클라이언트 및 서버 애플리케이션이 BCrypt의 새 ML-KEM API를 사용하여 키 교환을 수행하는 방법을 보여 줍니다.
설정 및 키 쌍 생성
다음 단계에서는 ML-KEM 키 쌍 생성 및 캡슐화를 설정하는 프로세스를 간략하게 설명합니다.
BCRYPT_MLKEM_ALG_HANDLE BCryptGenerateKeyPair를 사용하여 키 캡슐화에 대한 새 키 쌍을 만듭니다. 키 길이는 ML-KEM 매개 변수 집합에 의해 정의되므로 길이와 플래그 필드는 둘 다
0입니다.// Generate the key pair for key exchange unique_bcrypt_key hKeyPair; THROW_IF_NTSTATUS_FAILED( BCryptGenerateKeyPair( BCRYPT_MLKEM_ALG_HANDLE, &hKeyPair, 0, // dwLength 0)); // dwFlags키 쌍에서 BCryptSetProperty를 호출하여 BCRYPT_PARAMETER_SET_NAME을 BCRYPT_MLKEM_PARAMETER_SET_768로 설정하고, ML-KEM 작업에 필요한 매개 변수 집합을 지정합니다. ML-KEM NIST에서 정의한 512 및 1024 매개 변수 집합도 지원합니다.
THROW_IF_NTSTATUS_FAILED( BCryptSetProperty( &hKeyPair, BCRYPT_PARAMETER_SET_NAME, (PUCHAR) BCRYPT_MLKEM_PARAMETER_SET_768, sizeof(BCRYPT_MLKEM_PARAMETER_SET_768), 0)); // dwFlagsBCryptFinalizeKeyPair를 호출하여 후속 작업에서 키 쌍을 사용할 준비가 되도록 합니다.
THROW_IF_NTSTATUS_FAILED( BCryptFinalizeKeyPair( hKeyPair.get(), 0)); // dwFlags
공개 키 내보내기 및 Exchange
출력 버퍼를 사용하여 BCryptExportKey 를
NULL호출하여 BCRYPT_MLKEM_ENCAPSULATION_BLOB 내보내는 데 필요한 크기를 쿼리합니다.ULONG cbEncapsulationKeyBlob = 0; THROW_IF_NTSTATUS_FAILED( BCryptExportKey( hKeyPair.get(), NULL, BCRYPT_MLKEM_ENCAPSULATION_BLOB, NULL, // pbOutput 0, // cbOutput &cbEncapsulationKeyBlob, 0)); // dwFlags이전에 검색한 크기에 따라 버퍼를 할당한 다음 BCryptExportKey를 사용하여 캡슐화(공개) 키를 내보냅니다. 이 Blob은 키 교환 파트너(예: 클라이언트 서버 시나리오의 서버)로 전송됩니다.
vector<BYTE> encapsulationKeyBlob(cbEncapsulationKeyBlob); THROW_IF_NTSTATUS_FAILED( BCryptExportKey( hKeyPair.get(), NULL, BCRYPT_MLKEM_ENCAPSULATION_BLOB, encapsulationKeyBlob.data(), static_cast<ULONG>(encapsulationKeyBlob.size()), &cbEncapsulationKeyBlob, 0));BCRYPT_MLKEM_KEY_BLOB에 정확한 public magic와 768 매개 변수 집합이 있는지 확인합니다.
BCRYPT_MLKEM_KEY_BLOB* pEncapsulationKeyBlob = reinterpret_cast<BCRYPT_MLKEM_KEY_BLOB *>(encapsulationKeyBlob.data()); ASSERT(pEncapsulationKeyBlob->dwMagic == BCRYPT_MLKEM_PUBLIC_MAGIC); ASSERT(pEncapsulationKeyBlob->cbParameterSet == sizeof(BCRYPT_MLKEM_PARAMETER_SET_768)); if (wcscmp(BCRYPT_MLKEM_PARAMETER_SET_768, reinterpret_cast<WCHAR *>(pEncapsulationKeyBlob + 1)) != 0) { return; }클라이언트 키 교환 메시지의 서버에 캡슐화 키를 보냅니다.
PBYTE pbEncapsulationKey = reinterpret_cast<PBYTE>(pEncapsulationKeyBlob) + sizeof(BCRYPT_MLKEM_KEY_BLOB) + sizeof(BCRYPT_MLKEM_PARAMETER_SET_768); ULONG cbEncapsulationKey = pEncapsulationKeyBlob->cbKey; SendToServer(pbEncapsulationKey, cbEncapsulationKey);
캡슐화 및 디캡슐화
다음 단계는 공유 비밀 키를 캡슐화하고 디캡슐화하는 프로세스를 개요를 설명합니다.
서버는 클라이언트의 키 교환 메시지를 수신하고 캡슐화 키의 바이트를 검색합니다.
// Server receives the client's key_exchange message and retrieves the // encapsulation key bytes. vector<BYTE> encapsulationKey = GetClientKeyExchange(); ULONG cbEncapsulationKey = static_cast<ULONG>(encapsulationKey.size());서버는 768 매개 변수 집합 및 퍼블릭 매직을 사용하여 키를 BCRYPT_KEY_BLOB 배치하고 캡슐화 키를 가져옵니다.
// Put the Key in a BCRYPT_KEY_BLOB and import it. ULONG cbEncapsulationKeyBlob = sizeof(BCRYPT_MLKEM_KEY_BLOB) + sizeof(BCRYPT_MLKEM_PARAMETER_SET_768) + cbEncapsulationKey; vector<BYTE> encapsulationKeyBlob(cbEncapsulationKeyBlob); BCRYPT_MLKEM_KEY_BLOB* pEncapsulationKeyBlob = reinterpret_cast<BCRYPT_MLKEM_KEY_BLOB *>(encapsulationKeyBlob.data()); pEncapsulationKeyBlob->dwMagic = BCRYPT_MLKEM_PUBLIC_MAGIC; pEncapsulationKeyBlob->cbParameterSet = sizeof(BCRYPT_MLKEM_PARAMETER_SET_768); pEncapsulationKeyBlob->cbKey = cbEncapsulationKey; CopyMemory( reinterpret_cast<PBYTE>(pEncapsulationKeyBlob) + sizeof(BCRYPT_MLKEM_KEY_BLOB), BCRYPT_MLKEM_PARAMETER_SET_768, sizeof(BCRYPT_MLKEM_PARAMETER_SET_768)); CopyMemory( reinterpret_cast<PBYTE>(pEncapsulationKeyBlob) + sizeof(BCRYPT_MLKEM_KEY_BLOB) + sizeof(BCRYPT_MLKEM_PARAMETER_SET_768), encapsulationKey.data(), encapsulationKey.size()); unique_bcrypt_key hKeyPair; // The server knows the ML-KEM parameter set from the client's // key_exchange, which denotes the parameter set associated with the // encapsulation key it sent. In this case, we know it's // BCRYPT_MLKEM_PARAMETER_SET_768. THROW_IF_NTSTATUS_FAILED( BCryptImportKeyPair( BCRYPT_MLKEM_ALG_HANDLE, NULL, // hImportKey BCRYPT_MLKEM_ENCAPSULATION_BLOB, &hKeyPair, encapsulationKeyBlob.data(), static_cast<ULONG>(encapsulationKeyBlob.size()), 0)); // dwFlags // Get the secret key length and ciphertext length. These values are static // and can be cached for the algorithm handle. ULONG cbSecretKey = 0; ULONG cbProperty = sizeof(cbSecretKey);서버는 BCryptGetProperty 를 사용하여 비밀 키 길이와 암호 텍스트 길이를 얻고 두 가지 모두에 필요한 버퍼를 할당합니다.
THROW_IF_NTSTATUS_FAILED( BCryptGetProperty( &hKeyPair, BCRYPT_KEM_SHARED_SECRET_LENGTH, reinterpret_cast<PUCHAR>(&cbSecretKey), cbProperty, &cbProperty, 0)); // dwFlags ULONG cbCipherText = 0; cbProperty = sizeof(cbCipherText); THROW_IF_NTSTATUS_FAILED( BCryptGetProperty( &hKeyPair, BCRYPT_KEM_CIPHERTEXT_LENGTH, reinterpret_cast<PUCHAR>(&cbCipherText), cbProperty, &cbProperty, 0)); // dwFlags // Then allocate the required buffers. vector<BYTE> secretKey(cbSecretKey); vector<BYTE> cipherText(cbCipherText);서버는 BCryptEncapsulate 를 수행하고 서버 키 교환 메시지의 클라이언트에 암호 텍스트를 보냅니다.
// Perform the encapsulate operation. THROW_IF_NTSTATUS_FAILED( BCryptEncapsulate( hKeyPair.get(), secretKey.data(), static_cast<ULONG>(secretKey.size()), &cbSecretKey, cipherText.data(), static_cast<ULONG>(cipherText.size()), &cbCipherText, 0)); // dwFlags // cipherText is sent to the client in the server's key_exchange message. SendToClient(cipherText.data(), cipherText.size());클라이언트는 수신된 서버 키 교환을 사용하여 공유 비밀을 캡슐화하는 암호 텍스트를 생성합니다.
// pbEncapsulationKey is sent on the wire in the client's key_exchange // message. // ... // < It's now the server's turn. It will use the encapsulation key to // generate the a CipherText encapsulating the shared secret key and send // it as a response to the client's key_exchange message. Sample_Server() // demonstrates how a hypothetical server may do this.> // ... // When the ServerKeyExchange message is received from the TLS server, // get the ML-KEM CipherText from the ServerKeyExchange message. vector<BYTE> cipherText = GetServerKeyExchange();클라이언트는 BCryptGetProperty 를 호출하여 비밀 키 길이를 가져와 적절한 버퍼를 할당합니다.
// Get the secret key length. This value is static and can be cached for // the algorithm handle. ULONG cbSecretKey = 0; ULONG cbProperty = sizeof(cbSecretKey); THROW_IF_NTSTATUS_FAILED( BCryptGetProperty( &hKeyPair, BCRYPT_KEM_SHARED_SECRET_LENGTH, reinterpret_cast<PUCHAR>(&cbSecretKey), cbProperty, &cbProperty, 0)); // dwFlags클라이언트는 키 Blob을 만들고 암호화 텍스트 및 비밀 길이로 BCryptDecapsulate 를 호출하여 공유 비밀 키를 생성합니다.
vector<BYTE> secretKey(cbSecretKey); THROW_IF_NTSTATUS_FAILED( BCryptDecapsulate( hKeyPair.get(), cipherText.data(), static_cast<ULONG>(cipherText.size()), secretKey.data(), static_cast<ULONG>(secretKey.size()), &cbSecretKey, 0)); // dwFlags
세션 키 파생하기
이제 클라이언트와 서버 모두 동일한 공유 비밀을 갖게 되며, 이는 보안 통신을 위한 세션 키를 생성하기 위해 DeriveSessionKeys 와 같은 키 파생 함수에 전달될 수 있습니다.
// secretKey contains the shared secret key which plugs into the TLS key
// schedule.
DeriveSessionKeys(secretKey);
전체 코드 샘플 검토
아래의 전체 코드 샘플을 검토할 수 있습니다.
void Sample_Client()
{
// Generate the key pair for key exchange
unique_bcrypt_key hKeyPair;
THROW_IF_NTSTATUS_FAILED(
BCryptGenerateKeyPair(
BCRYPT_MLKEM_ALG_HANDLE,
&hKeyPair,
0, // dwLength
0)); // dwFlags
THROW_IF_NTSTATUS_FAILED(
BCryptSetProperty(
&hKeyPair,
BCRYPT_PARAMETER_SET_NAME,
(PUCHAR) BCRYPT_MLKEM_PARAMETER_SET_768,
sizeof(BCRYPT_MLKEM_PARAMETER_SET_768),
0)); // dwFlags
THROW_IF_NTSTATUS_FAILED(
BCryptFinalizeKeyPair(
hKeyPair.get(),
0)); // dwFlags
ULONG cbEncapsulationKeyBlob = 0;
THROW_IF_NTSTATUS_FAILED(
BCryptExportKey(
hKeyPair.get(),
NULL,
BCRYPT_MLKEM_ENCAPSULATION_BLOB,
NULL, // pbOutput
0, // cbOutput
&cbEncapsulationKeyBlob,
0)); // dwFlags
vector<BYTE> encapsulationKeyBlob(cbEncapsulationKeyBlob);
THROW_IF_NTSTATUS_FAILED(
BCryptExportKey(
hKeyPair.get(),
NULL,
BCRYPT_MLKEM_ENCAPSULATION_BLOB,
encapsulationKeyBlob.data(),
static_cast<ULONG>(encapsulationKeyBlob.size()),
&cbEncapsulationKeyBlob,
0));
BCRYPT_MLKEM_KEY_BLOB* pEncapsulationKeyBlob =
reinterpret_cast<BCRYPT_MLKEM_KEY_BLOB *>(encapsulationKeyBlob.data());
ASSERT(pEncapsulationKeyBlob->dwMagic == BCRYPT_MLKEM_PUBLIC_MAGIC);
ASSERT(pEncapsulationKeyBlob->cbParameterSet == sizeof(BCRYPT_MLKEM_PARAMETER_SET_768));
if (wcscmp(BCRYPT_MLKEM_PARAMETER_SET_768, reinterpret_cast<WCHAR *>(pEncapsulationKeyBlob + 1)) != 0)
{
return;
}
PBYTE pbEncapsulationKey = reinterpret_cast<PBYTE>(pEncapsulationKeyBlob) + sizeof(BCRYPT_MLKEM_KEY_BLOB) + sizeof(BCRYPT_MLKEM_PARAMETER_SET_768);
ULONG cbEncapsulationKey = pEncapsulationKeyBlob->cbKey;
SendToServer(pbEncapsulationKey, cbEncapsulationKey);
// pbEncapsulationKey is sent on the wire in the client's key_exchange
// message.
// ...
// < It's now the server's turn. It will use the encapsulation key to
// generate the a CipherText encapsulating the shared secret key and send
// it as a response to the client's key_exchange message. Sample_Server()
// demonstrates how a hypothetical server may do this.>
// ...
// When the ServerKeyExchange message is received from the TLS server,
// get the ML-KEM CipherText from the ServerKeyExchange message.
vector<BYTE> cipherText = GetServerKeyExchange();
// Get the secret key length. This value is static and can be cached for
// the algorithm handle.
ULONG cbSecretKey = 0;
ULONG cbProperty = sizeof(cbSecretKey);
THROW_IF_NTSTATUS_FAILED(
BCryptGetProperty(
&hKeyPair,
BCRYPT_KEM_SHARED_SECRET_LENGTH,
reinterpret_cast<PUCHAR>(&cbSecretKey),
cbProperty,
&cbProperty,
0)); // dwFlags
vector<BYTE> secretKey(cbSecretKey);
THROW_IF_NTSTATUS_FAILED(
BCryptDecapsulate(
hKeyPair.get(),
cipherText.data(),
static_cast<ULONG>(cipherText.size()),
secretKey.data(),
static_cast<ULONG>(secretKey.size()),
&cbSecretKey,
0)); // dwFlags
// secretKey is the shared secret key which plugs into the TLS key
// schedule.
DeriveSessionKeys(secretKey);
}
void Sample_Server()
{
// Server receives the client's key_exchange message and retrieves the
// encapsulation key bytes.
vector<BYTE> encapsulationKey = GetClientKeyExchange();
ULONG cbEncapsulationKey = static_cast<ULONG>(encapsulationKey.size());
// Put the Key in a BCRYPT_KEY_BLOB and import it.
ULONG cbEncapsulationKeyBlob = sizeof(BCRYPT_MLKEM_KEY_BLOB) + sizeof(BCRYPT_MLKEM_PARAMETER_SET_768) + cbEncapsulationKey;
vector<BYTE> encapsulationKeyBlob(cbEncapsulationKeyBlob);
BCRYPT_MLKEM_KEY_BLOB* pEncapsulationKeyBlob =
reinterpret_cast<BCRYPT_MLKEM_KEY_BLOB *>(encapsulationKeyBlob.data());
pEncapsulationKeyBlob->dwMagic = BCRYPT_MLKEM_PUBLIC_MAGIC;
pEncapsulationKeyBlob->cbParameterSet = sizeof(BCRYPT_MLKEM_PARAMETER_SET_768);
pEncapsulationKeyBlob->cbKey = cbEncapsulationKey;
CopyMemory(
reinterpret_cast<PBYTE>(pEncapsulationKeyBlob) + sizeof(BCRYPT_MLKEM_KEY_BLOB),
BCRYPT_MLKEM_PARAMETER_SET_768,
sizeof(BCRYPT_MLKEM_PARAMETER_SET_768));
CopyMemory(
reinterpret_cast<PBYTE>(pEncapsulationKeyBlob) + sizeof(BCRYPT_MLKEM_KEY_BLOB) + sizeof(BCRYPT_MLKEM_PARAMETER_SET_768),
encapsulationKey.data(),
encapsulationKey.size());
unique_bcrypt_key hKeyPair;
// The server knows the ML-KEM parameter set from the client's
// key_exchange, which denotes the parameter set associated with the
// encapsulation key it sent. In this case, we know it's
// BCRYPT_MLKEM_PARAMETER_SET_768.
THROW_IF_NTSTATUS_FAILED(
BCryptImportKeyPair(
BCRYPT_MLKEM_ALG_HANDLE,
NULL, // hImportKey
BCRYPT_MLKEM_ENCAPSULATION_BLOB,
&hKeyPair,
encapsulationKeyBlob.data(),
static_cast<ULONG>(encapsulationKeyBlob.size()),
0)); // dwFlags
// Get the secret key length and ciphertext length. These values are static
// and can be cached for the algorithm handle.
ULONG cbSecretKey = 0;
ULONG cbProperty = sizeof(cbSecretKey);
THROW_IF_NTSTATUS_FAILED(
BCryptGetProperty(
&hKeyPair,
BCRYPT_KEM_SHARED_SECRET_LENGTH,
reinterpret_cast<PUCHAR>(&cbSecretKey),
cbProperty,
&cbProperty,
0)); // dwFlags
ULONG cbCipherText = 0;
cbProperty = sizeof(cbCipherText);
THROW_IF_NTSTATUS_FAILED(
BCryptGetProperty(
&hKeyPair,
BCRYPT_KEM_CIPHERTEXT_LENGTH,
reinterpret_cast<PUCHAR>(&cbCipherText),
cbProperty,
&cbProperty,
0)); // dwFlags
// Then allocate the required buffers.
vector<BYTE> secretKey(cbSecretKey);
vector<BYTE> cipherText(cbCipherText);
// Perform the encapsulate operation.
THROW_IF_NTSTATUS_FAILED(
BCryptEncapsulate(
hKeyPair.get(),
secretKey.data(),
static_cast<ULONG>(secretKey.size()),
&cbSecretKey,
cipherText.data(),
static_cast<ULONG>(cipherText.size()),
&cbCipherText,
0)); // dwFlags
// cipherText is sent to the client in the server's key_exchange message.
SendToClient(cipherText.data(), cipherText.size());
// secretKey contains the shared secret key which plugs into the TLS key
// schedule.
DeriveSessionKeys(secretKey);
}