phpencryptiondiffie-hellman

openssl_dh_compute_key(): Argument #2 ($private_key) must be of type OpenSSLAsymmetricKey


I am trying to decrypt a response Tokens which where encrypted using a Diffie-Hellman (type curve25519) via AES-GCM and additional authenticated data (AAD). I am new to PHP so I am not sure how can this be accomplished.

here is my attempt:

<?php


$private_key =  openssl_pkey_get_private("file:///vault-sink/private");

$pub_key_struct =  file_get_contents("/vault-sink/public");
$json_public_key = json_decode($pub_key_struct);


$secret = openssl_dh_compute_key($json_public_key->curve25519_public_key, $private_key);

$token_response =  file_get_contents("/vault-sink/token.txt");
$token_response_json = json_decode($token_response);

echo $token_response_json->encrypted_payload;

openssl_decrypt(
    $secret,
    $token_response_json->encrypted_payload,
    $token_response_json->nonce,
    $aad = "test"
);

error

PHP Fatal error:  Uncaught TypeError: openssl_dh_compute_key(): Argument #2 ($private_key) must be of type OpenSSLAsymmetricKey, bool given in /vault-sink/php-client/main.php:10

content of public and private keys:

# cat public
{"curve25519_public_key":"ru7+Ncx9x8Y/pHlj3D/wg+WYCQgqMKuVpAmTjCmf7Hs="}

# cat private
9fd06c52f7fda2b89bb05ac591dbeba8e57984c2bb0e6181ddcf275fb87015b6

So my guess is that the private's key format is not readable by openssl_pkey_get_private function. My question is how can I use these private and public keys in openssl_dh_compute_key?

For more context, below is an example on how I did it with go.


Working example with Go

Generate KeyPair

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"

    dh "github.com/hashicorp/vault/helper/dhutil"
)

func check(e error) {
    if e != nil {
        log.Fatal(e)
    }
}

func generatePublicPrivateKey() {

    var keyinfo dh.PublicKeyInfo

    public, private, err := dh.GeneratePublicPrivateKey()
    check(err)

    keyinfo.Curve25519PublicKey = public
    pubkey, err := json.Marshal(keyinfo)
    check(err)

    err = os.WriteFile("public", []byte(pubkey), 0644)
    if err != nil {
        log.Fatal(err)
    }
    privateKey := fmt.Sprintf("%x", private)

    err = os.WriteFile("private", []byte(privateKey), 0644)
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
    generatePublicPrivateKey()
}

output

# cat public
{"curve25519_public_key":"ru7+Ncx9x8Y/pHlj3D/wg+WYCQgqMKuVpAmTjCmf7Hs="}

# cat private
9fd06c52f7fda2b89bb05ac591dbeba8e57984c2bb0e6181ddcf275fb87015b6

Encrypt Token Using Hashicorp Vault

then I run Vault Agent and get the following response ("token.txt"):

{
    "curve25519_public_key": "ONwU5iknsVP57p7PdHtN4rzxbivMB4Bt5o2BJFC6oSc=",
    "nonce": "AA2VibL5SXE9MU19",
    "encrypted_payload": "pPseQIue1IXxutEKoZrjsORj+8AZVgRrcTRvaPVxukzU2w28TL4T0be7aGFKZmHwudiHiNQyp5i8D0ZUgP/ILLYPfhO+gwxUFEDhA4PJNAgKc8nSaMpjG9RipAyRcepgk42SXuRIgZ1D7HrmeWT8"
}

for more info, check https://www.vaultproject.io/docs/agent/autoauth#encrypting-tokens

Decrypt the token

and then I decrypt the response as following

package main

import (
    "encoding/hex"
    "encoding/json"
    "fmt"
    "log"
    "os"

    dh "github.com/hashicorp/vault/helper/dhutil"
)

func check(e error) {
    if e != nil {
        log.Fatal(e)
    }
}

