离线操作示例 - AWS Key Management Service

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

离线操作示例

下载非对称 KMS 密钥对的公有密钥后,您可以与他人共享该密钥并使用它来执行离线操作。

AWS CloudTrail 记录每个 AWS KMS 操作(包括请求、响应、日期、时间和授权用户)的日志不会记录外部公钥的使用情况 AWS KMS。

本主题提供了离线操作示例,以及这些工具 AWS KMS 为简化离线操作而提供的详细信息。

离线派生共享密钥

您可以下载 ECC 密钥对的公有密钥以在离线操作中使用,即 AWS KMS外部的操作。

以下 OpenSSL 演练演示了一种在使用 ECC KMS 密钥对的公钥和 AWS KMS 使用 OpenSSL 创建的私钥之外获取共享密钥的方法。

  1. 在 OpenSSL 中创建 ECC 密钥对并做好使用准备。 AWS KMS

    // Create an ECC key pair in OpenSSL and save the private key in openssl_ecc_key_priv.pem export OPENSSL_CURVE_NAME="P-256" export KMS_CURVE_NAME="ECC_NIST_P256" export OPENSSL_KEY1_PRIV_PEM="openssl_ecc_key1_priv.pem" openssl ecparam -name ${OPENSSL_CURVE_NAME} -genkey -out ${OPENSSL_KEY1_PRIV_PEM} // Derive the public key from the private key export OPENSSL_KEY1_PUB_PEM="openssl_ecc_key1_pub.pem" openssl ec -in ${OPENSSL_KEY1_PRIV_PEM} -pubout -outform pem \ -out ${OPENSSL_KEY1_PUB_PEM} // View the PEM file containing the public key and extract the public key as a // Base64 encoded string into OPENSSL_KEY1_PUB_BASE64 for use with AWS KMS export OPENSSL_KEY1_PUB_BASE64=`cat ${OPENSSL_KEY1_PUB_PEM} | \ tee /dev/stderr | grep -v "PUBLIC KEY" | tr -d "\n"`
  2. 在中创建 ECC 密钥协议密钥对, AWS KMS 并准备好将其与 OpenSSL 一起使用。

    // Create a KMS key on the same curve as the key pair from step 1 // with a key usage of KEY_AGREEMENT // Save its ARN in KMS_KEY1_ARN. export KMS_KEY1_ARN=`aws kms create-key --key-spec ${KMS_CURVE_NAME} \ --key-usage KEY_AGREEMENT | tee /dev/stderr | jq -r .KeyMetadata.Arn` // Download the public key and save the Base64-encoded version in KMS_KEY1_PUB_BASE64 export KMS_KEY1_PUB_BASE64=`aws kms get-public-key --key-id ${KMS_KEY1_ARN} | \ tee /dev/stderr | jq -r .PublicKey` // Create a PEM file for the public KMS key for use with OpenSSL export KMS_KEY1_PUB_PEM="aws_kms_ecdh_key1_pub.pem" echo "-----BEGIN PUBLIC KEY-----" > ${KMS_KEY1_PUB_PEM} echo ${KMS_KEY1_PUB_BASE64} | fold -w 64 >> ${KMS_KEY1_PUB_PEM} echo "-----END PUBLIC KEY-----" >> ${KMS_KEY1_PUB_PEM}
  3. 使用 OpenSSL 中的私有密钥和 KMS 公有密钥在 OpenSSL 中派生共享密钥。

    export OPENSSL_SHARED_SECRET1_BIN="openssl_shared_secret1.bin" openssl pkeyutl -derive -inkey ${OPENSSL_KEY1_PRIV_PEM} \ -peerkey ${KMS_KEY1_PUB_PEM} -out ${OPENSSL_SHARED_SECRET1_BIN}

使用 ML-DSA 密钥对进行离线验证

AWS KMS 支持 ML-DSA 签名的套期保值变体,如联邦信息处理标准 (FIPS) 204 标准第 3.4 节中所述,适用于最大 4 KB 字节的消息。

要对大于 4 KB 的邮件进行签名,请在外面执行邮件预处理步骤。 AWS KMS此哈希步骤创建了一条 64 字节的消息代表 μ,如 NIST FIPS 204 第 6.2 节中所定义。

