Compartir a través de


Uso de ML-DSA con CNG

Nota:

Cierta información se relaciona con un producto de versión preliminar que puede modificarse sustancialmente antes de su publicación comercial. Microsoft no ofrece ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí. La característica descrita en este tema está disponible en versiones preliminares de Windows Insider Preview.

En este artículo se proporciona una guía para implementar el flujo de trabajo de un extremo a otro para crear y comprobar firmas digitales mediante el algoritmo de ML-DSA con la API de CNG de Microsoft.

Código de ejemplo ML-DSA usando BCrypt

Aprenda a usar el algoritmo de ML-DSA con la API de CNG de Microsoft para firmas digitales. El ejemplo BCrypt incluye funciones para firmar un mensaje y comprobar la firma, así como exportar la clave pública. El código está diseñado para ser fácil de seguir y comprender, lo que hace que sea adecuado para los desarrolladores que buscan implementar firmas digitales posteriores a la cuántica en sus aplicaciones.

Generación y comprobación de firmas con ML-DSA

En este ejemplo de código se muestra el flujo de trabajo de un extremo a otro para crear y comprobar firmas digitales mediante el algoritmo de ML-DSA con la API de CNG de Microsoft. Resalta la importancia de la coincidencia de contexto durante la firma y comprobación, así como una administración cuidadosa de los identificadores criptográficos y la memoria. Los algoritmos de firma digital posteriores a la cuántica son equivalentes en un alto nivel a los algoritmos de firma digital existentes que usamos hoy (RSA-PSS, ECDSA, etc.), donde una entidad genera un par de claves pública o privada y firma un mensaje (o su valor hash) con la clave privada para generar una firma. Cualquier persona que tenga la clave pública asociada puede comprobar la firma. Una diferencia es que los algoritmos de firma digital PQ admiten variantes de hash previos, que luego firman datos similares a los algoritmos de firma tradicionales, así como variantes puras, que firman los datos de entrada de longitud arbitraria. En este ejemplo se usa la variante de ML-DSA pura y se describe cómo usar ML-DSA de hash previo. Siguiendo estos pasos, los desarrolladores pueden implementar firmas digitales seguras y posteriores a la cuántica en sus aplicaciones.

Configuración de manejadores de algoritmos y pares de claves

Siga estos pasos para configurar los identificadores de algoritmo y los pares clave para ML-DSA:

  1. Use BCryptOpenAlgorithmProvider para el algoritmo de ML-DSA, eligiendo el proveedor primitivo predeterminado de Microsoft u otro proveedor HSM. Este paso configura el contexto criptográfico para las operaciones posteriores.

    status = BCryptOpenAlgorithmProvider(
        &hAlg,
        BCRYPT_MLDSA_ALGORITHM,
        MS_PRIMITIVE_PROVIDER,
        NULL);
    
    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }
    
  2. Use BCryptGenerateKeyPair para crear claves públicas y privadas para el algoritmo elegido.

    status = BCryptGenerateKeyPair(hAlg, &hKeyPair, 0, NULL);
    
    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }
    

    Los parámetros son los siguientes:

    • hAlg: identificador del proveedor de algoritmos, que se obtuvo de BCryptOpenAlgorithmProvider.
    • hKeyPair: identificador del par de claves.
    • 0: indica el tamaño de clave predeterminado para ML-DSA.
    • NULL: no se establecen banderas para esta operación.
  3. Use BCryptSetProperty para especificar el conjunto de parámetros a utilizar para ML-DSA, los cuales tienen compromisos entre resistencia y rendimiento. En este ejemplo se elige el conjunto de parámetros ML-DSA-44.

    status = BCryptSetProperty(&hKeyPair, BCRYPT_PARAMETER_SET_NAME, (PUCHAR)BCRYPT_MLDSA_PARAMETER_SET_44, sizeof(BCRYPT_MLDSA_PARAMETER_SET_44), 0); 
    
    if (!NT_SUCCESS(status)) { 
        goto cleanup; 
    }
    

    Establecer BCRYPT_PARAMETER_SET_NAME indica qué parámetro se va a usar (por ejemplo, ML-DSA-44).

  4. Use BCryptFinalizeKeyPair para que la clave pública y privada estén listas para usarse.

    status = BCryptFinalizeKeyPair(hKeyPair, NULL); 
    if (!NT_SUCCESS(status)) { 
        goto cleanup; 
    }
    
    // Public/Private key pair is generated at this point 
    

