当您创建密钥材料来源为外部的密钥时,KMS不会为您创建的用户主密钥(CMK)生成密钥材料,此时您可以将自己的密钥材料导入到CMK中。本文为您介绍如何导入外部密钥材料。

前提条件

进行操作前,请确保您已经注册了阿里云账号。如还未注册,请先完成账号注册

背景信息

用户主密钥(CMK)是KMS的基本资源,由密钥ID、基本元数据(如密钥状态等)以及用于加密、解密数据的密钥材料组成。默认情况下,当您创建主密钥时,会由KMS生成密钥材料。您也可以选择创建密钥材料来源为外部的密钥,此时,KMS将不会为您创建的CMK生成密钥材料,您可以将自己的密钥材料导入到CMK中。

您可以通过调用DescribeKey接口来判断密钥材料来源情况。
  • 当KeyMetadata中OriginAliyun_KMS时,说明密钥材料由KMS生成,称为普通密钥
  • OriginEXTERNAL时,说明密钥材料由外部导入,称为外部密钥
在您选择密钥材料来源为外部,使用您自己导入的密钥材料时,需要注意以下几点:
  • 请确保您使用了符合要求的随机源生成密钥材料。
  • 请确保密钥材料的可靠性。
    • KMS可以确保导入密钥材料高可用,但是不能确保导入密钥材料与KMS生成的密钥材料具有相同的可靠性。
    • 您导入的密钥材料可以直接调用DeleteKeyMaterial接口进行删除,也可以设置过期时间,在密钥材料过期后进行删除(CMK不会被删除)。KMS生成的密钥材料无法直接被删除,只能通过调用ScheduleKeyDeletion接口,在等待7到30天后,随着CMK一起被删除。
    • 当导入的密钥材料被删除后,可以再次导入相同的密钥材料使得CMK再次可用,因此您需要自行保存密钥材料的副本。
  • 每个CMK只能拥有一个导入密钥材料。当您将一个密钥材料导入CMK时,该CMK将与该密钥材料绑定,即便密钥材料已经过期或者被删除,也不能导入其他密钥材料。如果您需要轮换使用外部密钥材料的CMK,只能创建一个新的CMK然后导入新的密钥材料。
  • CMK具有独立性。例如:您使用CMK加密的数据,无法使用其他CMK进行解密,即便这些CMK都使用相同的密钥材料。
  • 只能导入128位或256位对称密钥作为密钥材料。128位密钥仅支持部分地域,详情请参见支持的地域

