首页 > 解决方案 > .NET Core 5.0 到 Javascript DFH 密钥交换不起作用

问题描述

我们正在尝试使用 JS 获取基于浏览器的应用程序,以使用 Eliptical Curve Diffie Hellman 与 .Net Core 5.0 服务器交换密钥。我们的应用程序需要在两端共享一个秘密来进行我们所做的某些特定处理(不是加密,而是我们自己的过程),并且出于安全目的,我们希望导出该秘密而不是传输该秘密。我们已经四处寻找合适的解决方案并将各种解决方案修补在一起,我们已经想出了这个,但它失败了,在 C# 方面有一个例外。基本上,我们的伪代码如下(到目前为止,我们只到了第 3 步):

  1. 客户端 (Bob) 使用 window.crypto.subtle 库在客户端中生成一个 ECDH / P-256 密钥对。
  2. Bob 从这对导出公钥并向服务器 (Alice) 发出 GET 以获取她的公钥(将 bobPublicKeyB64 作为查询 arg 传递)。
  3. Alice 接受传入请求并调用 C# 方法以使用 Bob 的公钥创建共享密钥。
  4. Alice 将此共享秘密存储在内存缓存中,并将她的公钥返回给 Bob。
  5. 然后 Bob 使用 Alice 的公钥获取他自己的共享秘密并将其存储在浏览器的“会话存储”中。

将 Bob 的公钥发送给 Alice 的代码在这里没有显示,它是一个标准的XMLHttpRequest。但是,它确实有效,并且服务器被调用,正如 C# 代码中的故障所见证的那样。我们已经验证了BobPublicKeyB64在 Bob 发送它时和 Alice 得到它时是完全相同的。

一旦这工作起来,我们将加强两端的存储方法,但首先我们需要让交换工作。Alice (C#) 代码块中的注释显示了它失败的地方。getDerivedKey方法(现在已注释掉)来自这篇文章 - ECDH nodejs 和 C# 密钥交换 ,它失败并出现不同的异常(我确信这两个失败都是由于 JS 库和 .Net 实现之间的一些不匹配造成的,但是我们无法处理它)。非常感谢任何和所有帮助 - 基本问题是“当 Bob 是 JS 而 Alice 是 C# 时,我们如何让 Bob 和 Alice 说话?

以下是 Bob 上的 JS 代码:

async function setUpDFHKeys() {
let bobPublicKeyB64;
let bobPrivateKeyB64;
try {

    const bobKey = await window.crypto.subtle.generateKey(
        { name: 'ECDH', namedCurve: 'P-256' },
        true,
        ["deriveKey"]
    );
            
    const publicKeyData = await window.crypto.subtle.exportKey("raw", bobKey.publicKey);
    const publicKeyBytes = new Uint8Array(publicKeyData);
    const publicKeyB64 = btoa(publicKeyBytes);        
    bobPublicKeyB64 = publicKeyB64;

    const privateKeyData = await window.crypto.subtle.exportKey("pkcs8", bobKey.privateKey);
    const privateKeyBytes = new Uint8Array(privateKeyData);
    const privateKeyB64 = btoa(String.fromCharCode.apply(null, privateKeyBytes));
    bobPrivateKeyB64 = privateKeyB64;
}
catch (error) {
    console.log("Could not setup DFH Keys " + error);
}};

以下是 Alice 上的 C# 代码:

  private string WorkWithJSPublicKey(string bobPublicKeyB64, out string alicePublicKey)
    {
        try
        {
            //
            // Alice is this server.
            // Bob is a browser that uses the window.crypto.subtle.generateKey with 'ECDH' and 'P-256' as parameters
            // and window.crypto.subtle.exportKey of the key.publicKey in "raw" format (this is then converted to a B64 string using btoa)
            //
            using (ECDiffieHellman alice = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256))
            {
                //
                // Get the public key info from Bob and convert to B64 string to return to Alice
                //
                Span<byte> exported = new byte[alice.KeySize];
                int len = 0;
                alice.TryExportSubjectPublicKeyInfo(exported, out len);
                alicePublicKey = Convert.ToBase64String(exported.Slice(0, len));
                //
                // Get Alice's private key to use to generate a shared secret
                //
                byte[] alicePrivateKey = alice.ExportECPrivateKey();
                //
                // Import Bob's public key after converting it to bytes
                //
                var bobPubKeyBytes = Convert.FromBase64String(bobPublicKeyB64);
                //
                // TRY THIS... (Bombs with "The specified curve 'nistP256' or its parameters are not valid for this platform").
                //
                // getDerivedKey(bobPubKeyBytes, alice);
                //
                // This throws exception ("The provided data is tagged with 'Universal' class value '20', but it should have been 'Universal' class value '16'.")
                //
                alice.ImportSubjectPublicKeyInfo(bobPubKeyBytes, out len);
                //
                // Once Alice knows about Bob, create a shared secret and return it.
                //
                byte[] sharedSecret = alice.DeriveKeyMaterial(alice.PublicKey);
                return Convert.ToBase64String(sharedSecret);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, ex.Message);
            alicePublicKey = string.Empty;
            return string.Empty;
        }
    }