Firmar el mensaje

Para firmar un mensaje con el par de claves generado, el código usa BCryptSignHash.

  1. El código determina primero el tamaño del búfer necesario para la firma llamando a BCryptSignHash con NULL salida. A continuación, asigna memoria para la firma.

     // Get the signature size
    status = BCryptSignHash(hKeyPair, NULL, NULL, 0, NULL, 0, &cbSignature, NULL);
    
    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }
    
    pbSignature = (PBYTE)LocalAlloc(LMEM_FIXED, cbSignature);
    
    if (pbSignature == NULL) {
    status = STATUS_NO_MEMORY;
        goto cleanup;
    }
    
  2. A continuación, configura una estructura de BCRYPT_PQDSA_PADDING_INFO :

    // Sign with pure ML-DSA
    //
    // Pure variants sign arbitrary length messages whereas
    // the pre-hash variants sign a hash value of the input.
    // To sign with pure ML-DSA or pure SLH-DSA, pszPrehashAlgId must be set
    // to NULL if BCRYPT_PQDSA_PADDING_INFO is provided to the sign/verify
    // functions.
    
    // For pre-hash signing, the hash algorithm used in creating
    // the hash of the input must be specified using the pszPrehashAlgId
    // field of BCRYPT_PQDSA_PADDING_INFO. This hash algorithm information
    // is required in generating and verifying the signature as the OID of
    // the hash algorithm becomes a prefix of the input to be signed.
    //
    
    padinfo.pbCtx = (PUCHAR)ctx;
    padinfo.cbCtx = sizeof(ctx);
    padinfo.pszPrehashAlgId = NULL;
    

    La estructura BCRYPT_PQDSA_PADDING_INFO contiene los campos siguientes:

    • pbCtx y cbCtx: puntero y tamaño para una cadena de contexto opcional (datos autenticados adicionales).
    • pszPrehashAlgId: se establece en NULL para la firma "pura" (el mensaje se firma directamente, no un hash).
  3. Por último, la firma real se genera con otra llamada a BCryptSignHash. El identificador de clave, la información de relleno, el mensaje y el búfer de firmas asignados se pasan a la llamada. La bandera BCRYPT_PAD_PQDSA especifica el relleno PQDSA.

    status = BCryptSignHash(
        hKeyPair,
        &padinfo,
        (PUCHAR)msg,
        sizeof(msg),
        pbSignature,
        cbSignature,
        &cbWritten,
        BCRYPT_PAD_PQDSA); 
    
    if (!NT_SUCCESS(status)) {
        goto cleanup;
    } 
    

Exportación de la clave pública

En esta sección, se exporta la clave pública para que otros usuarios puedan comprobar las firmas.

  1. BCryptExportKey se llama primero con NULL parámetros y el formato BCRYPT_PQDSA_PUBLIC_BLOB para obtener el tamaño de búfer necesario:

    // Get the public key blob size
    status = BCryptExportKey(
        hKeyPair,
        NULL,
        BCRYPT_PQDSA_PUBLIC_BLOB,
        NULL,
        0,
        &cbPublicKeyBlob,
        NULL);
    
    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }
    
  2. El búfer se asigna y se llama a BCryptExportKey de nuevo para exportar la clave pública:

    pbPublicKeyBlob = (PBYTE)LocalAlloc(LMEM_FIXED, cbPublicKeyBlob);
    
    if (pbPublicKeyBlob == NULL) {
    status = STATUS_NO_MEMORY;
        goto cleanup;
    }
    
    // Export the public key
    status = BCryptExportKey(
        hKeyPair,
        NULL,
        BCRYPT_PQDSA_PUBLIC_BLOB,
        pbPublicKeyBlob,
        cbPublicKeyBlob,
        &cbWritten,
        NULL);
    
    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }
    