通过控制台导入密钥材料

  1. 创建外部密钥。
    1. 登录密钥管理服务控制台
    2. 在页面左上角的地域下拉列表,选择密钥所在的地域。
    3. 在左侧导航栏,单击用户主密钥
    4. 单击创建密钥
    5. 在弹出的创建密钥对话框,选择密钥类型
      请选择对称密钥类型Aliyun_AES_256或Aliyun_SM4。密钥类型详情请参见对称密钥的类型
    6. 配置别名描述轮转周期
    7. 单击高级选项,选择密钥材料来源外部
    8. 勾选我了解使用外部密钥材料的方法和意义,单击确认
  2. 获取导入密钥材料参数。
    导入密钥材料参数包括一个用于加密密钥材料的公钥,以及一个导入令牌。
    1. 在左侧导航栏,单击用户主密钥
    2. 找到待导入密钥材料的密钥,单击别名,进入密钥管理页面。
    3. 密钥材料区域,单击获取导入密钥材料参数
    4. 在弹出的获取导入密钥材料参数对话框,选择加密算法RSAES_OAEP_SHA_1,单击下一步
      您可以选择加密算法RSAES_PKCS1_V1_5(默认值)、RSAES_OAEP_SHA_1或RSAES_OAEP_SHA_256,本文以RSAES_OAEP_SHA_1为例为您介绍。
    5. 在弹出的获取导入密钥材料参数对话框,下载加密公钥导入令牌,单击关闭
  3. 使用OPENSSL加密密钥材料。
    加密公钥是一个2048比特的RSA公钥,使用的加密算法需要与获取导入密钥材料参数时指定的一致。由于加密公钥经过Base64编码,因此在使用时需要先进行Base64解码。您可以通过OPENSSL加密公钥,获取您自己的密钥材料。
    1. 创建一个密钥材料,使用OPENSSL产生一个32字节的随机数进行演示。
    2. 将加密公钥进行Base64解码。
    3. 根据指定的加密算法(以RSAES_OAEP_SHA_1为例)加密密钥材料。
    4. 将加密后的密钥材料进行Base64编码,保存为文本文件。
      openssl rand -out KeyMaterial.bin 32
      openssl enc -d -base64 -A -in PublicKey_base64.txt -out PublicKey.bin
      openssl rsautl -encrypt -in KeyMaterial.bin -oaep -inkey PublicKey.bin  -keyform DER  -pubin -out EncryptedKeyMaterial.bin
      openssl enc -e -base64 -A -in EncryptedKeyMaterial.bin -out EncryptedKeyMaterial_base64.txt
  4. 导入密钥材料。

    导入密钥材料时,可以将从未导入过密钥材料的外部密钥进行导入,也可以重新导入已经过期和已被删除的密钥材料,或者重置密钥材料的过期时间。

    导入令牌与加密密钥材料的公钥具有绑定关系,一个令牌只能为其生成时指定的主密钥导入密钥材料。导入令牌的有效期为24小时,在有效期内可以重复使用,失效以后需要获取新的导入令牌和加密公钥。

    1. 在左侧导航栏,单击用户主密钥
    2. 找到待导入密钥材料的密钥,单击别名,进入密钥管理页面。
    3. 密钥材料区域,单击导入密钥材料
    4. 在弹出的导入密钥材料对话框,上传加密密钥材料导入令牌
      • 加密密钥材料:上传步骤3生成的密钥材料文本文件。
      • 导入令牌:上传步骤2获取的导入令牌文本文件。
    5. 设置密钥材料过期时间,单击确认
      导入密钥材料成功后,密钥状态从待导入更新为启用中

通过ALIYUN CLI导入密钥材料

  1. 创建外部密钥。
    通过命令aliyun kms CreateKey调用CreateKey接口,指定参数OriginEXTERNAL
    aliyun kms CreateKey --Origin EXTERNAL --Description "External key"
  2. 获取导入密钥材料参数。
    通过命令aliyun kms GetParametersForImport调用GetParametersForImport接口获取导入密钥材料参数。
    aliyun kms GetParametersForImport --KeyId 1339cb7d-54d3-47e0-b595-c7d3dba8**** --WrappingAlgorithm RSAES_OAEP_SHA_1 --WrappingKeySpec RSA_2048
  3. 导入密钥材料。
    1. 使用加密公钥对密钥材料进行加密。

      加密公钥是一个RSA(2048比特)或者SM2公钥,使用的加密算法需要与获取导入密钥材料参数时指定的一致。由于API返回的加密公钥经过Base64编码,因此在使用时需要先进行Base64解码。目前KMS支持的加密算法有RSAES_OAEP_SHA_1、RSAES_OAEP_SHA_256、RSAES_PKCS1_V1_5和SM2PKE,其中SM2PKE仅部分地域支持,详情请参见支持的地域

    2. 将加密后的密钥材料进行Base64编码。
    3. 通过命令aliyun kms ImportKeyMaterial调用ImportKeyMaterial接口,将编码后的密钥材料与导入令牌一起,作为ImportKeyMaterial接口的参数导入KMS。
      aliyun kms ImportKeyMaterial --KeyId 1339cb7d-54d3-47e0-b595-c7d3dba8**** --EncryptedKeyMaterial xxx --ImportToken xxxx