AWS KMS EXTERNAL_MU对于大于 4 KB 的消息,有一种名为的消息类型。当你用它来代替RAW消息类型时, AWS KMS:

  • 假设你已经执行了哈希步骤

  • 跳过其内部哈希处理过程

  • 可处理任何大小的消息

验证消息时,使用的方法取决于外部系统或库的大小限制,以及它是否支持 64 字节的消息代表 μ:

  • 如果消息小于大小限制,请使用RAW消息类型。

  • 如果消息大于大小限制,请在外部系统中使用代表性 μ。

以下各节演示如何使用 OpenSSL 对消息进行签名 AWS KMS 以及如何使用 OpenSSL 验证消息。我们为低于和超过 4 KB 邮件大小限制的消息提供了示例 AWS KMS。OpenSSL 不会对用于验证的邮件大小施加限制。

对于这两个示例,首先从中获取公钥 AWS KMS。使用以下 AWS CLI 命令:

aws kms get-public-key \ --key-id _<1234abcd-12ab-34cd-56ef-1234567890ab>_ \ --output text \ --query PublicKey | base64 --decode > public_key.der

邮件大小小于 4KB

对于小于 4 KB 的消息,请使用带的RAW消息类型 AWS KMS。虽然您可以使用EXTERNAL_MU,但对于大小限制范围内的邮件,则不必这样做。

使用以下 AWS CLI 命令对邮件进行签名:

aws kms sign \ --key-id _<1234abcd-12ab-34cd-56ef-1234567890ab>_ \ --message 'your message' \ --message-type RAW \ --signing-algorithm ML_DSA_SHAKE_256 \ --output text \ --query Signature | base64 --decode > ExampleSignature.bin

要使用 OpenSSL 验证此消息,请使用以下命令:

echo -n 'your message' | ./openssl dgst -verify public_key.der -signature ExampleSignature.bin

邮件大小超过 4KB

要对大于 4KB 的邮件进行签名,请使用EXTERNAL_MU消息类型。使用时EXTERNAL_MU,您可以按照 NIST FIPS 204 第 6.2 节的定义,在外部将消息预哈希到 64 字节的代表 μ,然后将其传递给签名或验证操作。请注意,这与 NIST FIPS 204 第 5.4 节中定义的 “pre-Hash MLDSA” 或 Hashml-dsa 不同。

  1. 首先,构造一个消息前缀。前缀包含域分隔符、任何上下文的长度和上下文。域分隔符和上下文长度的默认值为零。

  2. 在消息前面加上消息前缀。

  3. 用于 SHAKE256 对公钥进行哈希处理并将其置于步骤 2 的结果之前。

  4. 最后,对步骤 3 的结果进行哈希处理以生成 64 字节EXTERNAL_MU

以下示例使用 OpenSSL 3.5 来构造:EXTERNAL_MU

{ openssl asn1parse -inform DER -in public_key.der -strparse 17 -noout -out - 2>/dev/null | openssl dgst -provider default -shake256 -xoflen 64 -binary; printf '\x00\x00'; echo -n "your message" } | openssl dgst -provider default -shake256 -xoflen 64 -binary > mu.bin

创建mu.bin文件后,使用以下命令调用 AWS KMS API 对消息进行签名:

aws kms sign \ --key-id _<1234abcd-12ab-34cd-56ef-1234567890ab>_ \ --message fileb://mu.bin \ --message-type EXTERNAL_MU \ --signing-algorithm ML_DSA_SHAKE_256 \ --output text \ --query Signature | base64 --decode > ExampleSignature.bin

生成的签名与原始邮件上的RAW签名相同。你可以使用相同的 OpenSSL 3.5 命令来验证消息:

echo -n 'your message' | ./openssl dgst -verify public_key.der -signature ExampleSignature.bin

使用 SM2 密钥对进行离线验证(仅限中国区域)

要使用 SM2 公钥验证外部 AWS KMS 的签名,必须指定可区分的 ID。当您将原始消息传递给 S ign API 时 MessageType:RAW,会 AWS KMS 使用 OSCCA 在 0009-2012 年中 GM/T 定义的默认区分 ID。1234567812345678您不能在 AWS KMS中指定自己的区分 ID。

