Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Bundle/Resources/config/commands.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Jose\Component\Console\OptimizeRsaKeyCommand;
use Jose\Component\Console\P12CertificateLoaderCommand;
use Jose\Component\Console\PemConverterCommand;
use Jose\Component\Console\Pkcs8ConverterCommand;
use Jose\Component\Console\PublicKeyCommand;
use Jose\Component\Console\PublicKeysetCommand;
use Jose\Component\Console\RotateKeysetCommand;
Expand Down Expand Up @@ -50,6 +51,7 @@
$container->set(OkpKeysetGeneratorCommand::class);
$container->set(P12CertificateLoaderCommand::class);
$container->set(PemConverterCommand::class);
$container->set(Pkcs8ConverterCommand::class);
$container->set(PublicKeyCommand::class);
$container->set(PublicKeysetCommand::class);
$container->set(RotateKeysetCommand::class);
Expand Down
58 changes: 58 additions & 0 deletions src/Library/Console/Pkcs8ConverterCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Jose\Component\Console;

use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\ECKey;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Core\Util\OKPKey;
use Jose\Component\Core\Util\RSAKey;
use Override;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_array;
use function is_string;

#[AsCommand(name: 'key:convert:pkcs8', description: 'Converts a RSA, EC or OKP key into PKCS#8 key.')]
final class Pkcs8ConverterCommand extends ObjectOutputCommand
{
#[Override]
protected function configure(): void
{
parent::configure();
$this
->setHelp(
'This command converts a RSA, EC or OKP key into a PKCS#8 key. As PKCS#8 only covers private keys, public keys are converted into a SubjectPublicKeyInfo structure.'
)
->addArgument('jwk', InputArgument::REQUIRED, 'The key');
}

#[Override]
protected function execute(InputInterface $input, OutputInterface $output): int
{
$jwk = $input->getArgument('jwk');
if (! is_string($jwk)) {
throw new InvalidArgumentException('Invalid JWK');
}
$json = JsonConverter::decode($jwk);
if (! is_array($json)) {
throw new InvalidArgumentException('Invalid JWK.');
}
$key = new JWK($json);

$pem = match ($key->get('kty')) {
'RSA' => RSAKey::createFromJWK($key)->toPEM(),
'EC' => ECKey::convertToPKCS8PEM($key),
'OKP' => OKPKey::convertToPKCS8PEM($key),
default => throw new InvalidArgumentException('Not a RSA, EC or OKP key.'),
};
$output->write($pem);

return self::SUCCESS;
}
}
92 changes: 87 additions & 5 deletions src/Library/Core/Util/ECKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use RuntimeException;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\ASN1\Type\Primitive\BitString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\Integer;
use SpomkyLabs\Pki\ASN1\Type\Primitive\ObjectIdentifier;
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitlyTaggedType;
use SpomkyLabs\Pki\CryptoEncoding\PEM;
use function extension_loaded;
use function is_array;
use function is_string;
Expand All @@ -19,6 +26,11 @@
*/
final readonly class ECKey
{
/**
* OID of the id-ecPublicKey algorithm identifier.
*/
private const EC_PUBLIC_KEY_OID = '1.2.840.10045.2.1';

public static function convertToPEM(JWK $jwk): string
{
if ($jwk->has('d')) {
Expand All @@ -28,6 +40,49 @@ public static function convertToPEM(JWK $jwk): string
return self::convertPublicKeyToPEM($jwk);
}

/**
* Converts the key into a PKCS#8 PEM. As PKCS#8 only covers private keys, public keys are converted into a
* SubjectPublicKeyInfo structure, which is the format expected by the tools consuming PKCS#8 private keys.
*/
public static function convertToPKCS8PEM(JWK $jwk): string
{
if ($jwk->has('d')) {
return self::convertPrivateKeyToPKCS8PEM($jwk);
}

return self::convertPublicKeyToPEM($jwk);
}

/**
* Converts the private key into a PKCS#8 (RFC 5208) PEM, i.e. a PrivateKeyInfo structure wrapping the RFC 5915
* ECPrivateKey. The curve is only carried by the algorithm identifier: the optional "parameters" field of the
* inner ECPrivateKey is left out to avoid the duplication, exactly as OpenSSL does.
*/
public static function convertPrivateKeyToPKCS8PEM(JWK $jwk): string
{
$curve = $jwk->get('crv');
if (! is_string($curve)) {
throw new InvalidArgumentException('Unable to get the curve');
}
$length = (int) ceil(self::getCurveSize($curve) / 8);
$ecPrivateKey = Sequence::create(
Integer::create(1),
OctetString::create(self::getPrivateKeyBytes($jwk, $length)),
ExplicitlyTaggedType::create(1, BitString::create(self::getKey($jwk))),
);
$privateKeyInfo = Sequence::create(
Integer::create(0),
Sequence::create(
ObjectIdentifier::create(self::EC_PUBLIC_KEY_OID),
ObjectIdentifier::create(self::getCurveOid($curve)),
),
OctetString::create($ecPrivateKey->toDER()),
);

return PEM::create(PEM::TYPE_PRIVATE_KEY, $privateKeyInfo->toDER())
->string();
}

public static function convertPublicKeyToPEM(JWK $jwk): string
{
$der = match ($jwk->get('crv')) {
Expand Down Expand Up @@ -133,6 +188,24 @@ private static function createECKeyUsingOpenSSL(string $curve): array
];
}

/**
* Returns the OID of the named curve, as used by the AlgorithmIdentifier of the PKCS#8 and SubjectPublicKeyInfo
* structures.
*/
private static function getCurveOid(string $curve): string
{
return match ($curve) {
'P-256' => '1.2.840.10045.3.1.7',
'secp256k1' => '1.3.132.0.10',
'P-384' => '1.3.132.0.34',
'P-521' => '1.3.132.0.35',
'BP-256' => '1.3.36.3.3.2.8.1.1.7',
'BP-384' => '1.3.36.3.3.2.8.1.1.11',
'BP-512' => '1.3.36.3.3.2.8.1.1.13',
default => throw new InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve)),
};
}