代码示例

  • JAVA SDK
    //使用最新KMS JAVA SDK。
    //KmsClient.java
    
    import com.aliyuncs.kms.model.v20160120.*;
    import com.aliyuncs.profile.DefaultProfile;
    
    //KMS API封装。
    public class KmsClient {
            DefaultAcsClient client;
    
            public KmsClient( String region_id, String ak, String secret) {
                    DefaultProfile profile = DefaultProfile.getProfile(region_id, ak, secret);
                    this.client = new DefaultAcsClient(profile);
            }
    
            public CreateKeyResponse createKey() throws Exception {
                    CreateKeyRequest request = new CreateKeyRequest();
                    request.setOrigin("EXTERNAL"); //创建外部密钥。
                    return this.client.getAcsResponse(request);
            }
            //... 省略,其余API类似。
    }
    //example.java
    import com.aliyuncs.kms.model.v20160120.*;
    import KmsClient
    import java.security.KeyFactory;
    import java.security.PublicKey;
    import java.security.spec.MGF1ParameterSpec;
    import javax.crypto.Cipher;
    import javax.crypto.spec.OAEPParameterSpec;
    import javax.crypto.spec.PSource.PSpecified;
    import java.security.spec.X509EncodedKeySpec;
    import java.util.Random;
    import javax.xml.bind.DatatypeConverter;
    
    public class CreateAndImportExample {
            public static void main(String[] args) {
                    String regionId = "cn-hangzhou";
            String accessKeyId = "*** Provide your AccessKeyId ***";
            String accessKeySecret = "*** Provide your AccessKeySecret ***";
            KmsClient kmsclient = new KmsClient(regionId,accessKeyId,accessKeySecret);
            //创建外部密钥。
            try {
                    CreateKeyResponse keyResponse = kmsclient.createKey();
                    String keyId = keyResponse.KeyMetadata.getKeyId();
                    //产生一个32字节随机数。
                    byte[] keyMaterial = new byte[32];
                    new Random().nextBytes(keyMaterial);
                    //获取导入密钥材料参数。
                    GetParametersForImportResponse paramResponse = kmsclient.getParametersForImport(keyId,"RSAES_OAEP_SHA_256");
                    String importToekn = paramResponse.getImportToken();
                    String encryptPublicKey = paramResponse.getPublicKey();
                    //Base64解码加密公钥。
                    byte[] publicKeyDer = DatatypeConverter.parseBase64Binary(encryptPublicKey);
                    //解析成RSA的公钥。
                    KeyFactory keyFact = KeyFactory.getInstance("RSA");
                    X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyDer);
                    PublicKey publicKey = keyFact.generatePublic(spec);
                    //加密密钥材料。
                    Cipher oaepFromAlgo = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
                    String hashFunc = "SHA-256";
                    OAEPParameterSpec oaepParams = new OAEPParameterSpec(hashFunc, "MGF1", new MGF1ParameterSpec(hashFunc), PSpecified.DEFAULT);
                    oaepFromAlgo.init(Cipher.ENCRYPT_MODE, publicKey, oaepParams);
                    byte[] cipherDer = oaepFromAlgo.doFinal(keyMaterial);
                    //加密后的密钥材料,需要进行Base64编码。
                    String encryptedKeyMaterial = DatatypeConverter.printBase64Binary(cipherDer);
                    //导入密钥材料。
                    Long expireTimestamp = 1546272000L; //Unix时间戳,精确到秒,0表示永不过期。
                            kmsClient.importKeyMaterial(keyId,encryptedKeyMaterial, expireTimestamp);
            } catch(Exception e) {
                    //... 省略。
            }
            }
    }
  • Go SDK
    package main
    
    import (
        "crypto/rand"
        "crypto/rsa"
        "crypto/sha256"
        "crypto/x509"
        "encoding/base64"
        "fmt"
        "log"
        random "math/rand"
        "time"
    
        "github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
    )
    
    //KMS CreateKey API封装。
    func kmsCreateKey(client *kms.Client) (string, error) {
        request := kms.CreateCreateKeyRequest()
        request.Scheme = "https"
        request.Origin = "EXTERNAL" //创建外部密钥。
        response, err := client.CreateKey(request)
        if err != nil {
            return "", fmt.Errorf("CreateKey error:%v", err)
        }
        return response.KeyMetadata.KeyId, nil
    }
    
    //KMS GetParametersForImport API封装。
    func kmsGetParametersForImport(client *kms.Client, keyId, wrappingKeySpec, wrappingAlgorithm string) (string, string, error) {
        request := kms.CreateGetParametersForImportRequest()
        request.Scheme = "https"
        request.KeyId = keyId
        request.WrappingKeySpec = wrappingKeySpec
        request.WrappingAlgorithm = wrappingAlgorithm
        response, err := client.GetParametersForImport(request)
        if err != nil {
            return "", "", fmt.Errorf("GetParametersForImport error:%v", err)
        }
        return response.PublicKey, response.ImportToken, nil
    }
    
    //KMS ImportKeyMaterial API封装。
    func kmsImportKeyMaterial(client *kms.Client, keyId, importToken, encryptedKeyMaterial string) error {
        request := kms.CreateImportKeyMaterialRequest()
        request.Scheme = "https"
        request.KeyId = keyId
        request.ImportToken = importToken
        request.EncryptedKeyMaterial = encryptedKeyMaterial
        _, err := client.ImportKeyMaterial(request)
        if err != nil {
            return fmt.Errorf("ImportKeyMaterial error:%v", err)
        }
        return nil
    }
    
    func randBytes(n int) []byte {
        var r = random.New(random.NewSource(time.Now().UnixNano()))
        bytes := make([]byte, n)
        for i := range bytes {
            bytes[i] = byte(r.Intn(256))
        }
        return bytes
    }
    
    func main() {
        accessKeyId := "*** Provide your AccessKeyId ***"
        accessKeySecret := "*** Provide your AccessKeySecret ***"
        regionId := "cn-hangzhou"
        client, err := kms.NewClientWithAccessKey(regionId, accessKeyId, accessKeySecret)
        if err != nil {
            log.Fatalf("NewClientWithAccessKey error:%+v\n", err)
        }
        //创建一个外部密钥。
        keyId, err := kmsCreateKey(client)
        if err != nil {
            log.Fatalf("kmsCreateKey error:%+v\n", err)
        }
        //以下示例代码产生一个32字节随机数。在实际应用中您需要通过用户的密钥管理系统生成密钥,并使用导入密钥材料参数中的公钥进行加密。
        keyMaterial := randBytes(32)
        //获取导入密钥材料参数。
        encryptPublicKey, importToken, err := kmsGetParametersForImport(client, keyId, "RSA_2048", "RSAES_OAEP_SHA_256")
        if err != nil {
            log.Fatalf("kmsGetParametersForImport error:%v\n", err)
        }
        //Base64解码加密公钥。
        publicKeyDer, err := base64.StdEncoding.DecodeString(encryptPublicKey)
        if err != nil {
            log.Fatalf("base64.StdEncoding.DecodeString error:%v\n", err)
        }
        //解析成RSA的公钥。
        publicKey, err := x509.ParsePKIXPublicKey(publicKeyDer)
        if err != nil {
            log.Fatalf("x509.ParsePKIXPublicKey error:%v\n", err)
        }
        //加密密钥材料。
        cipherDer, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey.(*rsa.PublicKey), keyMaterial, nil)
        if err != nil {
            log.Fatalf("rsa.EncryptOAEP error:%v\n", err)
        }
        //加密后的密钥材料,需要进行Base64编码。
        encryptedKeyMaterial := base64.StdEncoding.EncodeToString(cipherDer)
        //导入密钥材料。
        err = kmsImportKeyMaterial(client, keyId, importToken, encryptedKeyMaterial)
        if err != nil {
            log.Fatalf("ImportKeyMaterial error:%v", err)
        }
    }