但是,如果您要在外部生成消息摘要 AWS,则可以指定自己的区分 ID,然后将消息摘要传递 AWS KMS 给签名。MessageType:DIGEST要执行此操作,请更改 SM2OfflineOperationHelper 类中的 DEFAULT_DISTINGUISHING_ID 值。您指定的区分 ID 可以是长度不超过 8192 个字符的任何字符串。对消息摘要进行 AWS KMS 签名后,您需要消息摘要或消息以及用于计算摘要的区分 ID 以进行离线验证。

重要

SM2OfflineOperationHelper 参考代码旨在兼容 Bouncy Castle 版本 1.68。如需其他版本的帮助,请联系 bouncycastle.org

SM2OfflineOperationHelper

为了帮助您使用 SM2 密钥进行离线操作,Java SM2OfflineOperationHelper 类提供了可以为您执行任务的方法。您可以将此帮助程序类用作其他加密提供程序的模型。

在内部 AWS KMS,原始密文转换和 SM2 DSA 消息摘要计算会自动进行。并非所有加密提供商都 SM2 以相同的方式实现。某些库(例如 OpenSSL 版本 1.1.1 及更高版本)会自动执行这些操作。 AWS KMS 在 OpenSSL 版本 3.0 的测试中确认了这种行为。使用以下带有库的 SM2OfflineOperationHelper 类,比如 Bouncy Castle,这需要您手动执行这些转换和计算。

SM2OfflineOperationHelper 类为以下离线操作提供了方法:

  • 消息摘要计算

    要离线生成可用于离线验证或传递 AWS KMS 给签名的消息摘要,请使用calculateSM2Digest方法。该calculateSM2Digest方法使用 SM3 哈希算法生成消息摘要。GetPublicKeyAPI 会以二进制格式返回您的公钥。您必须将二进制密钥解析为 Jav PublicKey a。提供解析后的公有密钥与消息。该方法会自动将您的消息与默认的区分 ID 1234567812345678 相结合,但您可以通过更改 DEFAULT_DISTINGUISHING_ID 值来设置自己的区分 ID。

  • 验证

    要离线验证签名,请使用 offlineSM2DSAVerify 方法。offlineSM2DSAVerify 方法会使用根据指定区分 ID 计算出的消息摘要和您提供的原始消息来验证数字签名。GetPublicKeyAPI 会以二进制格式返回您的公钥。您必须将二进制密钥解析为 Jav PublicKey a。向解析后的公有密钥提供原始消息和要验证的签名。有关更多详细信息,请参阅使用 SM2 密钥对进行离线验证

  • Encrypt

    要离线加密明文,请使用 offlineSM2PKEEncrypt 方法。此方法可确保密文采用 AWS KMS 可以解密的格式。该offlineSM2PKEEncrypt方法对明文进行加密,然后将 SM2 PKE 生成的原始密文转换为 ASN.1 格式。GetPublicKeyAPI 会以二进制格式返回您的公钥。您必须将二进制密钥解析为 Jav PublicKey a。为解析后的公有密钥提供您想要加密的明文。

    如果您不确定是否需要进行转换,请使用以下 OpenSSL 操作来测试加密文字的格式。如果操作失败,则需要将加密文字转换为 ASN.1 格式。

    openssl asn1parse -inform DER -in ciphertext.der

默认情况下,该SM2OfflineOperationHelper类在为 DSA 操作生成消息摘要时使用默认的区分 ID。1234567812345678 SM2