getDerivedKey的代码(借用ECDH nodejs和 C# key exchange)如下所示:

static byte[] getDerivedKey(byte[] key1, ECDiffieHellman alice)
    {
        byte[] keyX = new byte[key1.Length / 2];
        byte[] keyY = new byte[keyX.Length];
        Buffer.BlockCopy(key1, 1, keyX, 0, keyX.Length);
        Buffer.BlockCopy(key1, 1 + keyX.Length, keyY, 0, keyY.Length);
        ECParameters parameters = new ECParameters
        {
            Curve = ECCurve.NamedCurves.nistP256,
            Q = {
                    X = keyX,
                    Y = keyY,
                },
        };
        byte[] derivedKey;
        using (ECDiffieHellman bob = ECDiffieHellman.Create(parameters))
        using (ECDiffieHellmanPublicKey bobPublic = bob.PublicKey)
        {
            return derivedKey = alice.DeriveKeyFromHash(bobPublic, HashAlgorithmName.SHA256);
        }
    }

标签: javascriptc#diffie-hellman

解决方案


如果 Web Crypto 端的公钥以 X.509/SPKI 格式而不是作为原始密钥导出,则解决方案会更简单,因为 .NET 5ImportSubjectPublicKeyInfo()对此格式有专用的导入方法。此外,这与公钥也以 X.509/SPKI 格式导出的 C# 代码一致。在以下示例中,Web Crypto 代码以 X.509/SPKI 格式导出公钥。

第 1 步 - Web 加密端 (Bob):生成 EC 密钥对

以下 Web Crypto 代码创建一个 ECDH 密钥对并以 X.509/SPKI 格式导出公钥(以及 PKCS8 格式的私钥)。请注意,由于存在错误,在 Firefox 下无法以 PKCS8 格式导出,另请参见此处

setUpDFHKeys().then(() => {});

async function setUpDFHKeys() {
    var bobKey = await window.crypto.subtle.generateKey(
        { name: 'ECDH', namedCurve: 'P-256' },
        true,
        ["deriveKey"]
    );
    var publicKeyData = await window.crypto.subtle.exportKey("spki", bobKey.publicKey);
    var publicKeyBytes = new Uint8Array(publicKeyData);
    var publicKeyB64 = btoa(String.fromCharCode.apply(null, publicKeyBytes));        
    console.log("Bob's public: \n" + publicKeyB64.replace(/(.{56})/g,'$1\n'));
    var privateKeyData = await window.crypto.subtle.exportKey("pkcs8", bobKey.privateKey);
    var privateKeyBytes = new Uint8Array(privateKeyData);
    var privateKeyB64 = btoa(String.fromCharCode.apply(null, privateKeyBytes));
    console.log("Bob's private:\n" + privateKeyB64.replace(/(.{56})/g,'$1\n'));
};

一个可能的输出是下面的密钥对,作为后续课程中Web Crypto端的密钥对:

Bob's public:  MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2X9cW7P2g4Db3BEgfjs8lwpgukPY4Qg3mcLwVJW+WwA7lbiz+N1MIL3y+JumBF1qIdyx24r5+Sr4c4iYsTWh2w== 
Bob's private: MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg72fE/+7WX5aKAMiy8kTkCTVeGR8oOlKuoQ8iXTQWmxGhRANCAATZf1xbs/aDgNvcESB+OzyXCmC6Q9jhCDeZwvBUlb5bADuVuLP43UwgvfL4m6YEXWoh3LHbivn5KvhziJixNaHb

第 2 步 - C# 端 (Alice):从 Web Crypto 端导入公钥并生成共享密钥

以下 C# 代码生成一个 ECDH 密钥对,导出 X.509/SPKI 格式的公钥(以及 PKCS8 格式的私钥)并确定共享密钥。为了获取共享密钥,Web Crypto 端的 X.509/SPKI 格式的公钥使用ImportSubjectPublicKeyInfo().

string alicePublicKey;
string bobPublicKeyB64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2X9cW7P2g4Db3BEgfjs8lwpgukPY4Qg3mcLwVJW+WwA7lbiz+N1MIL3y+JumBF1qIdyx24r5+Sr4c4iYsTWh2w==";
string sharedSecret = WorkWithJSPublicKey(bobPublicKeyB64, out alicePublicKey);
Console.WriteLine("Alice's shared secret: " + sharedSecret);

private static string WorkWithJSPublicKey(string bobPublicKeyB64, out string alicePublicKey)
{
    alicePublicKey = null;
    using (ECDiffieHellman alice = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256))
    {
        alicePublicKey = Convert.ToBase64String(alice.ExportSubjectPublicKeyInfo());
        Console.WriteLine("Alice's public:        " + alicePublicKey);
        Console.WriteLine("Alice's private:       " + Convert.ToBase64String(alice.ExportPkcs8PrivateKey()));
        ECDiffieHellman bob = ECDiffieHellman.Create();
        bob.ImportSubjectPublicKeyInfo(Convert.FromBase64String(bobPublicKeyB64), out _);
        byte[] sharedSecret = alice.DeriveKeyMaterial(bob.PublicKey);
        return Convert.ToBase64String(sharedSecret);
    }           
}

