diff --git a/phpseclib/Crypt/Common/AsymmetricKey.php b/phpseclib/Crypt/Common/AsymmetricKey.php old mode 100644 new mode 100755 index 06c28022..2b2b78e2 --- a/phpseclib/Crypt/Common/AsymmetricKey.php +++ b/phpseclib/Crypt/Common/AsymmetricKey.php @@ -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(); diff --git a/phpseclib/Crypt/DILITHIUM.php b/phpseclib/Crypt/DILITHIUM.php new file mode 100755 index 00000000..b23eb480 --- /dev/null +++ b/phpseclib/Crypt/DILITHIUM.php @@ -0,0 +1,60 @@ +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); + } +} diff --git a/phpseclib/Crypt/DILITHIUM/Formats/Keys/PEM.php b/phpseclib/Crypt/DILITHIUM/Formats/Keys/PEM.php new file mode 100755 index 00000000..0138e1b3 --- /dev/null +++ b/phpseclib/Crypt/DILITHIUM/Formats/Keys/PEM.php @@ -0,0 +1,72 @@ + 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; + } +} diff --git a/phpseclib/Crypt/DILITHIUM/PrivateKey.php b/phpseclib/Crypt/DILITHIUM/PrivateKey.php new file mode 100755 index 00000000..f9d58fc9 --- /dev/null +++ b/phpseclib/Crypt/DILITHIUM/PrivateKey.php @@ -0,0 +1,85 @@ +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"); + } + } +} diff --git a/phpseclib/Crypt/DILITHIUM/PublicKey.php b/phpseclib/Crypt/DILITHIUM/PublicKey.php new file mode 100755 index 00000000..549d1ac4 --- /dev/null +++ b/phpseclib/Crypt/DILITHIUM/PublicKey.php @@ -0,0 +1,41 @@ +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"); + } + } +} diff --git a/phpseclib/Crypt/FALCON.php b/phpseclib/Crypt/FALCON.php new file mode 100755 index 00000000..b95b0cce --- /dev/null +++ b/phpseclib/Crypt/FALCON.php @@ -0,0 +1,45 @@ +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); + } +} diff --git a/phpseclib/Crypt/FALCON/Formats/Keys/PEM.php b/phpseclib/Crypt/FALCON/Formats/Keys/PEM.php new file mode 100755 index 00000000..e9503b12 --- /dev/null +++ b/phpseclib/Crypt/FALCON/Formats/Keys/PEM.php @@ -0,0 +1,26 @@ + 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; + } +} diff --git a/phpseclib/Crypt/FALCON/PrivateKey.php b/phpseclib/Crypt/FALCON/PrivateKey.php new file mode 100755 index 00000000..a5effc35 --- /dev/null +++ b/phpseclib/Crypt/FALCON/PrivateKey.php @@ -0,0 +1,36 @@ +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; + } +} diff --git a/phpseclib/Crypt/FALCON/PublicKey.php b/phpseclib/Crypt/FALCON/PublicKey.php new file mode 100755 index 00000000..9e6b86ae --- /dev/null +++ b/phpseclib/Crypt/FALCON/PublicKey.php @@ -0,0 +1,29 @@ +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; + } +} diff --git a/phpseclib/Crypt/SPHINCS.php b/phpseclib/Crypt/SPHINCS.php new file mode 100755 index 00000000..ddb6f208 --- /dev/null +++ b/phpseclib/Crypt/SPHINCS.php @@ -0,0 +1,45 @@ +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); + } +} diff --git a/phpseclib/Crypt/SPHINCS/Formats/Keys/PEM.php b/phpseclib/Crypt/SPHINCS/Formats/Keys/PEM.php new file mode 100755 index 00000000..a6601b5a --- /dev/null +++ b/phpseclib/Crypt/SPHINCS/Formats/Keys/PEM.php @@ -0,0 +1,26 @@ + 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; + } +} diff --git a/phpseclib/Crypt/SPHINCS/PrivateKey.php b/phpseclib/Crypt/SPHINCS/PrivateKey.php new file mode 100755 index 00000000..9296a26a --- /dev/null +++ b/phpseclib/Crypt/SPHINCS/PrivateKey.php @@ -0,0 +1,36 @@ +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; + } +} diff --git a/phpseclib/Crypt/SPHINCS/PublicKey.php b/phpseclib/Crypt/SPHINCS/PublicKey.php new file mode 100755 index 00000000..3f158b60 --- /dev/null +++ b/phpseclib/Crypt/SPHINCS/PublicKey.php @@ -0,0 +1,29 @@ +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; + } +} diff --git a/phpseclib/File/X509.php b/phpseclib/File/X509.php old mode 100644 new mode 100755 index e5ae067e..08b4951d --- a/phpseclib/File/X509.php +++ b/phpseclib/File/X509.php @@ -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;