package com.amazon.kms.utils; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.PublicKey; import org.bouncycastle.crypto.CryptoException; import org.bouncycastle.jce.interfaces.ECPublicKey; import java.util.Arrays; import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.gm.GMNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.CipherParameters; import org.bouncycastle.crypto.params.ParametersWithID; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.crypto.signers.SM2Signer; import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; public class SM2OfflineOperationHelper { // You can change the DEFAULT_DISTINGUISHING_ID value to set your own distinguishing ID, // the DEFAULT_DISTINGUISHING_ID can be any string up to 8,192 characters long. private static final byte[] DEFAULT_DISTINGUISHING_ID = "1234567812345678".getBytes(StandardCharsets.UTF_8); private static final X9ECParameters SM2_X9EC_PARAMETERS = GMNamedCurves.getByName("sm2p256v1"); // ***calculateSM2Digest*** // Calculate message digest public static byte[] calculateSM2Digest(final PublicKey publicKey, final byte[] message) throws NoSuchProviderException, NoSuchAlgorithmException { final ECPublicKey ecPublicKey = (ECPublicKey) publicKey; // Generate SM3 hash of default distinguishing ID, 1234567812345678 final int entlenA = DEFAULT_DISTINGUISHING_ID.length * 8; final byte [] entla = new byte[] { (byte) (entlenA & 0xFF00), (byte) (entlenA & 0x00FF) }; final byte [] a = SM2_X9EC_PARAMETERS.getCurve().getA().getEncoded(); final byte [] b = SM2_X9EC_PARAMETERS.getCurve().getB().getEncoded(); final byte [] xg = SM2_X9EC_PARAMETERS.getG().getXCoord().getEncoded(); final byte [] yg = SM2_X9EC_PARAMETERS.getG().getYCoord().getEncoded(); final byte[] xa = ecPublicKey.getQ().getXCoord().getEncoded(); final byte[] ya = ecPublicKey.getQ().getYCoord().getEncoded(); final byte[] za = MessageDigest.getInstance("SM3", "BC") .digest(ByteBuffer.allocate(entla.length + DEFAULT_DISTINGUISHING_ID.length + a.length + b.length + xg.length + yg.length + xa.length + ya.length).put(entla).put(DEFAULT_DISTINGUISHING_ID).put(a).put(b).put(xg).put(yg).put(xa).put(ya) .array()); // Combine hashed distinguishing ID with original message to generate final digest return MessageDigest.getInstance("SM3", "BC") .digest(ByteBuffer.allocate(za.length + message.length).put(za).put(message) .array()); } // ***offlineSM2DSAVerify*** // Verify digital signature with SM2 public key public static boolean offlineSM2DSAVerify(final PublicKey publicKey, final byte [] message, final byte [] signature) throws InvalidKeyException { final SM2Signer signer = new SM2Signer(); CipherParameters cipherParameters = ECUtil.generatePublicKeyParameter(publicKey); cipherParameters = new ParametersWithID(cipherParameters, DEFAULT_DISTINGUISHING_ID); signer.init(false, cipherParameters); signer.update(message, 0, message.length); return signer.verifySignature(signature); } // ***offlineSM2PKEEncrypt*** // Encrypt data with SM2 public key public static byte[] offlineSM2PKEEncrypt(final PublicKey publicKey, final byte [] plaintext) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, IOException { final Cipher sm2Cipher = Cipher.getInstance("SM2", "BC"); sm2Cipher.init(Cipher.ENCRYPT_MODE, publicKey); // By default, Bouncy Castle returns raw ciphertext in the c1c2c3 format final byte [] cipherText = sm2Cipher.doFinal(plaintext); // Convert the raw ciphertext to the ASN.1 format before passing it to AWS KMS final ASN1EncodableVector asn1EncodableVector = new ASN1EncodableVector(); final int coordinateLength = (SM2_X9EC_PARAMETERS.getCurve().getFieldSize() + 7) / 8 * 2 + 1; final int sm3HashLength = 32; final int xCoordinateInCipherText = 33; final int yCoordinateInCipherText = 65; byte[] coords = new byte[coordinateLength]; byte[] sm3Hash = new byte[sm3HashLength]; byte[] remainingCipherText = new byte[cipherText.length - coordinateLength - sm3HashLength]; // Split components out of the ciphertext System.arraycopy(cipherText, 0, coords, 0, coordinateLength); System.arraycopy(cipherText, cipherText.length - sm3HashLength, sm3Hash, 0, sm3HashLength); System.arraycopy(cipherText, coordinateLength, remainingCipherText, 0,cipherText.length - coordinateLength - sm3HashLength); // Build standard SM2PKE ASN.1 ciphertext vector asn1EncodableVector.add(new ASN1Integer(new BigInteger(1, Arrays.copyOfRange(coords, 1, xCoordinateInCipherText)))); asn1EncodableVector.add(new ASN1Integer(new BigInteger(1, Arrays.copyOfRange(coords, xCoordinateInCipherText, yCoordinateInCipherText)))); asn1EncodableVector.add(new DEROctetString(sm3Hash)); asn1EncodableVector.add(new DEROctetString(remainingCipherText)); return new DERSequence(asn1EncodableVector).getEncoded("DER"); } }