Introduce PQ support (Dilithium5 functional)

This commit is contained in:
Petr Muzikant 2023-06-26 11:08:56 +02:00
parent 656a46ae12
commit 2a4e457693
14 changed files with 585 additions and 2 deletions

4
phpseclib/Crypt/Common/AsymmetricKey.php Normal file → Executable file
View File

@ -116,7 +116,7 @@ abstract class AsymmetricKey
}
self::loadPlugins('Keys');
if (static::ALGORITHM != 'RSA' && static::ALGORITHM != 'DH') {
if (static::ALGORITHM != 'RSA' && static::ALGORITHM != 'DH' && static::ALGORITHM != 'DILITHIUM') {
self::loadPlugins('Signature');
}
}
@ -213,7 +213,7 @@ abstract class AsymmetricKey
*
* @return static
*/
public static function loadFormat(string $type, string $key, ?string $password = null): AsymmetricKey
public static function loadFormat(string $type, string|array $key, ?string $password = null): AsymmetricKey
{
self::initialize_static_variables();

60
phpseclib/Crypt/DILITHIUM.php Executable file
View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt;
use phpseclib3\Crypt\Common\AsymmetricKey;
use phpseclib3\Crypt\DILITHIUM\PrivateKey;
use phpseclib3\Crypt\DILITHIUM\PublicKey;
use phpseclib3\Exception\UnsupportedAlgorithmException;
abstract class DILITHIUM extends AsymmetricKey
{
public const ALGORITHM = 'DILITHIUM';
protected string $pem;
protected string $raw_bit_string;
protected string $method_name;
protected function __construct()
{
parent::__construct();
$this->hash = new Hash('sha512');
exec(
"openssl list -providers",
$output,
$return
);
$has_oqsprovider = array_search('oqsprovider', array_map('trim', $output)) !== false;
// set proper engine
self::$engines = [
'oqsphp' => extension_loaded('oqsphp'),
'PQC-OpenSSL' => $has_oqsprovider
];
}
protected static function onLoad(array $components)
{
$key = $components['isPublicKey'] ?
new PublicKey() :
new PrivateKey();
$key->method_name = $components['method_name'];
$key->pem = $components['pem'];
$key->raw_bit_string = $components['raw'];
return $key;
}
public function withHash(string $hash): AsymmetricKey
{
if ($hash != 'sha512') {
throw new UnsupportedAlgorithmException('Post-quantum algorithm only supports sha512 as a hash');
}
return parent::withHash($hash);
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt\DILITHIUM\Formats\Keys;
use OQS_SIGNATURE;
use phpseclib3\Exception\UnexpectedValueException;
use phpseclib3\File\ASN1;
use phpseclib3\File\ASN1\Maps\PrivateKeyInfo;
use phpseclib3\File\ASN1\Maps\PublicKeyInfo;
abstract class PEM
{
public static function load($array, ?string $password = null): array
{
$key_pem = $array['key_pem'];
$method_name = $array['method_name'];
$components = [
'isPublicKey' => str_contains($key_pem, 'PUBLIC'),
'isPrivateKey' => str_contains($key_pem, 'PRIVATE'),
'method_name' => $method_name,
];
if (!$components['isPublicKey'] && !$components['isPrivateKey']) {
throw new UnexpectedValueException('Key should be a PEM formatted string');
}
$components['pem'] = $key_pem;
// extract raw key
$extractedBER = ASN1::extractBER($key_pem);
$decodedBER = ASN1::decodeBER($extractedBER);
if ($components['isPrivateKey']) {
$raw_key = ASN1::asn1map($decodedBER[0], PrivateKeyInfo::MAP)['privateKey'];
$sig = new OQS_SIGNATURE($method_name);
$max_length = $sig->length_private_key;
// PQC-OpenSSL encodes privates keys as 0x04 or 0x03 || length || private_key || public_key
// We need to extract private_key only
if (strlen($raw_key) > $max_length) {
$bytearray = unpack('c*', $raw_key);
$offset = 0;
// if it still has ASN1 type and length
if ($bytearray[1] == 0x04 || $bytearray[1] == 0x03) {
// 0x80 indicates that second byte encodes number of bytes containing length
$len_bytes = ($bytearray[2] & 0x80) == 0x80 ? 1 + ($bytearray[2] & 0x7f) : 1;
// 1 is for type 0x04 or 0x03, rest is length_bytes
$offset = 1 + $len_bytes;
}
$private_key_raw = pack('c*', ...array_slice($bytearray, $offset, $max_length));
}
$components['raw'] = $private_key_raw;
} else if ($components['isPublicKey']) {
$raw_key = ASN1::asn1map($decodedBER[0], PublicKeyInfo::MAP)['publicKey'];
// Check if first byte in string is 0
// If it is, it means that the public key is encoded as a positive integer and we need to remove first byte in order to extract the public key
// If it is not, it means that the public key is encoded as a bit string - TODO: fact check this
if (unpack('c', $raw_key)[1] === 0) {
// Remove first byte from bit string
$raw_key = pack('c*', ...array_slice(unpack('c*', $raw_key), 1));
}
$components['raw'] = $raw_key;
}
return $components;
}
}

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt\DILITHIUM;
use phpseclib3\Crypt\DILITHIUM;
use phpseclib3\Crypt\Common;
use phpseclib3\Exception\RuntimeException;
use phpseclib3\File\ASN1;
use phpseclib3\File\ASN1\Maps\PrivateKeyInfo;
final class PrivateKey extends DILITHIUM implements Common\PrivateKey
{
use Common\Traits\PasswordProtected;
public function sign($message)
{
if (self::$engines["PQC-OpenSSL"] && !empty($this->pem)) {
if (openssl_sign($message, $signature, $this->pem, $this->hash->getHash())) {
return $signature;
} else {
throw new RuntimeException("openssl_sign failed: " . openssl_error_string());
}
} else if (self::$engines['oqsphp'] && !empty($this->raw_bit_string)) {
$signature = '';
$oqs_signature = new OQS_SIGNATURE($this->method_name);
if (OQS_SUCCESS === $oqs_signature->sign($signature, $this->hash->hash($message), $this->raw_bit_string)) {
return $signature;
} else {
throw new RuntimeException("OQS_SIGNATURE->sign failed");
}
} else {
throw new RuntimeException("No engine available");
}
}
public function getPublicKey(): string
{
if (self::$engines["PQC-OpenSSL"] && !empty($this->pem)) {
$private_key = openssl_pkey_get_private($this->pem);
$private_key_details = openssl_pkey_get_details($private_key);
return $private_key_details['key'];
} else if (self::$engines['oqsphp'] && !empty($this->pem)) {
$extractedBER = ASN1::extractBER($this->pem);
$decodedBER = ASN1::decodeBER($extractedBER);
$private_key_raw = ASN1::asn1map($decodedBER[0], PrivateKeyInfo::MAP)['privateKey'];
$sig = new OQS_SIGNATURE($this->method_name);
$public_key_length = $sig->length_public_key;
$private_key_length = $sig->length_private_key;
// PQC-OpenSSL encodes privates keys as 0x04 or 0x03 || length || private_key || public_key
// We need to extract public_key only
if (strlen($private_key_raw) >= ($private_key_length + $public_key_length)) {
$bytearray = unpack('c*', $private_key_raw);
$offset = $private_key_length;
// if it still has ASN1 type and length
if ($bytearray[1] == 0x04 || $bytearray[1] == 0x03) {
// 0x80 indicates that second byte encodes number of bytes containing length
$len_bytes = ($bytearray[2] & 0x80) == 0x80 ? 1 + ($bytearray[2] & 0x7f) : 1;
// 1 is for type 0x04 or 0x03, rest is length_bytes
$offset = 1 + $len_bytes + $private_key_length;
}
return pack('c*', ...array_slice($bytearray, $offset, $public_key_length));
} else {
throw new RuntimeException("Could not extract public key from private key");
}
} else {
throw new RuntimeException("No engine available");
}
}
public function toString(string $type, array $options = []): string
{
if ((self::$engines["PQC-OpenSSL"] && self::$engines['oqsphp']) && !empty($this->pem)) {
return $this->pem;
} else {
throw new RuntimeException("No data available");
}
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt\DILITHIUM;
use phpseclib3\Crypt\Common;
use phpseclib3\Crypt\DILITHIUM;
use phpseclib3\Exception\RuntimeException;
final class PublicKey extends DILITHIUM implements Common\PublicKey
{
use Common\Traits\Fingerprint;
public function verify($message, $signature)
{
if (self::$engines["PQC-OpenSSL"] && !empty($this->pem)) {
$result = openssl_verify($message, $signature, $this->pem, $this->hash->getHash());
if ($result === false) {
throw new RuntimeException("openssl_verify failed: " . openssl_error_string());
} else {
return boolval($result);
}
} else if (self::$engines['oqsphp'] && !empty($this->raw_bit_string)) {
$oqs_signature = new \OQS_SIGNATURE($this->method_name);
return \OQS_SUCCESS === $oqs_signature->verify($this->hash->hash($message), $signature, $this->raw_bit_string);
} else {
throw new RuntimeException("No engine available");
}
}
public function toString(string $type, array $options = []): string
{
if ((self::$engines["PQC-OpenSSL"] || self::$engines['oqsphp']) && !empty($this->pem)) {
return $this->pem;
} else {
throw new RuntimeException("No data available");
}
}
}

45
phpseclib/Crypt/FALCON.php Executable file
View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt;
use phpseclib3\Crypt\Common\AsymmetricKey;
use phpseclib3\Crypt\FALCON\PrivateKey;
use phpseclib3\Crypt\FALCON\PublicKey;
use phpseclib3\Exception\UnsupportedAlgorithmException;
abstract class FALCON extends AsymmetricKey
{
public const ALGORITHM = 'FALCON';
protected string $pem;
protected function __construct()
{
parent::__construct();
$this->hash = new Hash('sha512');
}
protected static function onLoad(array $components)
{
$key = $components['isPublicKey'] ?
new PublicKey() :
new PrivateKey();
if (isset($components['pem'])) {
$key->pem = $components['pem'];
}
return $key;
}
public function withHash(string $hash): AsymmetricKey
{
if ($hash != 'sha512') {
throw new UnsupportedAlgorithmException('Post-quantum algorithm only supports sha512 as a hash');
}
return parent::withHash($hash);
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt\FALCON\Formats\Keys;
use phpseclib3\Exception\UnexpectedValueException;
abstract class PEM
{
public static function load($key, ?string $password = null): array
{
$components = [
'isPublicKey' => str_contains($key, 'PUBLIC'),
'isPrivateKey' => str_contains($key, 'PRIVATE')
];
if (!isset($components['isPublicKey']) && !isset($components['isPrivateKey'])) {
throw new UnexpectedValueException('Key should be a PEM formatted string');
}
$components['pem'] = $key;
return $components;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt\FALCON;
use phpseclib3\Crypt\FALCON;
use phpseclib3\Crypt\Common;
use phpseclib3\Exception\RuntimeException;
final class PrivateKey extends FALCON implements Common\PrivateKey
{
use Common\Traits\PasswordProtected;
public function sign($message)
{
if ($result = openssl_sign($message, $signature, $this->pem, $this->hash->getHash())) {
return $signature;
} else {
throw new RuntimeException("openssl_sign failed: " . openssl_error_string());
}
}
public function getPublicKey(): string
{
$private_key = openssl_pkey_get_private($this->pem);
$private_key_details = openssl_pkey_get_details($private_key);
return $private_key_details['key'];
}
public function toString(string $type, array $options = []): string
{
return $this->pem;
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt\FALCON;
use phpseclib3\Crypt\FALCON;
use phpseclib3\Crypt\Common;
use phpseclib3\Exception\RuntimeException;
final class PublicKey extends FALCON implements Common\PublicKey
{
use Common\Traits\Fingerprint;
public function verify($message, $signature)
{
$result = openssl_verify($message, $signature, $this->pem, $this->hash->getHash());
if ($result === false) {
throw new RuntimeException("openssl_verify failed: " . openssl_error_string());
} else {
return boolval($result);
}
}
public function toString(string $type, array $options = []): string
{
return $this->pem;
}
}

45
phpseclib/Crypt/SPHINCS.php Executable file
View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt;
use phpseclib3\Crypt\Common\AsymmetricKey;
use phpseclib3\Crypt\SPHINCS\PrivateKey;
use phpseclib3\Crypt\SPHINCS\PublicKey;
use phpseclib3\Exception\UnsupportedAlgorithmException;
abstract class SPHINCS extends AsymmetricKey
{
public const ALGORITHM = 'SPHINCS';
protected string $pem;
protected function __construct()
{
parent::__construct();
$this->hash = new Hash('sha512');
}
protected static function onLoad(array $components)
{
$key = $components['isPublicKey'] ?
new PublicKey() :
new PrivateKey();
if (isset($components['pem'])) {
$key->pem = $components['pem'];
}
return $key;
}
public function withHash(string $hash): AsymmetricKey
{
if ($hash != 'sha512') {
throw new UnsupportedAlgorithmException('Post-quantum algorithm only supports sha512 as a hash');
}
return parent::withHash($hash);
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt\SPHINCS\Formats\Keys;
use phpseclib3\Exception\UnexpectedValueException;
abstract class PEM
{
public static function load($key, ?string $password = null): array
{
$components = [
'isPublicKey' => str_contains($key, 'PUBLIC'),
'isPrivateKey' => str_contains($key, 'PRIVATE')
];
if (!isset($components['isPublicKey']) && !isset($components['isPrivateKey'])) {
throw new UnexpectedValueException('Key should be a PEM formatted string');
}
$components['pem'] = $key;
return $components;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt\SPHINCS;
use phpseclib3\Crypt\SPHINCS;
use phpseclib3\Crypt\Common;
use phpseclib3\Exception\RuntimeException;
final class PrivateKey extends SPHINCS implements Common\PrivateKey
{
use Common\Traits\PasswordProtected;
public function sign($message)
{
if ($result = openssl_sign($message, $signature, $this->pem, $this->hash->getHash())) {
return $signature;
} else {
throw new RuntimeException("openssl_sign failed: " . openssl_error_string());
}
}
public function getPublicKey(): string
{
$private_key = openssl_pkey_get_private($this->pem);
$private_key_details = openssl_pkey_get_details($private_key);
return $private_key_details['key'];
}
public function toString(string $type, array $options = []): string
{
return $this->pem;
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace phpseclib3\Crypt\SPHINCS;
use phpseclib3\Crypt\SPHINCS;
use phpseclib3\Crypt\Common;
use phpseclib3\Exception\RuntimeException;
final class PublicKey extends SPHINCS implements Common\PublicKey
{
use Common\Traits\Fingerprint;
public function verify($message, $signature)
{
$result = openssl_verify($message, $signature, $this->pem, $this->hash->getHash());
if ($result === false) {
throw new RuntimeException("openssl_verify failed: " . openssl_error_string());
} else {
return boolval($result);
}
}
public function toString(string $type, array $options = []): string
{
return $this->pem;
}
}

53
phpseclib/File/X509.php Normal file → Executable file
View File

@ -29,13 +29,16 @@ namespace phpseclib3\File;
use phpseclib3\Common\Functions\Strings;
use phpseclib3\Crypt\Common\PrivateKey;
use phpseclib3\Crypt\Common\PublicKey;
use phpseclib3\Crypt\DILITHIUM;
use phpseclib3\Crypt\DSA;
use phpseclib3\Crypt\EC;
use phpseclib3\Crypt\FALCON;
use phpseclib3\Crypt\Hash;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\Random;
use phpseclib3\Crypt\RSA;
use phpseclib3\Crypt\RSA\Formats\Keys\PSS;
use phpseclib3\Crypt\SPHINCS;
use phpseclib3\Exception\RuntimeException;
use phpseclib3\Exception\UnsupportedAlgorithmException;
use phpseclib3\File\ASN1\Element;
@ -1405,6 +1408,9 @@ class X509
throw new UnsupportedAlgorithmException('Signature algorithm unsupported');
}
break;
case 'dilithium5':
$key = DILITHIUM::loadFormat('PEM', ['key_pem' => $publicKey, 'method_name' => $publicKeyAlgorithm]);
break;
default:
throw new UnsupportedAlgorithmException('Public key algorithm unsupported');
}
@ -2077,6 +2083,53 @@ class X509
return EC::loadFormat('PKCS8', $key);
case 'id-dsa':
return DSA::loadFormat('PKCS8', $key);
case 'dilithium2':
case 'dilithium3':
case 'dilithium5':
return DILITHIUM::loadFormat(
'PEM',
['key_pem' => $key, 'method_name' => $keyinfo['algorithm']['algorithm']]
);
case 'falcon512':
case 'falcon1024':
return FALCON::loadFormat('PEM', $key);
case 'sphincsharaka128frobust':
case 'sphincsharaka128fsimple':
case 'sphincsharaka128srobust':
case 'sphincsharaka128ssimple':
case 'sphincsharaka192frobust':
case 'sphincsharaka192fsimple':
case 'sphincsharaka192srobust':
case 'sphincsharaka192ssimple':
case 'sphincsharaka256frobust':
case 'sphincsharaka256fsimple':
case 'sphincsharaka256srobust':
case 'sphincsharaka256ssimple':
case 'sphincssha256128frobust':
case 'sphincssha256128fsimple':
case 'sphincssha256128srobust':
case 'sphincssha256128ssimple':
case 'sphincssha256192frobust':
case 'sphincssha256192fsimple':
case 'sphincssha256192srobust':
case 'sphincssha256192ssimple':
case 'sphincssha256256frobust':
case 'sphincssha256256fsimple':
case 'sphincssha256256srobust':
case 'sphincssha256256ssimple':
case 'sphincsshake256128frobust':
case 'sphincsshake256128fsimple':
case 'sphincsshake256128srobust':
case 'sphincsshake256128ssimple':
case 'sphincsshake256196frobust':
case 'sphincsshake256196fsimple':
case 'sphincsshake256196srobust':
case 'sphincsshake256196ssimple':
case 'sphincsshake256256frobust':
case 'sphincsshake256256fsimple':
case 'sphincsshake256256srobust':
case 'sphincsshake256256ssimple':
return SPHINCS::loadFormat('PEM', $key);
}
return false;