可能的输出是以下密钥对和共享密钥。该密钥对作为后续课程中C#端的密钥对:

Alice's public:        MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJVUW57L2QeswZhnIp5gjMSiHhqyOVTsPUq2QwHv+R4jQetMQ8JDT+3VQyP/dPpskUhzDd3lKxdRBaiZrWby+VQ==
Alice's private:       MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgbho81UNFdNwULs7IoWk1wSy2PP9soSlt4/bveAtoPBOhRANCAAQlVRbnsvZB6zBmGcinmCMxKIeGrI5VOw9SrZDAe/5HiNB60xDwkNP7dVDI/90+myRSHMN3eUrF1EFqJmtZvL5V
Alice's shared secret: hayYCAA23oC98d1SxhFpfiYgY5DVElmEno4851HtgKM=
    

第 3 步 - Web Crypto 端 (Bob):从 C# 端导入公钥并生成共享密钥

以下 Web Crypto 代码创建共享密钥。为此,必须导入 C# 端的公钥。请注意(类似于导出)PKCS8 格式的导入在Firefox 下由于错误而无法工作:

getSharedSecret().then(() => {});

async function getSharedSecret() {      
    var bobPrivateKeyB64 = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg72fE/+7WX5aKAMiy8kTkCTVeGR8oOlKuoQ8iXTQWmxGhRANCAATZf1xbs/aDgNvcESB+OzyXCmC6Q9jhCDeZwvBUlb5bADuVuLP43UwgvfL4m6YEXWoh3LHbivn5KvhziJixNaHb';
    var alicePublicKeyB64 = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJVUW57L2QeswZhnIp5gjMSiHhqyOVTsPUq2QwHv+R4jQetMQ8JDT+3VQyP/dPpskUhzDd3lKxdRBaiZrWby+VQ==';     
    var privateKey = await window.crypto.subtle.importKey(
        "pkcs8", 
        new Uint8Array(_base64ToArrayBuffer(bobPrivateKeyB64)),
        { name: "ECDH", namedCurve: "P-256" },
        true, 
        ["deriveKey", "deriveBits"] 
    );
    var publicKey = await window.crypto.subtle.importKey(
        "spki", 
        new Uint8Array(_base64ToArrayBuffer(alicePublicKeyB64)),
       { name: "ECDH", namedCurve: "P-256"},
       true, 
       [] 
    );
    var sharedSecret = await window.crypto.subtle.deriveBits(
        { name: "ECDH", namedCurve: "P-256", public: publicKey },
        privateKey, 
       256 
    );
    var sharedSecretHash = await crypto.subtle.digest('SHA-256', sharedSecret);
    var sharedSecretHashB64 = btoa(String.fromCharCode.apply(null, new Uint8Array(sharedSecretHash)));
    console.log("Bob's shared secret: " + sharedSecretHashB64.replace(/(.{64})/g,'$1\n'));
};

// from https://stackoverflow.com/a/21797381/9014097
function _base64ToArrayBuffer(base64) {
    var binary_string = window.atob(base64);
    var len = binary_string.length;
    var bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++) {
        bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes.buffer;
}

在 Web Crypto 方面,这会产生共享密钥:

Bob's shared secret: hayYCAA23oC98d1SxhFpfiYgY5DVElmEno4851HtgKM=

按照C#端。


请注意,DeriveKeyMaterial()在 C# 端不返回实际的共享密钥 S,而是共享密钥的 SHA-256 哈希 H(S)。由于哈希是不可逆的,因此无法确定实际的共享秘密。因此,唯一的选择是通过使用 SHA-256 的显式散列在 Web Crypto 端创建 H(S),另请参见此处


推荐阅读