Web 加密实现 HKDF 用于 ECDH 的输出

Web cryptography implement HKDF for the output of ECDH

我想使用 HKDF 作为密钥派生函数实现一个椭圆曲线 diffie hellman。我在前端使用 python 后端和 (vanilla) javascript。我正在使用 python cryptography library in backend and Web Crypto api in frontend as cryptographic library. I created ECDH key pair in both side and exchanged the pbulic keys. Now I am trying to create the AES shared key with the exchanged public key and private key along with HKDF algorithm. I am able to do it in the python backend (I followed this example 作为 python 代码):

def encrypt(public_key, secret):
global loaded_public_key
loaded_public_key = public_key
shared_key = server_private_key.exchange(ec.ECDH(), public_key)
IV = bytes("ddfbccae-b4c4-11", encoding="utf-8")
derived_key = HKDF(
        algorithm=hashes.SHA256(),
        length=32,
        salt=None,
        info=None,
).derive(shared_key)
aes = Cipher(algorithms.AES(derived_key), modes.GCM(IV))
encryptor = aes.encryptor()
padder = padding.PKCS7(128).padder()
padded_data = padder.update(secret.encode()) + padder.finalize()
return encryptor.update(secret.encode()) + encryptor.finalize()

但我不确定如何使用网络加密 api。这是我的尝试:(但不起作用)

async function deriveSecretKey(privateKey, publicKey) {
  let sharedKey = await window.crypto.subtle.deriveKey(
    {
      name: "ECDH",
      public: publicKey
    },
    privateKey,
    {
      name: "AES-GCM",
      length: 256
    },
    false,
    ["encrypt", "decrypt"]
  );
  return window.crypto.subtle.deriveKey(
    {
      name: "HKDF",
      hash:  {name: "SHA-256"} ,
      salt: new ArrayBuffer(0),
      info: new ArrayBuffer(0)
    },
    sharedKey,
    {
      name: "AES-GCM",
      length: 256
    },
    false,
    ["encrypt", "decrypt"]
  );
}

如何在前端使用网络加密 api 创建共享 AES 密钥和 HKDF(与 python 相同的方式)?

referenced Python code uses P-384 (aka secp384r1) as elliptic curve. This is compatible with the WebCrypto API, which supports three curves P-256 (aka secp256r1), P-384 and P-521 (aka secp521r1), see EcKeyImportParams.

以下 WebCrypto 代码使用 ECDH 生成共享密钥,并使用 HKDF 从共享密钥派生 AES 密钥。详细情况如下:

  • 为了允许将派生密钥与引用的 Python 代码进行比较,应用了预定义的 EC 密钥。私钥作为 PKCS#8 导入,public 密钥作为 X.509/SPKI。请注意,由于有关导入 EC 密钥的 Firefox 错误,以下脚本不能在 Firefox 浏览器中 运行。
  • 导入后,使用 deriveBits()(而非 deriveKey())通过 ECDH 创建共享密钥。
  • 使用 importKey() 导入共享密钥,然后使用 HKDF 导出 AES 密钥,再次使用 deriveBits()

(async () => {
    await deriveKey();
})();