func main() {

    var envelope dh.Envelope
    var keyinfo dh.PublicKeyInfo

    token_env, err := os.ReadFile("token.txt")
    check(err)

    err = json.Unmarshal(token_env, &envelope)
    check(err)

    ourPublic, err := os.ReadFile("public")
    check(err)

    ourPrivate, err := os.ReadFile("private")
    check(err)

    json.Unmarshal(ourPublic, &keyinfo)

    privatekey, err := hex.DecodeString(string(ourPrivate))
    check(err)

    secret, err := dh.GenerateSharedSecret(privatekey, envelope.Curve25519PublicKey)
    check(err)

    shared_key, err := dh.DeriveSharedKey(secret, keyinfo.Curve25519PublicKey, envelope.Curve25519PublicKey)
    check(err)

    test, err := dh.DecryptAES(shared_key, envelope.EncryptedPayload, envelope.Nonce, []byte("test"))
    check(err)

    fmt.Println(string(test))
}

output

hvs.CAESIB1eP79cHXPc4ttZIz20qL2I2A8kcfNpkhPjvIE4DZt3Gh4KHGh2cy4wNWV1WHB5NWNuZXV1dDMzdkh4YkY2OUE

Solution

  • The shared secret can be determined in PHP with the Sodium library and the sodium_crypto_scalarmult() function.

    For the calculation of the shared key the Go source code of DeriveSharedKey() shows that the shared key is derived with HKDF (see hkdf.go) and the two public keys are sorted and used as salt and info. For this purpose hash_hkdf() can be used on the PHP side.

    Finally, decryption takes place with AES-256 in GCM mode, e.g. with openssl_encrypt(). In contrast to the Go code, where ciphertext and tag are concatenated, openssl_encrypt() handles both separately.

    The resulting PHP code is (for simplicity, the input data is assigned directly, i.e. without file I/O):

    // Input data
    $payload = base64_decode("pPseQIue1IXxutEKoZrjsORj+8AZVgRrcTRvaPVxukzU2w28TL4T0be7aGFKZmHwudiHiNQyp5i8D0ZUgP/ILLYPfhO+gwxUFEDhA4PJNAgKc8nSaMpjG9RipAyRcepgk42SXuRIgZ1D7HrmeWT8");
    $nonce = base64_decode("AA2VibL5SXE9MU19");
    $ourPublicKey = base64_decode("ru7+Ncx9x8Y/pHlj3D/wg+WYCQgqMKuVpAmTjCmf7Hs=");
    $theirPublicKey = base64_decode("ONwU5iknsVP57p7PdHtN4rzxbivMB4Bt5o2BJFC6oSc=");
    $privateKey = hex2bin("9fd06c52f7fda2b89bb05ac591dbeba8e57984c2bb0e6181ddcf275fb87015b6");
    
    // Get shared secret
    $sharedSecret = sodium_crypto_scalarmult($privateKey, $theirPublicKey);
    print("Shared secret (hex): " . sodium_bin2hex($sharedSecret) . PHP_EOL); // Shared secret (hex): 48cb642fe6ecd7e4ff4a58610524f873e8ab86b8ccb195f0c90d59c477d6f437
    
    // Get shared key
    // Go:  hkdf.New(hash, secret, salt, info)
    // PHP: hash_hkdf($algo, $key, $length, $info, $salt)
    if (strcmp($ourPublicKey, $theirPublicKey) == -1) {
        $salt = $ourPublicKey;
        $info = $theirPublicKey;
    } else { // for simplicity the 0 case is not considered dedicated, the Go code throws an exception here
        $salt = $theirPublicKey;
        $info = $ourPublicKey;
    }
    $sharedKey = hash_hkdf("sha256", $sharedSecret, 32, $info, $salt);
    print("Shared key (hex): " . bin2hex($sharedKey) . PHP_EOL); // Shared key (hex): cacbda4874426b1208903d24378bdc0fb7a7dd08c91b5f275c11ff39e58add38
    
    // Decrypt with AES-256, GCM
    $ciphertext = substr($payload, 0, strlen($payload) - 16);
    $tag = substr($payload, strlen($payload) - 16, 16);
    $aad = "test";
    $plaintext = openssl_encrypt($ciphertext, "aes-256-gcm", $sharedKey, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
    print("Plaintext: " . $plaintext . PHP_EOL); // Plaintext: hvs.CAESIB1eP79cHXPc4ttZIz20qL2I2A8kcfNpkhPjvIE4DZt3Gh4KHGh2cy4wNWV1WHB5NWNuZXV1dDMzdkh4YkY2OUE
    

    with the same output as the Go code.