离线操作示例 - AWS Key Management Service

离线操作示例

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

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

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

离线派生共享密钥

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

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

  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. 在 AWS KMS 中创建 ECC 密钥协议密钥对,并准备好将其与 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 节所定义。

对于大于 4 KB 的消息,AWS KMS 提供了一种名为 EXTERNAL_MU 的消息类型。使用此消息类型而不是 RAW 消息类型时,AWS KMS 会:

  • 假定您已经完成了哈希化步骤

  • 跳过其内部哈希化过程

  • 支持任何大小的消息

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

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

  • 如果消息大于大小限制,请使用外部系统中的表示值 μ。

以下各节说明了如何使用 AWS KMS 对消息进行签名以及如何使用 OpenSSL 验证消息。对于小于和大于 AWS KMS 所设定 4 KB 消息大小限制的消息,我们都提供了示例。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 节定义的“预哈希 ML-DSA”或 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。将原始消息 MessageType:RAW 传递到签名 API 时,AWS KMS 将使用 OSCCA 在 GM/T 0009-2012 中定义的默认区分 ID 1234567812345678。您不能在 AWS KMS 中指定自己的区分 ID。

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

重要

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

SM2OfflineOperationHelper

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

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

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

  • 消息摘要计算

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

  • 验证

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

  • Encrypt

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

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

    openssl asn1parse -inform DER -in ciphertext.der

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

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"); } }