private static function getOpensslCurveName(string $curve): string
{
return match ($curve) {
Expand Down Expand Up @@ -346,18 +419,27 @@ private static function bp512PrivateKey(JWK $jwk): string
*/
private static function getPrivateKeyOctets(JWK $jwk, int $length): string
{
$d = $jwk->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Unable to get the private key');
}
$data = unpack('H*', str_pad(Base64UrlSafe::decodeNoPadding($d), $length, "\0", STR_PAD_LEFT));
$data = unpack('H*', self::getPrivateKeyBytes($jwk, $length));
if (! is_array($data) || ! isset($data[1]) || ! is_string($data[1])) {
throw new InvalidArgumentException('Unable to get the private key');
}

return $data[1];
}

/**
* Returns the binary representation of the private key, left-padded to the size of the curve.
*/
private static function getPrivateKeyBytes(JWK $jwk, int $length): string
{
$d = $jwk->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Unable to get the private key');
}

return str_pad(Base64UrlSafe::decodeNoPadding($d), $length, "\0", STR_PAD_LEFT);
}

private static function getKey(JWK $jwk): string
{
$crv = $jwk->get('crv');
Expand Down
105 changes: 105 additions & 0 deletions src/Library/Core/Util/OKPKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

namespace Jose\Component\Core\Util;

use InvalidArgumentException;
use Jose\Component\Core\JWK;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PrivateKey;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PrivateKeyInfo;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PublicKey;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RFC8410\Curve25519\Ed25519PrivateKey;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RFC8410\Curve25519\Ed25519PublicKey;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RFC8410\Curve25519\X25519PrivateKey;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RFC8410\Curve25519\X25519PublicKey;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RFC8410\Curve448\Ed448PrivateKey;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RFC8410\Curve448\Ed448PublicKey;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RFC8410\Curve448\X448PrivateKey;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RFC8410\Curve448\X448PublicKey;
use function is_string;
use function sprintf;

/**
* Converts Octet Key Pair keys (RFC 8037) into the PEM structures defined by RFC 8410.
*
* @internal
*/
final readonly class OKPKey
{
/**
* Converts the key into a PKCS#8 PEM. As PKCS#8 only covers private keys, public keys are converted into a
* SubjectPublicKeyInfo structure, which is the format expected by the tools consuming PKCS#8 private keys.
*/
public static function convertToPKCS8PEM(JWK $jwk): string
{
if ($jwk->has('d')) {
return self::convertPrivateKeyToPKCS8PEM($jwk);
}

return self::convertPublicKeyToPEM($jwk);
}

/**
* Converts the private key into a PKCS#8 (RFC 5208) PEM. The public key is deliberately left out of the structure:
* the resulting OneAsymmetricKey stays at version 0, which is what RFC 8410 section 7 recommends and what the
* widely deployed PKCS#8 parsers expect.
*/
public static function convertPrivateKeyToPKCS8PEM(JWK $jwk): string
{
$privateKey = self::createPrivateKey($jwk);

return PrivateKeyInfo::create($privateKey->algorithmIdentifier(), $privateKey->toDER())
->toPEM()
->string();
}

/**
* Converts the public key into a SubjectPublicKeyInfo (RFC 5280) PEM.
*/
public static function convertPublicKeyToPEM(JWK $jwk): string
{
return self::createPublicKey($jwk)
->publicKeyInfo()
->toPEM()
->string();
}

private static function createPrivateKey(JWK $jwk): PrivateKey
{
$curve = self::getParameter($jwk, 'crv');
$d = Base64UrlSafe::decodeNoPadding(self::getParameter($jwk, 'd'));

return match ($curve) {
'Ed25519' => Ed25519PrivateKey::create($d),
'Ed448' => Ed448PrivateKey::create($d),
'X25519' => X25519PrivateKey::create($d),
'X448' => X448PrivateKey::create($d),
default => throw new InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve)),
};
}

private static function createPublicKey(JWK $jwk): PublicKey
{
$curve = self::getParameter($jwk, 'crv');
$x = Base64UrlSafe::decodeNoPadding(self::getParameter($jwk, 'x'));

return match ($curve) {
'Ed25519' => Ed25519PublicKey::create($x),
'Ed448' => Ed448PublicKey::create($x),
'X25519' => X25519PublicKey::create($x),
'X448' => X448PublicKey::create($x),
default => throw new InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve)),
};
}

private static function getParameter(JWK $jwk, string $parameter): string
{
$value = $jwk->get($parameter);
if (! is_string($value)) {
throw new InvalidArgumentException(sprintf('Unable to get the "%s" parameter', $parameter));
}

return $value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public static function allCommandsAreAvailable(): void
'key:optimize',
'key:load:p12',
'key:convert:pkcs1',
'key:convert:pkcs8',
'keyset:convert:public',
'keyset:rotate',
'key:generate:rsa',
Expand Down
Loading
Loading