async function deriveKey() {

    //
    // Key import
    //
    var server_x509 =  `-----BEGIN PUBLIC KEY-----
                        MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEd7fej9GYVI7Vt6x5B6XhruHvmE/rnzIj
                        HmpxP8PKfnfWgrJbyG2cgQc3mf9uusqk1FKImA86rx2+avK8+7xIK9wxuF3x2KQq
                        nxNp7bUBit3phyhp72Nt/QLXmZHcDKXL
                        -----END PUBLIC KEY-----`;
    var client_pkcs8 = `-----BEGIN PRIVATE KEY-----
                        MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBjr4EGktNtx+3xErsC
                        MzldruzzfAEEO8Oth1/3b8sNfrqRsAgMnB/oVy024I+15wOhZANiAASbTF7LLedW
                        dik6nH8JX8WeU0R1ZRlqq0EAZ/t+UrFcSOaVJSOx5jMJ3nrqwuk2DnobDqFwXH6t
                        ZMsZHh4NFZ+bCVeHJRqy4SCZvQFB/xcksF29p1v14XHYI/XKMGyLLx4=
                        -----END PRIVATE KEY-----`;

    var client_private_key = b64_to_ab(client_pkcs8.replace('-----BEGIN PRIVATE KEY-----', '').replace('-----END PRIVATE KEY-----', ''));
    var server_public_key = b64_to_ab(server_x509.replace('-----BEGIN PUBLIC KEY-----', '').replace('-----END PUBLIC KEY-----', ''));
    var privateKey = await window.crypto.subtle.importKey( 
        'pkcs8', 
        client_private_key,
        { name: "ECDH", namedCurve: "P-384" },
        true, 
        ["deriveKey", "deriveBits"] 
    );
    var publicKey = await window.crypto.subtle.importKey(
        "spki", 
        server_public_key,
        { name: "ECDH", namedCurve: "P-384" },
        true, 
        [] 
    );
    
    //
    // Determine shared secret
    //
    var sharedSecret = await window.crypto.subtle.deriveBits(
        { name: "ECDH", namedCurve: "P-384", public: publicKey },
        privateKey, 
        384 
    );
    console.log("Shared secret:\n", ab_to_b64(sharedSecret).replace(/(.{48})/g,'\n'));
    
    //
    // Derive key from shared secret via HKDF
    //
    var sharedSecretKey = await window.crypto.subtle.importKey(
        "raw", 
        sharedSecret, 
        { name: "HKDF" }, 
        false, 
        ["deriveKey", "deriveBits"]
    );
    var derived_key = await crypto.subtle.deriveBits(
        { name: "HKDF", hash: "SHA-256", salt: new Uint8Array([]), info: new Uint8Array([]) }, 
        sharedSecretKey, 
        256
    );
    console.log("Derived key:\n", ab_to_b64(derived_key).replace(/(.{48})/g,'\n'))
}; 

function b64_to_ab(base64_string){
    return Uint8Array.from(atob(base64_string), c => c.charCodeAt(0));
}

function ab_to_b64(arrayBuffer){
    return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}

具有以下输出:

Shared secret:
 xbU6oDHMTYj3O71liM5KEJof3/0P4HlHJ28k7qtdqU/36llCizIlOWXtj8v+IngF
Derived key:
 Yh0FkhqrT9XDQqIiSrGv5YmBjCSj9jhR5fF6HusbN1Q=

为了将生成的 AES 密钥与引用的 Python 代码进行比较,使用了以下 Python 代码,该代码基于引用的代码,但应用了与WebCrypto 代码中使用的密钥。由于这里的重点是密钥推导,所以AES部分不考虑:

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
import base64

def deriveKey():

  server_pkcs8 = b'''-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBReGpDVmoVTzxNbJx6
aL4L9z1EdB91eonAmAw7mKDocLfCJITXZPUAmM46c6AipTmhZANiAAR3t96P0ZhU
jtW3rHkHpeGu4e+YT+ufMiMeanE/w8p+d9aCslvIbZyBBzeZ/266yqTUUoiYDzqv
Hb5q8rz7vEgr3DG4XfHYpCqfE2nttQGK3emHKGnvY239AteZkdwMpcs=
-----END PRIVATE KEY-----'''

  client_x509 = b'''-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEm0xeyy3nVnYpOpx/CV/FnlNEdWUZaqtB
AGf7flKxXEjmlSUjseYzCd566sLpNg56Gw6hcFx+rWTLGR4eDRWfmwlXhyUasuEg
mb0BQf8XJLBdvadb9eFx2CP1yjBsiy8e
-----END PUBLIC KEY-----'''

  client_public_key = serialization.load_pem_public_key(client_x509)
  server_private_key = serialization.load_pem_private_key(server_pkcs8, password=None)
  shared_secret = server_private_key.exchange(ec.ECDH(), client_public_key)
  print('Shared secret: ' + base64.b64encode(shared_secret).decode('utf8')) # Shared secret: xbU6oDHMTYj3O71liM5KEJof3/0P4HlHJ28k7qtdqU/36llCizIlOWXtj8v+IngF

  derived_key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,
    salt=None,
    info=None,
  ).derive(shared_secret) 
  print('Derived key:   ' + base64.b64encode(derived_key).decode('utf8')) # Derived key:   Yh0FkhqrT9XDQqIiSrGv5YmBjCSj9jhR5fF6HusbN1Q=

deriveKey()

具有以下输出:

Shared secret: xbU6oDHMTYj3O71liM5KEJof3/0P4HlHJ28k7qtdqU/36llCizIlOWXtj8v+IngF
Derived key:   Yh0FkhqrT9XDQqIiSrGv5YmBjCSj9jhR5fF6HusbN1Q=

对应于 WebCrypto 代码的值。