La clave exportada tiene el formato BCRYPT_PQDSA_PUBLIC_BLOB .

Limpieza de recursos

En el paso de limpieza, se liberan recursos asignados como búferes y manejadores, lo que garantiza que no haya pérdidas de memoria ni manejadores colgantes.

cleanup: 

if (pbPublicKeyBlob)
{
    LocalFree(pbPublicKeyBlob);
}

if (pbSignature)
{
    LocalFree(pbSignature);
} 

if (hKeyPair != NULL)
{
    BCryptDestroyKey(hKeyPair);
}

if (hAlg != NULL)
{
    BCryptCloseAlgorithmProvider(hAlg, NULL);
}

Comprobación de la firma

Para comprobar una firma, el código usa BCryptVerifySignature.

  1. La función MLDSAVerifySample del código de ejemplo siguiente recibe el blob de clave pública exportado, el mensaje, el contexto y la firma.
  2. BCryptImportKeyPair importa el blob de clave pública para crear un manejador de clave que pueda usar la API.
  3. De nuevo, BCRYPT_PQDSA_PADDING_INFO se prepara con el contexto (debe coincidir con el usado durante la firma). Se establecen los siguientes campos:
    • pbCtx/cbCtx (Contexto): se usa para proporcionar datos adicionales, lo que puede ayudar a enlazar firmas a una aplicación específica o un caso de uso. Tanto la firma como la comprobación deben usar el mismo contexto.
    • pszPrehashAlgId: se establece en NULL para "puro" ML-DSA. Para las variantes anteriores al hash, especifica el identificador del algoritmo hash.
    • BCRYPT_PAD_PQDSA: especifica el uso del relleno PQDSA para la seguridad post cuántica.
  4. Se llama a BCryptVerifySignature con el identificador de clave, la información de relleno, el mensaje original y la firma. Si la función devuelve éxito, la firma es válida; de lo contrario, no lo es.

Los pasos anteriores se anotan en el ejemplo de código siguiente:

//
// This function takes as input an ML-DSA-44 public key blob,
// and a signature generated by the same algorithm along with
// the message the signature belongs to. It verifies whether the
// signature is valid or not.
//
// STEP 1: Receive the public key blob, message, context, and signature
NTSTATUS MLDSAVerifySample(
    PCBYTE pbPublicKeyBlob,
    ULONG cbPublicKeyBlob,
    PCBYTE pbMsg,
    ULONG cbMsg,
    PCBYTE pbContext,
    ULONG cbContext,
    PCBYTE pbSignature,
    ULONG cbSignature)
{
    NTSTATUS status = STATUS_SUCCESS;
    BCRYPT_ALG_HANDLE hAlg = NULL;
    BCRYPT_KEY_HANDLE hKey = NULL;
    BCRYPT_PQDSA_PADDING_INFO padinfo = { 0 };

    status = BCryptOpenAlgorithmProvider(
        &hAlg,
        BCRYPT_MLDSA_ALGORITHM,
        MS_PRIMITIVE_PROVIDER,
        NULL);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    // STEP 2: Import the public key
    // Import the public key
    status = BCryptImportKeyPair(
        hAlg,
        NULL,
        BCRYPT_PQDSA_PUBLIC_BLOB,
        &hKey,
        (PUCHAR)pbPublicKeyBlob,
        cbPublicKeyBlob,
        NULL);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    // STEP 3: Prepare the padding info
    // Verify the signature
    // Assuming the signature is generated with pure ML-DSA.
    // Otherwise pszPrehashAlgId must be set to the identifier
    // of the hash algorithm used in pre-hashing the input.
    padinfo.pbCtx = (PUCHAR)pbContext;
    padinfo.cbCtx = cbContext;
    padinfo.pszPrehashAlgId = NULL;

    // STEP 4: Verify the signature
    status = BCryptVerifySignature(
        hKey,
        &padinfo,
        (PUCHAR)pbMsg,      // pbHash
        cbMsg,              // cbHash
        (PUCHAR)pbSignature,
        cbSignature,
        BCRYPT_PAD_PQDSA);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

cleanup:

    if (hKey != NULL)
    {
        BCryptDestroyKey(hKey);
    }

    if (hAlg != NULL)
    {
        BCryptCloseAlgorithmProvider(hAlg, NULL);
    }

    return status;
}

