Skip to content

Commit

Permalink
Add support for ECDH-DS algorithms (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
kelvinmo authored Jan 24, 2022
1 parent ff46458 commit 6185874
Show file tree
Hide file tree
Showing 14 changed files with 1,046 additions and 42 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to this project will be documented in this file.

## Unreleased

- Added: Support for Elliptic Curve Diffie-Hellman Ephemeral Static algorithms

## Version 0.5.3

- Fixed: typos in documentation leading to deprecation error (#39)
Expand All @@ -20,6 +24,8 @@ All notable changes to this project will be documented in this file.
## Version 0.5.0

- Added: Support for AES GCM family of algorithms
- Added: Support for Elliptic Curve Diffie-Hellman key derivation
algorithm
- Changed: SimpleJWT\JWT::decode() no longer supports $format parameter
(format is automatically detected)
- Changed: SimpleJWT\JWT::deserialise() no longer supports $format parameter
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,22 @@ SimpleJWT is a simple JSON web token library written in PHP.
* RSAES with OAEP (RSA-OAEP, RSA-OAEP-256)
* AES key wrap (A128KW, A192KW, A256KW)
* PBES2 (PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW)
* Elliptic Curve Diffie-Hellman (ECDH-ES) - requires PHP 7.3 or later
- Content encryption algorithms
* AES_CBC_HMAC_SHA2 family (A128CBC-HS256, A192CBC-HS384, A256CBC-HS512)
* AES GCM family (A128GCM, A192GCM, A256GCM) - requires PHP 7.1 or later

## Requirements

- PHP 7.1.0 or later
- `gmp` extension
- `hash` extension
- `openssl` extension

A working `openssl.cnf` configuration is also required if the `ECDH-ES`
algorithm is used. See the [PHP manual](https://www.php.net/manual/en/openssl.installation.php)
for details.

## Installation

You can install via [Composer](http://getcomposer.org/).
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"php": "^7.1 || ^8.0",
"ext-openssl": "*",
"ext-hash": "*",
"ext-gmp": "*",
"symfony/console": "^4.0 || ^5.0"
},
"require-dev": {
Expand Down
116 changes: 116 additions & 0 deletions src/SimpleJWT/Crypt/AESWrappedKeyAlgorithm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php
/*
* SimpleJWT
*
* Copyright (C) Kelvin Mo 2020
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

namespace SimpleJWT\Crypt;

use SimpleJWT\Keys\Key;
use SimpleJWT\Keys\KeySet;
use SimpleJWT\Keys\SymmetricKey;

/**
* Abstract class for the AES Key Wrap encryption algorithm, where the key to be wrapped
* is derived from a key derivation algorithm.
*
* This is a convenience class for algorithms implementing the `x+AyyyKW` family of
* algorithms. Subclasses can call methods in this class for the AES Key Wrap functions.
*/
abstract class AESWrappedKeyAlgorithm extends Algorithm implements KeyEncryptionAlgorithm {
/** @var AESKeyWrap the underlying AES key wrap algorithm */
private $aeskw;

public function __construct($alg) {
if ($alg == null) {
$this->aeskw = new AESKeyWrap(null);
} else {
list($dummy, $aeskw_alg) = explode('+', $alg, 2);
$this->aeskw = new AESKeyWrap($aeskw_alg);
}

parent::__construct($alg);
}

/**
* Returns the supported AES Key Wrap algorithms
*
* @return array an array of AES Key Wrap algorithms
*/
protected function getAESKWAlgs() {
return $this->aeskw->getSupportedAlgs();
}

/**
* Returns the required key size for the AES key wrap key
*
* @return int the key size, in bits
*/
protected function getAESKWKeySize() {
$criteria = $this->aeskw->getKeyCriteria();
return $criteria[Key::SIZE_PROPERTY];
}

/**
* Wraps a key using the AES Key Wrap algorithm
*
* @param string $plain_key the key to wrap as a binary string
* @param string $wrapping_key the key wrapping key as a binary string
* @param array &$headers the JWE header, which can be modified
* @return string the wrapped key as a binary string
*/
protected function wrapKey($plain_key, $wrapping_key, &$headers) {
$keys = $this->createKeySet($wrapping_key);
return $this->aeskw->encryptKey($plain_key, $keys, $headers);
}

/**
* Unwraps a key using the AES Key Wrap algorithm
*
* @param string $encrypted_key the key to unwrap as a binary string
* @param string $unwrapping_key the key wrapping key as a binary string
* @param array $headers the JWE header, which can be modified
* @return string the unwrapped key as a binary string
*/
protected function unwrapKey($encrypted_key, $unwrapping_key, $headers) {
$keys = $this->createKeySet($unwrapping_key);
return $this->aeskw->decryptKey($encrypted_key, $keys, $headers);
}

private function createKeySet($key) {
$keys = new KeySet();
$keys->add(new SymmetricKey($key, 'bin'));
return $keys;
}
}

?>
2 changes: 2 additions & 0 deletions src/SimpleJWT/Crypt/AlgorithmFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class AlgorithmFactory {
'/^RSA-OAEP-256$/' => 'SimpleJWT\Crypt\RSAES',
'/^A\d+KW$/' => 'SimpleJWT\Crypt\AESKeyWrap',
'/^PBES2-HS\d+\\+A\d+KW$/' => 'SimpleJWT\Crypt\PBES2',
'/^ECDH-ES$/' => 'SimpleJWT\Crypt\ECDH',
'/^ECDH-ES\\+A\d+KW$/' => 'SimpleJWT\Crypt\ECDH_AESKeyWrap',

// Content encryption algorithms
'/^A\d+CBC-HS\d+$/' => 'SimpleJWT\Crypt\AESCBC_HMACSHA2',
Expand Down
203 changes: 203 additions & 0 deletions src/SimpleJWT/Crypt/ECDH.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
<?php
/*
* SimpleJWT
*
* Copyright (C) Kelvin Mo 2020
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

namespace SimpleJWT\Crypt;

use SimpleJWT\Keys\ECKey;
use SimpleJWT\Keys\KeyFactory;
use SimpleJWT\Util\Util;

/**
* Implementation of the Elliptic Curve Diffie-Hellman
* Ephemeral Static algorithm.
*
* @see https://tools.ietf.org/html/rfc7518#section-4.6
*/
class ECDH extends Algorithm implements KeyDerivationAlgorithm {
private $default_key_size;

public function __construct($alg, $default_key_size = null) {
parent::__construct($alg);
$this->default_key_size = $default_key_size;
}

public function getSupportedAlgs() {
if (defined('OPENSSL_KEYTYPE_EC') && function_exists('openssl_pkey_derive')) {
// openssl_pkey_derive is made available from PHP 7.3?
return ['ECDH-ES'];
} else {
// Not supported
return [];
}
}

public function getKeyCriteria() {
return ['kty' => 'EC', '@use' => 'enc', '@key_ops' => 'deriveKey'];
}

public function deriveKey($keys, &$headers, $kid = null) {
$key = $this->selectKey($keys, $kid);
if ($key == null) {
throw new CryptException('Key not found or is invalid');
}

// 1. Get the required key length and alg input into Concat KDF
if (isset($headers['enc'])) {
try {
$enc = AlgorithmFactory::create($headers['enc'], Algorithm::ENCRYPTION_ALGORITHM);
$size = $enc->getCEKSize();
} catch (\UnexpectedValueException $e) {
throw new CryptException('Unexpected enc algorithm', $e);
}
} elseif ($this->default_key_size != null) {
$size = $this->default_key_size;
} else {
throw new CryptException('Key size not specified');
}

if ($this->getAlg() == 'ECDH-ES') {
$alg = $headers['enc'];
} else {
$alg = $this->getAlg();
}

// 2. If 'epk' header is present, check the ephemeral public key for compatibility
// against (our) private key specified in $key
//
// Otherwise, generate the ephemeral public key based on the recipient's public
// key specified in $key
if (isset($headers['epk'])) {
// (a) Load the ephemeral public key
$ephemeral_public_key = KeyFactory::create($headers['epk'], 'php');
if (!($ephemeral_public_key instanceof ECKey)) {
throw new CryptException("Invalid epk: not an EC key");
}

// (b) Check that $key is a private key
if ($key->isPublic()) {
throw new CryptException('Key is a public key; private key expected');
}

// (c) Check whether the epk is on the private key's curve to mitigate
// against invalid curve attacks
if (!$key->isOnSameCurve($ephemeral_public_key)) {
throw new CryptException('Invalid epk: incompatible curve');
}

// (d) Set the ECDH keys
$dh_public_key = $ephemeral_public_key;
$dh_private_key = $key;
} else {
// (a) Check that $key is a public key (i.e. the recipient's)
if (!$key->isPublic()) {
throw new CryptException('Key is a private key; public key expected');
}

// (b) Create an ephemeral key pair with the same curve as the recipient's public
// key, then set the epk header
$crv = $key->getCurve();
$ephemeral_private_key = $this->createEphemeralKey($crv);
$ephemeral_public_key = $ephemeral_private_key->getPublicKey();
$headers['epk'] = $ephemeral_public_key->getKeyData();

// (c) Set the ECDH keys
$dh_public_key = $key;
$dh_private_key = $ephemeral_private_key;
}

// 3. Calculate agreement key (Z)
$Z = $this->deriveAgreementKey($dh_public_key, $dh_private_key);

// 4. Derive key from Concat KDF
$apu = (isset($headers['apu'])) ? $headers['apu'] : '';
$apv = (isset($headers['apv'])) ? $headers['apv'] : '';
return $this->concatKDF($Z, $alg, $size, $apu, $apv);
}

protected function createEphemeralKey($crv) {
if (!isset(ECKey::$curves[$crv])) throw new \InvalidArgumentException('Curve not found');
$openssl_curve_name = ECKey::$curves[$crv]['openssl'];

if (!in_array($openssl_curve_name, openssl_get_curve_names()))
throw new CryptException('Unable to create ephemeral key: unsupported curve');

// Note openssl.cnf needs to be correctly configured for this to work.
// See https://www.php.net/manual/en/openssl.installation.php for the
// appropriate location of this configuration file
$pkey = openssl_pkey_new([
'curve_name' => $openssl_curve_name,
'private_key_type' => OPENSSL_KEYTYPE_EC,
]);
if ($pkey === false) throw new CryptException('Unable to create ephemeral key (is openssl.cnf missing?)');

// Note openssl.cnf needs to be correctly configured for this to work.
// See https://www.php.net/manual/en/openssl.installation.php for the
// appropriate location of this configuration file
$result = openssl_pkey_export($pkey, $pem);
if ($result === false) throw new CryptException('Unable to create ephemeral key');

return new ECKey($pem, 'pem');
}

private function deriveAgreementKey($public_key, $private_key) {
assert(function_exists('openssl_pkey_derive'));

$public_key_res = openssl_pkey_get_public($public_key->toPEM());
if ($public_key_res === false) throw new CryptException('Public key load error: ' . openssl_error_string());

$private_key_res = openssl_pkey_get_private($private_key->toPEM());
if ($private_key_res === false) throw new CryptException('Private key load error: ' . openssl_error_string());

$result = openssl_pkey_derive($public_key_res, $private_key_res);
if ($result === false) throw new CryptException('Key agreement error: ' . openssl_error_string());
return $result;
}

private function concatKDF($Z, $alg, $size, $apu = '', $apv = '') {
$apu = ($apu == null) ? '' : Util::base64url_decode($apu);
$apv = ($apv == null) ? '' : Util::base64url_decode($apv);

$input = pack('N', 1)
. $Z
. pack('N', strlen($alg)) . $alg
. pack('N', strlen($apu)) . $apu
. pack('N', strlen($apv)) . $apv
. pack('N', $size);

return substr(hash('sha256', $input, true), 0, $size / 8);
}
}

?>
Loading

0 comments on commit 6185874

Please sign in to comment.