本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。
离线操作示例
下载非对称 KMS 密钥对的公有密钥后,您可以与他人共享该密钥并使用它来执行离线操作。
AWS CloudTrail 记录每个 AWS KMS 操作(包括请求、响应、日期、时间和授权用户)的日志不会记录外部公钥的使用情况 AWS KMS。
本主题提供了离线操作示例,以及这些工具 AWS KMS 为简化离线操作而提供的详细信息。
离线派生共享密钥
您可以下载 ECC 密钥对的公有密钥以在离线操作中使用,即 AWS KMS外部的操作。
以下 OpenSSL
-
在 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"`
-
在中创建 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}
-
使用 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 标准
要对大于 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 不同。
-
首先,构造一个消息前缀。前缀包含域分隔符、任何上下文的长度和上下文。域分隔符和上下文长度的默认值为零。
-
在消息前面加上消息前缀。
-
用于 SHAKE256 对公钥进行哈希处理并将其置于步骤 2 的结果之前。
-
最后,对步骤 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
SM2OfflineOperationHelper
类
为了帮助您使用 SM2 密钥进行离线操作,Java SM2OfflineOperationHelper
类提供了可以为您执行任务的方法。您可以将此帮助程序类用作其他加密提供程序的模型。
在内部 AWS KMS,原始密文转换和 SM2 DSA 消息摘要计算会自动进行。并非所有加密提供商都 SM2 以相同的方式实现。某些库(例如 OpenSSLSM2OfflineOperationHelper
类,比如 Bouncy Castle
SM2OfflineOperationHelper
类为以下离线操作提供了方法:
-
- 消息摘要计算
-
要离线生成可用于离线验证或传递 AWS KMS 给签名的消息摘要,请使用
calculateSM2Digest
方法。该calculateSM2Digest
方法使用 SM3 哈希算法生成消息摘要。GetPublicKeyAPI 会以二进制格式返回您的公钥。您必须将二进制密钥解析为 Jav PublicKey a。提供解析后的公有密钥与消息。该方法会自动将您的消息与默认的区分 ID1234567812345678
相结合,但您可以通过更改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"); } }