

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

# 离线操作示例
<a name="offline-operations"></a>

下载非对称 KMS 密钥对的[公有密钥](download-public-key.md)后，您可以与他人共享该密钥并使用它来执行离线操作。

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

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

**Topics**
+ [

## 离线派生共享密钥
](#key-spec-ecc-offline)
+ [

## 使用 ML-DSA 密钥对进行离线验证
](#mldsa-offline-verification)
+ [

## 使用 SM2 密钥对进行离线验证（仅限中国区域）
](#key-spec-sm-offline-verification)

## 离线派生共享密钥
<a name="key-spec-ecc-offline"></a>

您可以下载 ECC 密钥对的[公有密钥](download-public-key.md)以在离线操作中使用，即 AWS KMS外部的操作。

以下 [OpenSSL](https://openssl.org/) 演练演示了一种在使用 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"`
   ```

1. 在中创建 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}
   ```

1. 使用 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 密钥对进行离线验证
<a name="mldsa-offline-verification"></a>

AWS KMS 支持 ML-DSA 签名的套期保值变体，如[联邦信息处理标准 (FIPS) 204 标准](https://csrc.nist.gov/pubs/fips/204/final)第 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
<a name="mldsa-offline-verification-less-than-4KB"></a>

对于小于 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
<a name="mldsa-offline-verification-more-than-4KB"></a>

要对大于 4KB 的消息进行签名，请使用 `EXTERNAL_MU` 消息类型。使用 `EXTERNAL_MU` 时，您可以按照 NIST FIPS 204 第 6.2 节的定义，在外部将消息预哈希处理为 64 字节表示值 μ，然后将其传递给签名或验证操作。请注意，这不同于 NIST FIPS 204 第 5.4 节定义的“预哈希 ML-DSA”或 HashML-DSA。

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

1. 在消息前添加消息前缀。

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

1. 最后，对第 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 密钥对进行离线验证（仅限中国区域）
<a name="key-spec-sm-offline-verification"></a>

要使用 SM2 公钥验证外部 AWS KMS 的签名，必须指定可区分的 ID。当您将原始消息传递给 S [ign](https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html) API 时 [https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html#KMS-Sign-request-MessageType](https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html#KMS-Sign-request-MessageType)，会 AWS KMS 使用 OSCCA 在 0009-2012 年中 GM/T 定义的默认区分 ID。`1234567812345678`您不能在 AWS KMS中指定自己的区分 ID。

但是，如果您要在外部生成消息摘要 AWS，则可以指定自己的区分 ID，然后将消息摘要传递 AWS KMS 给签名。[https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html#API_Sign_RequestSyntax](https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html#API_Sign_RequestSyntax)要执行此操作，请更改 `SM2OfflineOperationHelper` 类中的 `DEFAULT_DISTINGUISHING_ID` 值。您指定的区分 ID 可以是长度不超过 8192 个字符的任何字符串。对消息摘要进行 AWS KMS 签名后，您需要消息摘要或消息以及用于计算摘要的区分 ID 以进行离线验证。

**重要**  
`SM2OfflineOperationHelper` 参考代码旨在兼容 [Bouncy Castle](https://www.bouncycastle.org/documentation/documentation-java/) 版本 1.68。如需其他版本的帮助，请联系 [bouncycastle.org](https://www.bouncycastle.org)。

### `SM2OfflineOperationHelper` 类
<a name="key-spec-sm-offline-helper"></a>

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

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

`SM2OfflineOperationHelper` 类为以下离线操作提供了方法：
+   
**消息摘要计算**  
要离线生成可用于离线验证或传递 AWS KMS 给签名的消息摘要，请使用`calculateSM2Digest`方法。该`calculateSM2Digest`方法使用 SM3 哈希算法生成消息摘要。[GetPublicKey](https://docs.aws.amazon.com/kms/latest/APIReference/API_GetPublicKey.html)API 会以二进制格式返回您的公钥。您必须将二进制密钥解析为 Jav PublicKey a。提供解析后的公有密钥与消息。该方法会自动将您的消息与默认的区分 ID `1234567812345678` 相结合，但您可以通过更改 `DEFAULT_DISTINGUISHING_ID` 值来设置自己的区分 ID。
+   
**验证**  
要离线验证签名，请使用 `offlineSM2DSAVerify` 方法。`offlineSM2DSAVerify` 方法会使用根据指定区分 ID 计算出的消息摘要和您提供的原始消息来验证数字签名。[GetPublicKey](https://docs.aws.amazon.com/kms/latest/APIReference/API_GetPublicKey.html)API 会以二进制格式返回您的公钥。您必须将二进制密钥解析为 Jav PublicKey a。向解析后的公有密钥提供原始消息和要验证的签名。有关更多详细信息，请参阅[使用 SM2 密钥对进行离线验证](#key-spec-sm-offline-verification)。
+   
**Encrypt**  
要离线加密明文，请使用 `offlineSM2PKEEncrypt` 方法。此方法可确保密文采用 AWS KMS 可以解密的格式。该`offlineSM2PKEEncrypt`方法对明文进行加密，然后将 SM2 PKE 生成的原始密文转换为 ASN.1 格式。[GetPublicKey](https://docs.aws.amazon.com/kms/latest/APIReference/API_GetPublicKey.html)API 会以二进制格式返回您的公钥。您必须将二进制密钥解析为 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");
    }
}
```