En el cleanup: bloque, el código usa BCryptDestroyKey para eliminar el identificador de clave de la memoria y BCryptCloseAlgorithmProvider para eliminar el identificador de algoritmo de la memoria.

Revisión del ejemplo de código completo

En el ejemplo de código siguiente se muestra el proceso completo de generación y comprobación de una firma digital mediante el algoritmo de ML-DSA con la API de CNG de Microsoft:

//
// Sample ML-DSA code
//
// This function creates an ML-DSA-44 private key, signs
// a message with it, and exports the public-key. The receiver
// can use the public key, message, and the signature to verify
// that they are generated by the same party who owns the private key.
//
#define SAMPLE_MESSAGE "message"
#define SAMPLE_CONTEXT "context"

NTSTATUS MLDSASignSample()
{
    NTSTATUS status = STATUS_SUCCESS;
    BCRYPT_ALG_HANDLE hAlg = NULL;
    BCRYPT_KEY_HANDLE hKeyPair = NULL;
    BCRYPT_PQDSA_PADDING_INFO padinfo = { 0 };
    PBYTE pbSignature = NULL;
    ULONG cbSignature, cbWritten;
    const BYTE msg[] = SAMPLE_MESSAGE;
    const BYTE ctx[] = SAMPLE_CONTEXT;
    PBYTE pbPublicKeyBlob = NULL;
    ULONG cbPublicKeyBlob;

    status = BCryptOpenAlgorithmProvider(
        &hAlg,
        BCRYPT_MLDSA_ALGORITHM,
        MS_PRIMITIVE_PROVIDER,
        NULL);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    status = BCryptGenerateKeyPair(hAlg, &hKeyPair, 0, NULL);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    status = BCryptSetProperty(&hKeyPair, BCRYPT_PARAMETER_SET_NAME, (PUCHAR)BCRYPT_MLDSA_PARAMETER_SET_44, sizeof(BCRYPT_MLDSA_PARAMETER_SET_44), 0);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    status = BCryptFinalizeKeyPair(hKeyPair, NULL);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    // Public/Private key pair is generated at this point

    // Get the signature size
    status = BCryptSignHash(hKeyPair, NULL, NULL, 0, NULL, 0, &cbSignature, NULL);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    pbSignature = (PBYTE)LocalAlloc(LMEM_FIXED, cbSignature);

    if (pbSignature == NULL) {
  status = STATUS_NO_MEMORY;
        goto cleanup;
    }

    //
    // Sign with pure ML-DSA
    //
    // Pure variants sign arbitrary length messages whereas
    // the pre-hash variants sign a hash value of the input.
    // To sign with pure ML-DSA or pure SLH-DSA, pszPrehashAlgId must be set
    // to NULL if BCRYPT_PQDSA_PADDING_INFO is provided to the sign/verify
    // functions.

    // For pre-hash signing, the hash algorithm used in creating
    // the hash of the input must be specified using the pszPrehashAlgId
    // field of BCRYPT_PQDSA_PADDING_INFO. This hash algorithm information
    // is required in generating and verifying the signature as the OID of
    // the hash algorithm becomes a prefix of the input to be signed.
    //

    padinfo.pbCtx = (PUCHAR)ctx;
    padinfo.cbCtx = sizeof(ctx);
    padinfo.pszPrehashAlgId = NULL;

    status = BCryptSignHash(
        hKeyPair,
        &padinfo,
        (PUCHAR)msg,
        sizeof(msg),
        pbSignature,
        cbSignature,
        &cbWritten, 
        BCRYPT_PAD_PQDSA);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    //
    // Export the public key
    //

    // Get the public key blob size
    status = BCryptExportKey(
        hKeyPair,
        NULL,
        BCRYPT_PQDSA_PUBLIC_BLOB,
        NULL,
        0,
        &cbPublicKeyBlob,
        NULL);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    pbPublicKeyBlob = (PBYTE)LocalAlloc(LMEM_FIXED, cbPublicKeyBlob);

    if (pbPublicKeyBlob == NULL) {
  status = STATUS_NO_MEMORY;
        goto cleanup;
    }

    // Export the public key
    status = BCryptExportKey(
        hKeyPair,
        NULL,
        BCRYPT_PQDSA_PUBLIC_BLOB,
        pbPublicKeyBlob,
        cbPublicKeyBlob,
        &cbWritten,
        NULL);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

cleanup:

    if (pbPublicKeyBlob)
    {
        LocalFree(pbPublicKeyBlob);
    }

    if (pbSignature)
    {
        LocalFree(pbSignature);
    }

    if (hKeyPair != NULL)
    {
        BCryptDestroyKey(hKeyPair);
    }

    if (hAlg != NULL)
    {
        BCryptCloseAlgorithmProvider(hAlg, NULL);
    }

    return status;
}

//
// This function takes as input an ML-DSA-44 public key blob,
// and a signature generated by the same algorithm along with
// the message the signature belongs to. It verifies whether the
// signature is valid or not.
//
NTSTATUS MLDSAVerifySample(
    PCBYTE pbPublicKeyBlob,
    ULONG cbPublicKeyBlob,
    PCBYTE pbMsg,
    ULONG cbMsg,
    PCBYTE pbContext,
    ULONG cbContext,
    PCBYTE pbSignature,
    ULONG cbSignature)
{
    NTSTATUS status = STATUS_SUCCESS;
    BCRYPT_ALG_HANDLE hAlg = NULL;
    BCRYPT_KEY_HANDLE hKey = NULL;
    BCRYPT_PQDSA_PADDING_INFO padinfo = { 0 };

    status = BCryptOpenAlgorithmProvider(
        &hAlg,
        BCRYPT_MLDSA_ALGORITHM,
        MS_PRIMITIVE_PROVIDER,
        NULL);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    // Import the public key
    status = BCryptImportKeyPair(
        hAlg,
        NULL,
        BCRYPT_PQDSA_PUBLIC_BLOB,
        &hKey,
        (PUCHAR)pbPublicKeyBlob,
        cbPublicKeyBlob,
        NULL);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

    // Verify the signature
    // Assuming the signature is generated with pure ML-DSA.
    // Otherwise pszPrehashAlgId must be set to the identifier
    // of the hash algorithm used in pre-hashing the input.
    padinfo.pbCtx = (PUCHAR)pbContext;
    padinfo.cbCtx = cbContext;
    padinfo.pszPrehashAlgId = NULL;

    status = BCryptVerifySignature(
        hKey,
        &padinfo,
        (PUCHAR)pbMsg,      // pbHash
        cbMsg,              // cbHash
        (PUCHAR)pbSignature,
        cbSignature,
        BCRYPT_PAD_PQDSA);

    if (!NT_SUCCESS(status)) {
        goto cleanup;
    }

cleanup:

    if (hKey != NULL)
    {
        BCryptDestroyKey(hKey);
    }

    if (hAlg != NULL)
    {
        BCryptCloseAlgorithmProvider(hAlg, NULL);
    }

    return status;
}