diff --git a/Command/DeleteCommand.php b/Command/DeleteCommand.php new file mode 100644 index 0000000..3f0cd37 --- /dev/null +++ b/Command/DeleteCommand.php @@ -0,0 +1,64 @@ +setName('spomky-labs:jose:delete') + ->setDescription('Delete a key or key set.') + ->addArgument( + 'service', + InputArgument::REQUIRED + ) + ->setHelp(<<<'EOT' +The %command.name% command will delete a key or key set. +If the service is called, then the key will be created again. + + php %command.full_name% +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $service_name = $input->getArgument('service'); + if (!$this->getContainer()->has($service_name)) { + $output->writeln(sprintf('The service "%s" does not exist.', $service_name)); + + return 1; + } + $service = $this->getContainer()->get($service_name); + if (!$service instanceof StorableInterface) { + $output->writeln(sprintf('The service "%s" is not a storable object.', $service_name)); + + return 2; + } + + $service->delete(); + $output->writeln('Done.'); + } +} diff --git a/Command/KeyRotationCommand.php b/Command/KeyRotationCommand.php deleted file mode 100644 index e5c8eeb..0000000 --- a/Command/KeyRotationCommand.php +++ /dev/null @@ -1,79 +0,0 @@ -setName('spomky-labs:jose:rotate-keys') - ->setDescription('Rotate key') - ->addArgument( - 'key', - InputArgument::REQUIRED - ) - ->addArgument( - 'ttl', - InputArgument::OPTIONAL, - '', - 3600*24*7 - ) - ->setHelp(<<<'EOT' -The %command.name% command will create a new client. - - php %command.full_name% -EOT - ); - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $key_name = $input->getArgument('key'); - if (!$this->getContainer()->has($key_name)) { - $output->writeln(sprintf('The key "%s" does not exist', $key_name)); - return 1; - } - $key = $this->getContainer()->get($key_name); - if (!$key instanceof StorableJWKInterface) { - $output->writeln(sprintf('The key "%s" is not a storable key', $key_name)); - return 2; - } - - if (!file_exists($key->getFilename())) { - $output->writeln(sprintf('The key "%s" does not exist and will be created.', $key_name)); - $key->getAll(); - } else { - $ttl = $input->getArgument('ttl'); - $mtime = filemtime($key->getFilename()); - if ($mtime+$ttl <= time()) { - $output->writeln(sprintf('The key "%s" exists but expired. It will be updated.', $key_name)); - unlink($key->getFilename()); - $key->getAll(); - } else { - $output->writeln(sprintf('The key "%s" exists and is not expired.', $key_name)); - } - } - } -} diff --git a/Command/RegenCommand.php b/Command/RegenCommand.php new file mode 100644 index 0000000..469b09d --- /dev/null +++ b/Command/RegenCommand.php @@ -0,0 +1,63 @@ +setName('spomky-labs:jose:regen') + ->setDescription('Generate a new key or key set.') + ->addArgument( + 'service', + InputArgument::REQUIRED + ) + ->setHelp(<<<'EOT' +The %command.name% command will generate a new key or key set. + + php %command.full_name% +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $service_name = $input->getArgument('service'); + if (!$this->getContainer()->has($service_name)) { + $output->writeln(sprintf('The service "%s" does not exist.', $service_name)); + + return 1; + } + $service = $this->getContainer()->get($service_name); + if (!$service instanceof StorableInterface) { + $output->writeln(sprintf('The service "%s" is not a storable object.', $service_name)); + + return 2; + } + + $service->regen(); + $output->writeln('Done.'); + } +} diff --git a/Command/RotateCommand.php b/Command/RotateCommand.php new file mode 100644 index 0000000..ef8c7ac --- /dev/null +++ b/Command/RotateCommand.php @@ -0,0 +1,85 @@ +setName('spomky-labs:jose:rotate') + ->setDescription('Rotate a key or keys in the key set') + ->addArgument( + 'service', + InputArgument::REQUIRED + ) + ->addArgument( + 'ttl', + InputArgument::OPTIONAL, + '', + 3600 * 24 * 7 + ) + ->setHelp(<<<'EOT' +The %command.name% command will rotate a key or keys in the key set. + + php %command.full_name% +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $service_name = $input->getArgument('service'); + if (!$this->getContainer()->has($service_name)) { + $output->writeln(sprintf('The service "%s" does not exist.', $service_name)); + + return 1; + } + $service = $this->getContainer()->get($service_name); + if (!$service instanceof JWKSetInterface) { + $output->writeln(sprintf('The service "%s" is not a key set.', $service_name)); + + return 2; + } + + if (!$service instanceof RotatableInterface) { + $output->writeln(sprintf('The service "%s" is not a rotatable key set.', $service_name)); + + return 3; + } + + $mtime = $service->getLastModificationTime(); + + if (null === $mtime) { + $service->regen(); + $output->writeln('Done.'); + } elseif ($mtime + $input->getArgument('ttl') <= time()) { + $service->rotate(); + $output->writeln('Done.'); + } else { + $output->writeln(sprintf('The key set "%s" has not expired.', $service_name)); + } + } +} diff --git a/DependencyInjection/Compiler/AlgorithmCompilerPass.php b/DependencyInjection/Compiler/AlgorithmCompilerPass.php index 99b481e..175fe89 100644 --- a/DependencyInjection/Compiler/AlgorithmCompilerPass.php +++ b/DependencyInjection/Compiler/AlgorithmCompilerPass.php @@ -17,6 +17,9 @@ final class AlgorithmCompilerPass implements CompilerPassInterface { + /** + * {@inheritdoc} + */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('jose.algorithm_manager')) { diff --git a/DependencyInjection/Compiler/CheckerCompilerPass.php b/DependencyInjection/Compiler/CheckerCompilerPass.php index ded9758..0635d60 100644 --- a/DependencyInjection/Compiler/CheckerCompilerPass.php +++ b/DependencyInjection/Compiler/CheckerCompilerPass.php @@ -19,6 +19,9 @@ final class CheckerCompilerPass implements CompilerPassInterface { + /** + * {@inheritdoc} + */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('jose.checker_manager')) { diff --git a/DependencyInjection/Compiler/CompressionCompilerPass.php b/DependencyInjection/Compiler/CompressionCompilerPass.php index 373dc71..4e869cc 100644 --- a/DependencyInjection/Compiler/CompressionCompilerPass.php +++ b/DependencyInjection/Compiler/CompressionCompilerPass.php @@ -17,6 +17,9 @@ final class CompressionCompilerPass implements CompilerPassInterface { + /** + * {@inheritdoc} + */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('jose.compression_manager')) { diff --git a/DependencyInjection/Source/AbstractSource.php b/DependencyInjection/Source/AbstractSource.php new file mode 100644 index 0000000..bc05719 --- /dev/null +++ b/DependencyInjection/Source/AbstractSource.php @@ -0,0 +1,52 @@ +createDefinition($container, $config); + $definition->setPublic($config['is_public']); + $container->setDefinition($id, $definition); + } + + /** + * @param \Symfony\Component\Config\Definition\Builder\NodeDefinition $node + */ + public function addConfiguration(NodeDefinition $node) + { + $node + ->children() + ->booleanNode('is_public') + ->info('If true, the service will be public, else private.') + ->defaultTrue() + ->end() + ->end(); + } +} diff --git a/DependencyInjection/Source/DecrypterSource.php b/DependencyInjection/Source/DecrypterSource.php index 865045f..08e4f17 100644 --- a/DependencyInjection/Source/DecrypterSource.php +++ b/DependencyInjection/Source/DecrypterSource.php @@ -40,7 +40,7 @@ public function createService($name, array $config, ContainerBuilder $container) $definition->setArguments([ $config['key_encryption_algorithms'], $config['content_encryption_algorithms'], - $config['compression_methods'] + $config['compression_methods'], ]); $definition->setPublic($config['is_public']); diff --git a/DependencyInjection/Source/JWKSetSource/JKU.php b/DependencyInjection/Source/JWKSetSource/JKU.php index 6fbbb48..f1683fc 100644 --- a/DependencyInjection/Source/JWKSetSource/JKU.php +++ b/DependencyInjection/Source/JWKSetSource/JKU.php @@ -11,17 +11,18 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection\Source\JWKSetSource; +use SpomkyLabs\JoseBundle\DependencyInjection\Source\AbstractSource; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class JKU implements JWKSetSourceInterface +class JKU extends AbstractSource implements JWKSetSourceInterface { /** * {@inheritdoc} */ - public function create(ContainerBuilder $container, $id, array $config) + public function createDefinition(ContainerBuilder $container, array $config) { $definition = new Definition('Jose\Object\JWKSet'); $definition->setFactory([ @@ -31,10 +32,11 @@ public function create(ContainerBuilder $container, $id, array $config) $definition->setArguments([ $config['url'], $config['is_secured'], + $config['cache'], + $config['cache_ttl'], ]); - $definition->setPublic($config['is_public']); - $container->setDefinition($id, $definition); + return $definition; } /** @@ -50,14 +52,13 @@ public function getKeySet() */ public function addConfiguration(NodeDefinition $node) { + parent::addConfiguration($node); $node ->children() - ->booleanNode('is_public') - ->info('If true, the service will be public, else private.') - ->defaultTrue() - ->end() ->scalarNode('url')->isRequired()->end() ->booleanNode('is_secured')->defaultTrue()->end() + ->scalarNode('cache')->defaultNull()->end() + ->integerNode('cache_ttl')->defaultValue(0)->end() ->end(); } } diff --git a/DependencyInjection/Source/JWKSetSource/JWKSet.php b/DependencyInjection/Source/JWKSetSource/JWKSet.php index 2667786..803f67b 100644 --- a/DependencyInjection/Source/JWKSetSource/JWKSet.php +++ b/DependencyInjection/Source/JWKSetSource/JWKSet.php @@ -11,17 +11,18 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection\Source\JWKSetSource; +use SpomkyLabs\JoseBundle\DependencyInjection\Source\AbstractSource; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class JWKSet implements JWKSetSourceInterface +class JWKSet extends AbstractSource implements JWKSetSourceInterface { /** * {@inheritdoc} */ - public function create(ContainerBuilder $container, $id, array $config) + public function createDefinition(ContainerBuilder $container, array $config) { $definition = new Definition('Jose\Object\JWKSet'); $definition->setFactory([ @@ -31,9 +32,8 @@ public function create(ContainerBuilder $container, $id, array $config) $definition->setArguments([ json_decode($config['value'], true), ]); - $definition->setPublic($config['is_public']); - $container->setDefinition($id, $definition); + return $definition; } /** @@ -49,12 +49,9 @@ public function getKeySet() */ public function addConfiguration(NodeDefinition $node) { + parent::addConfiguration($node); $node ->children() - ->booleanNode('is_public') - ->info('If true, the service will be public, else private.') - ->defaultTrue() - ->end() ->scalarNode('value')->isRequired()->end() ->end(); } diff --git a/DependencyInjection/Source/JWKSetSource/JWKSets.php b/DependencyInjection/Source/JWKSetSource/JWKSets.php new file mode 100644 index 0000000..f94bdad --- /dev/null +++ b/DependencyInjection/Source/JWKSetSource/JWKSets.php @@ -0,0 +1,62 @@ +setFactory([ + new Reference('jose.factory.jwk'), + 'createKeySets', + ]); + foreach ($config['id'] as $key_set_id) { + $ref = new Reference($key_set_id); + $definition->addMethodCall('addKeySet', [$ref]); + } + + return $definition; + } + + /** + * {@inheritdoc} + */ + public function getKeySet() + { + return 'jwksets'; + } + + /** + * {@inheritdoc} + */ + public function addConfiguration(NodeDefinition $node) + { + parent::addConfiguration($node); + $node + ->children() + ->arrayNode('id') + ->prototype('scalar')->end() + ->isRequired() + ->end() + ->end(); + } +} diff --git a/DependencyInjection/Source/JWKSetSource/Keys.php b/DependencyInjection/Source/JWKSetSource/Keys.php index 052a204..5afed2e 100644 --- a/DependencyInjection/Source/JWKSetSource/Keys.php +++ b/DependencyInjection/Source/JWKSetSource/Keys.php @@ -11,26 +11,26 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection\Source\JWKSetSource; +use SpomkyLabs\JoseBundle\DependencyInjection\Source\AbstractSource; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class Keys implements JWKSetSourceInterface +class Keys extends AbstractSource implements JWKSetSourceInterface { /** * {@inheritdoc} */ - public function create(ContainerBuilder $container, $id, array $config) + public function createDefinition(ContainerBuilder $container, array $config) { $definition = new Definition('Jose\Object\JWKSet'); foreach ($config['id'] as $key_id) { $ref = new Reference($key_id); $definition->addMethodCall('addKey', [$ref]); } - $definition->setPublic($config['is_public']); - $container->setDefinition($id, $definition); + return $definition; } /** @@ -46,12 +46,9 @@ public function getKeySet() */ public function addConfiguration(NodeDefinition $node) { + parent::addConfiguration($node); $node ->children() - ->booleanNode('is_public') - ->info('If true, the service will be public, else private.') - ->defaultTrue() - ->end() ->arrayNode('id') ->prototype('scalar') ->end() diff --git a/DependencyInjection/Source/JWKSetSource/PublicJWKSet.php b/DependencyInjection/Source/JWKSetSource/PublicJWKSet.php new file mode 100644 index 0000000..a0d494e --- /dev/null +++ b/DependencyInjection/Source/JWKSetSource/PublicJWKSet.php @@ -0,0 +1,59 @@ +setFactory([ + new Reference('jose.factory.jwk'), + 'createPublicKeySet', + ]); + $definition->setArguments([new Reference($config['id'])]); + + return $definition; + } + + /** + * {@inheritdoc} + */ + public function getKeySet() + { + return 'public_jwkset'; + } + + /** + * {@inheritdoc} + */ + public function addConfiguration(NodeDefinition $node) + { + parent::addConfiguration($node); + $node + ->children() + ->scalarNode('id') + ->info('ID of the JWKSet to use.') + ->isRequired() + ->end() + ->end(); + } +} diff --git a/DependencyInjection/Source/JWKSetSource/RandomKeySet.php b/DependencyInjection/Source/JWKSetSource/RandomKeySet.php new file mode 100644 index 0000000..d034f0d --- /dev/null +++ b/DependencyInjection/Source/JWKSetSource/RandomKeySet.php @@ -0,0 +1,81 @@ +setFactory([ + new Reference('jose.factory.jwk'), + $method, + ]); + $definition->setArguments([ + $config['storage_path'], + $config['key_configuration'], + $config['nb_keys'], + ]); + + return $definition; + } + + /** + * {@inheritdoc} + */ + public function getKeySet() + { + return 'auto'; + } + + /** + * {@inheritdoc} + */ + public function addConfiguration(NodeDefinition $node) + { + parent::addConfiguration($node); + $node + ->children() + ->booleanNode('is_rotatable') + ->info('If true, the service will be a rotatable key, else just storable.') + ->defaultFalse() + ->end() + ->integerNode('nb_keys') + ->info('Number of keys in the key set.') + ->isRequired() + ->min(1) + ->end() + ->scalarNode('storage_path')->isRequired()->end() + ->arrayNode('key_configuration') + ->defaultValue([]) + ->useAttributeAsKey('key') + ->prototype('variable')->end() + ->end() + ->end(); + } +} diff --git a/DependencyInjection/Source/JWKSetSource/X5U.php b/DependencyInjection/Source/JWKSetSource/X5U.php index 743d086..1ac7adb 100644 --- a/DependencyInjection/Source/JWKSetSource/X5U.php +++ b/DependencyInjection/Source/JWKSetSource/X5U.php @@ -11,17 +11,18 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection\Source\JWKSetSource; +use SpomkyLabs\JoseBundle\DependencyInjection\Source\AbstractSource; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class X5U implements JWKSetSourceInterface +class X5U extends AbstractSource implements JWKSetSourceInterface { /** * {@inheritdoc} */ - public function create(ContainerBuilder $container, $id, array $config) + public function createDefinition(ContainerBuilder $container, array $config) { $definition = new Definition('Jose\Object\JWKSet'); $definition->setFactory([ @@ -31,10 +32,11 @@ public function create(ContainerBuilder $container, $id, array $config) $definition->setArguments([ $config['url'], $config['is_secured'], + $config['cache'], + $config['cache_ttl'], ]); - $definition->setPublic($config['is_public']); - $container->setDefinition($id, $definition); + return $definition; } /** @@ -50,14 +52,13 @@ public function getKeySet() */ public function addConfiguration(NodeDefinition $node) { + parent::addConfiguration($node); $node ->children() - ->booleanNode('is_public') - ->info('If true, the service will be public, else private.') - ->defaultTrue() - ->end() ->scalarNode('url')->isRequired()->end() ->booleanNode('is_secured')->defaultTrue()->end() + ->scalarNode('cache')->defaultNull()->end() + ->integerNode('cache_ttl')->defaultValue(0)->end() ->end(); } } diff --git a/DependencyInjection/Source/JWKSource/CertificateFile.php b/DependencyInjection/Source/JWKSource/CertificateFile.php index cbc2f34..de0596f 100644 --- a/DependencyInjection/Source/JWKSource/CertificateFile.php +++ b/DependencyInjection/Source/JWKSource/CertificateFile.php @@ -11,17 +11,18 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection\Source\JWKSource; +use SpomkyLabs\JoseBundle\DependencyInjection\Source\AbstractSource; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class CertificateFile implements JWKSourceInterface +class CertificateFile extends AbstractSource implements JWKSourceInterface { /** * {@inheritdoc} */ - public function create(ContainerBuilder $container, $id, array $config) + public function createDefinition(ContainerBuilder $container, array $config) { $definition = new Definition('Jose\Object\JWK'); $definition->setFactory([ @@ -32,9 +33,8 @@ public function create(ContainerBuilder $container, $id, array $config) $config['path'], $config['additional_values'], ]); - $definition->setPublic($config['is_public']); - $container->setDefinition($id, $definition); + return $definition; } /** @@ -50,12 +50,9 @@ public function getKey() */ public function addConfiguration(NodeDefinition $node) { + parent::addConfiguration($node); $node ->children() - ->booleanNode('is_public') - ->info('If true, the service will be public, else private.') - ->defaultTrue() - ->end() ->scalarNode('path')->isRequired()->end() ->arrayNode('additional_values') ->defaultValue([]) diff --git a/DependencyInjection/Source/JWKSource/JWK.php b/DependencyInjection/Source/JWKSource/JWK.php index c088cf7..33399b4 100644 --- a/DependencyInjection/Source/JWKSource/JWK.php +++ b/DependencyInjection/Source/JWKSource/JWK.php @@ -11,17 +11,18 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection\Source\JWKSource; +use SpomkyLabs\JoseBundle\DependencyInjection\Source\AbstractSource; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class JWK implements JWKSourceInterface +class JWK extends AbstractSource implements JWKSourceInterface { /** * {@inheritdoc} */ - public function create(ContainerBuilder $container, $id, array $config) + public function createDefinition(ContainerBuilder $container, array $config) { $definition = new Definition('Jose\Object\JWK'); $definition->setFactory([ @@ -31,9 +32,8 @@ public function create(ContainerBuilder $container, $id, array $config) $definition->setArguments([ json_decode($config['value'], true), ]); - $definition->setPublic($config['is_public']); - $container->setDefinition($id, $definition); + return $definition; } /** @@ -49,12 +49,9 @@ public function getKey() */ public function addConfiguration(NodeDefinition $node) { + parent::addConfiguration($node); $node ->children() - ->booleanNode('is_public') - ->info('If true, the service will be public, else private.') - ->defaultTrue() - ->end() ->scalarNode('value')->isRequired()->end() ->end(); } diff --git a/DependencyInjection/Source/JWKSource/JWKSet.php b/DependencyInjection/Source/JWKSource/JWKSet.php new file mode 100644 index 0000000..b9aa56c --- /dev/null +++ b/DependencyInjection/Source/JWKSource/JWKSet.php @@ -0,0 +1,62 @@ +setFactory([ + new Reference('jose.factory.jwk'), + 'createFromKeySet', + ]); + $definition->setArguments([new Reference($config['key_set']), $config['index']]); + + return $definition; + } + + /** + * {@inheritdoc} + */ + public function getKey() + { + return 'jwkset'; + } + + /** + * {@inheritdoc} + */ + public function addConfiguration(NodeDefinition $node) + { + parent::addConfiguration($node); + $node + ->children() + ->scalarNode('key_set') + ->info('The key set service.') + ->isRequired()->end() + ->integerNode('index') + ->info('The index of the key in the key set.') + ->isRequired() + ->end() + ->end(); + } +} diff --git a/DependencyInjection/Source/JWKSource/KeyFile.php b/DependencyInjection/Source/JWKSource/KeyFile.php index 5b0e25b..38bddda 100644 --- a/DependencyInjection/Source/JWKSource/KeyFile.php +++ b/DependencyInjection/Source/JWKSource/KeyFile.php @@ -11,17 +11,18 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection\Source\JWKSource; +use SpomkyLabs\JoseBundle\DependencyInjection\Source\AbstractSource; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class KeyFile implements JWKSourceInterface +class KeyFile extends AbstractSource implements JWKSourceInterface { /** * {@inheritdoc} */ - public function create(ContainerBuilder $container, $id, array $config) + public function createDefinition(ContainerBuilder $container, array $config) { $definition = new Definition('Jose\Object\JWK'); $definition->setFactory([ @@ -33,9 +34,8 @@ public function create(ContainerBuilder $container, $id, array $config) $config['password'], $config['additional_values'], ]); - $definition->setPublic($config['is_public']); - $container->setDefinition($id, $definition); + return $definition; } /** @@ -51,12 +51,9 @@ public function getKey() */ public function addConfiguration(NodeDefinition $node) { + parent::addConfiguration($node); $node ->children() - ->booleanNode('is_public') - ->info('If true, the service will be public, else private.') - ->defaultTrue() - ->end() ->scalarNode('path')->isRequired()->end() ->scalarNode('password')->defaultNull()->end() ->arrayNode('additional_values') diff --git a/DependencyInjection/Source/JWKSource/RandomECKey.php b/DependencyInjection/Source/JWKSource/RandomECKey.php index 691478e..3b81c95 100644 --- a/DependencyInjection/Source/JWKSource/RandomECKey.php +++ b/DependencyInjection/Source/JWKSource/RandomECKey.php @@ -20,7 +20,7 @@ class RandomECKey extends RandomKey */ protected function getKeyConfig(array $config) { - $values = $config['additional_values']; + $values = $config['key_configuration']; $values['kty'] = 'EC'; $values['crv'] = $config['curve']; @@ -40,7 +40,6 @@ public function getKey() */ public function addConfiguration(NodeDefinition $node) { - $node ->children() ->scalarNode('curve') @@ -53,7 +52,10 @@ public function addConfiguration(NodeDefinition $node) ->end(); parent::addConfiguration($node); } - + + /** + * @return \Closure + */ private static function checkCurve() { return function ($v) { diff --git a/DependencyInjection/Source/JWKSource/RandomKey.php b/DependencyInjection/Source/JWKSource/RandomKey.php index b81bcbb..ceeb327 100644 --- a/DependencyInjection/Source/JWKSource/RandomKey.php +++ b/DependencyInjection/Source/JWKSource/RandomKey.php @@ -11,30 +11,30 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection\Source\JWKSource; +use SpomkyLabs\JoseBundle\DependencyInjection\Source\AbstractSource; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -abstract class RandomKey implements JWKSourceInterface +abstract class RandomKey extends AbstractSource implements JWKSourceInterface { /** * {@inheritdoc} */ - public function create(ContainerBuilder $container, $id, array $config) + public function createDefinition(ContainerBuilder $container, array $config) { $definition = new Definition('Jose\Object\StorableJWK'); $definition->setFactory([ new Reference('jose.factory.jwk'), 'createStorableKey', ]); + $definition->setArguments([ + $config['storage_path'], + $this->getKeyConfig($config), + ]); - $key_config = $this->getKeyConfig($config); - - $definition->setArguments([$config['storage_path'], $key_config]); - $definition->setPublic($config['is_public']); - - $container->setDefinition($id, $definition); + return $definition; } /** @@ -49,14 +49,11 @@ abstract protected function getKeyConfig(array $config); */ public function addConfiguration(NodeDefinition $node) { + parent::addConfiguration($node); $node ->children() - ->booleanNode('is_public') - ->info('If true, the service will be public, else private.') - ->defaultTrue() - ->end() ->scalarNode('storage_path')->isRequired()->end() - ->arrayNode('additional_values') + ->arrayNode('key_configuration') ->defaultValue([]) ->useAttributeAsKey('key') ->prototype('variable')->end() diff --git a/DependencyInjection/Source/JWKSource/RandomNoneKey.php b/DependencyInjection/Source/JWKSource/RandomNoneKey.php index fbd7ada..14ef969 100644 --- a/DependencyInjection/Source/JWKSource/RandomNoneKey.php +++ b/DependencyInjection/Source/JWKSource/RandomNoneKey.php @@ -18,9 +18,9 @@ class RandomNoneKey extends RandomKey */ protected function getKeyConfig(array $config) { - $values = $config['additional_values']; + $values = $config['key_configuration']; $values['kty'] = 'none'; - + return $values; } diff --git a/DependencyInjection/Source/JWKSource/RandomOKPKey.php b/DependencyInjection/Source/JWKSource/RandomOKPKey.php index 919fd8b..5d085d4 100644 --- a/DependencyInjection/Source/JWKSource/RandomOKPKey.php +++ b/DependencyInjection/Source/JWKSource/RandomOKPKey.php @@ -20,7 +20,7 @@ class RandomOKPKey extends RandomKey */ protected function getKeyConfig(array $config) { - $values = $config['additional_values']; + $values = $config['key_configuration']; $values['kty'] = 'OKP'; $values['crv'] = $config['curve']; @@ -53,6 +53,9 @@ public function addConfiguration(NodeDefinition $node) parent::addConfiguration($node); } + /** + * @return \Closure + */ private static function checkCurve() { return function ($v) { diff --git a/DependencyInjection/Source/JWKSource/RandomOctKey.php b/DependencyInjection/Source/JWKSource/RandomOctKey.php index 5ec6b1e..b155b80 100644 --- a/DependencyInjection/Source/JWKSource/RandomOctKey.php +++ b/DependencyInjection/Source/JWKSource/RandomOctKey.php @@ -20,7 +20,7 @@ class RandomOctKey extends RandomKey */ protected function getKeyConfig(array $config) { - $values = $config['additional_values']; + $values = $config['key_configuration']; $values['kty'] = 'oct'; $values['size'] = $config['size']; diff --git a/DependencyInjection/Source/JWKSource/RandomRSAKey.php b/DependencyInjection/Source/JWKSource/RandomRSAKey.php index 8f74f18..7aba779 100644 --- a/DependencyInjection/Source/JWKSource/RandomRSAKey.php +++ b/DependencyInjection/Source/JWKSource/RandomRSAKey.php @@ -20,7 +20,7 @@ class RandomRSAKey extends RandomKey */ protected function getKeyConfig(array $config) { - $values = $config['additional_values']; + $values = $config['key_configuration']; $values['kty'] = 'RSA'; $values['size'] = $config['size']; diff --git a/DependencyInjection/Source/JWKSource/Values.php b/DependencyInjection/Source/JWKSource/Values.php index ef708dc..f507abd 100644 --- a/DependencyInjection/Source/JWKSource/Values.php +++ b/DependencyInjection/Source/JWKSource/Values.php @@ -11,17 +11,18 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection\Source\JWKSource; +use SpomkyLabs\JoseBundle\DependencyInjection\Source\AbstractSource; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class Values implements JWKSourceInterface +class Values extends AbstractSource implements JWKSourceInterface { /** * {@inheritdoc} */ - public function create(ContainerBuilder $container, $id, array $config) + public function createDefinition(ContainerBuilder $container, array $config) { $definition = new Definition('Jose\Object\JWK'); $definition->setFactory([ @@ -31,9 +32,8 @@ public function create(ContainerBuilder $container, $id, array $config) $definition->setArguments([ $config['values'], ]); - $definition->setPublic($config['is_public']); - $container->setDefinition($id, $definition); + return $definition; } /** @@ -49,12 +49,9 @@ public function getKey() */ public function addConfiguration(NodeDefinition $node) { + parent::addConfiguration($node); $node ->children() - ->booleanNode('is_public') - ->info('If true, the service will be public, else private.') - ->defaultTrue() - ->end() ->arrayNode('values') ->isRequired() ->useAttributeAsKey('key') diff --git a/DependencyInjection/Source/JWKSource/X5C.php b/DependencyInjection/Source/JWKSource/X5C.php index 7e5b36e..ce493bc 100644 --- a/DependencyInjection/Source/JWKSource/X5C.php +++ b/DependencyInjection/Source/JWKSource/X5C.php @@ -11,17 +11,18 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection\Source\JWKSource; +use SpomkyLabs\JoseBundle\DependencyInjection\Source\AbstractSource; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class X5C implements JWKSourceInterface +class X5C extends AbstractSource implements JWKSourceInterface { /** * {@inheritdoc} */ - public function create(ContainerBuilder $container, $id, array $config) + public function createDefinition(ContainerBuilder $container, array $config) { $definition = new Definition('Jose\Object\JWK'); $definition->setFactory([ @@ -31,9 +32,8 @@ public function create(ContainerBuilder $container, $id, array $config) $definition->setArguments([ $config['value'], ]); - $definition->setPublic($config['is_public']); - $container->setDefinition($id, $definition); + return $definition; } /** @@ -49,12 +49,9 @@ public function getKey() */ public function addConfiguration(NodeDefinition $node) { + parent::addConfiguration($node); $node ->children() - ->booleanNode('is_public') - ->info('If true, the service will be public, else private.') - ->defaultTrue() - ->end() ->scalarNode('value')->isRequired()->end() ->end(); } diff --git a/DependencyInjection/SpomkyLabsJoseBundleExtension.php b/DependencyInjection/SpomkyLabsJoseBundleExtension.php index 03675ac..aef6af3 100644 --- a/DependencyInjection/SpomkyLabsJoseBundleExtension.php +++ b/DependencyInjection/SpomkyLabsJoseBundleExtension.php @@ -12,7 +12,6 @@ namespace SpomkyLabs\JoseBundle\DependencyInjection; use Assert\Assertion; -use SpomkyLabs\JoseBundle\DependencyInjection\Source; use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -82,7 +81,7 @@ public function load(array $configs, ContainerBuilder $container) } /** - * {@inheritdoc} + * @return \SpomkyLabs\JoseBundle\DependencyInjection\Configuration */ public function getConfiguration(array $configs, ContainerBuilder $container) { @@ -110,9 +109,6 @@ private function initConfiguration(ContainerBuilder $container, array $config) } } - /** - * - */ private function addDefaultSources() { $this->addServiceSource(new Source\JWTCreatorSource()); diff --git a/Features/command.feature b/Features/command.feature index 0c154ba..6396b09 100644 --- a/Features/command.feature +++ b/Features/command.feature @@ -1,47 +1,187 @@ Feature: A Console Command to rotate keys - Scenario: I want to rotate a key that do not exist - When I run command "spomky-labs:jose:rotate-keys" with parameters + Scenario: I want to delete a key + When I run command "spomky-labs:jose:delete" with parameters """ { - "key": "jose.key.key10", - "ttl": 1800 + "service": "jose.key.key10" } """ - Then The command exit code should be null + Then The command exit code should be 0 And I should see """ - The key "jose.key.key10" does not exist and will be created. + Done. """ - Scenario: I want to rotate a key that is not expired - When I run command "spomky-labs:jose:rotate-keys" with parameters + Scenario: I want to delete a key set + When I run command "spomky-labs:jose:delete" with parameters """ { - "key": "jose.key.key10", - "ttl": 1800 + "service": "jose.key_set.auto_signature" } """ - Then The command exit code should be null + Then The command exit code should be 0 And I should see """ - The key "jose.key.key10" exists and is not expired. + Done. """ - Scenario: I want to rotate a key that expired - Given I wait 10 seconds - When I run command "spomky-labs:jose:rotate-keys" with parameters + Scenario: I want to delete a service that does not exist + When I run command "spomky-labs:jose:delete" with parameters """ { - "key": "jose.key.key10", - "ttl": 5 + "service": "not.a.service" } """ - Then The command exit code should be null + Then The command exit code should be 1 And I should see """ - The key "jose.key.key10" exists but expired. It will be updated. + The service "not.a.service" does not exist. + + """ + + Scenario: I want to delete a service that is not a rotatable service + When I run command "spomky-labs:jose:delete" with parameters + """ + { + "service": "jose.key_set.jwkset2" + } + """ + Then The command exit code should be 2 + And I should see + """ + The service "jose.key_set.jwkset2" is not a storable object. + + """ + + Scenario: I want to regen a key + When I run command "spomky-labs:jose:regen" with parameters + """ + { + "service": "jose.key.key10" + } + """ + Then The command exit code should be 0 + And I should see + """ + Done. + + """ + + Scenario: I want to regen a key set + When I run command "spomky-labs:jose:regen" with parameters + """ + { + "service": "jose.key_set.auto_signature" + } + """ + Then The command exit code should be 0 + And I should see + """ + Done. + + """ + + Scenario: I want to regen a service that does not exist + When I run command "spomky-labs:jose:regen" with parameters + """ + { + "service": "not.a.service" + } + """ + Then The command exit code should be 1 + And I should see + """ + The service "not.a.service" does not exist. + + """ + + Scenario: I want to regen a service that is not a rotatable service + When I run command "spomky-labs:jose:regen" with parameters + """ + { + "service": "jose.key_set.jwkset2" + } + """ + Then The command exit code should be 2 + And I should see + """ + The service "jose.key_set.jwkset2" is not a storable object. + + """ + + Scenario: I want to rotate a service that does not exist + When I run command "spomky-labs:jose:rotate" with parameters + """ + { + "service": "not.a.service" + } + """ + Then The command exit code should be 1 + And I should see + """ + The service "not.a.service" does not exist. + + """ + + Scenario: I want to rotate a key set + When I run command "spomky-labs:jose:rotate" with parameters + """ + { + "service": "jose.key_set.auto_signature", + "ttl": 3600 + } + """ + Then The command exit code should be 0 + And I should see + """ + The key set "jose.key_set.auto_signature" has not expired. + + """ + + Scenario: I want to rotate a key set + When I run command "spomky-labs:jose:rotate" with parameters + """ + { + "service": "jose.key_set.auto_signature", + "ttl": 0 + } + """ + Then The command exit code should be 0 + And I should see + """ + Done. + + """ + + Scenario: I want to rotate a service that is ot a key set + When I run command "spomky-labs:jose:rotate" with parameters + """ + { + "service": "jose.key.key10", + "ttl": 0 + } + """ + Then The command exit code should be 2 + And I should see + """ + The service "jose.key.key10" is not a key set. + + """ + + Scenario: I want to rotate a service that is ot a rotatable key set + When I run command "spomky-labs:jose:rotate" with parameters + """ + { + "service": "jose.key_set.jwkset2", + "ttl": 0 + } + """ + Then The command exit code should be 3 + And I should see + """ + The service "jose.key_set.jwkset2" is not a rotatable key set. """ diff --git a/Features/configuration_helper_services.feature b/Features/configuration_helper_services.feature new file mode 100644 index 0000000..3ca4e20 --- /dev/null +++ b/Features/configuration_helper_services.feature @@ -0,0 +1,24 @@ +Feature: The configuration helper allow developers to create services easily + + Scenario: A Rotatable Key Set created using the configuration helper contains the expected number of keys + When the service "jose.key_set.from_configuration_helper" should be an object that implements "\Jose\Object\RotatableInterface" + When the service "jose.key_set.from_configuration_helper" should be an object that implements "\Jose\Object\StorableInterface" + When the service "jose.key_set.from_configuration_helper" should be an object that implements "\Jose\Object\JWKSetInterface" + And the keyset in the service "jose.key_set.from_configuration_helper" contains 2 keys + + Scenario: A Rotatable Key Set created using the configuration helper contains the expected number of keys + When the service "jose.key_set.all_in_one_from_configuration_helper" should be an object that implements "\Jose\Object\JWKSetInterface" + And the keyset in the service "jose.key_set.all_in_one_from_configuration_helper" contains 2 keys + + Scenario: A Rotatable Key Set created using the configuration helper contains the expected number of keys + When the service "jose.key_set.all_in_one_public_from_configuration_helper" should be an object that implements "\Jose\Object\JWKSetInterface" + And the keyset in the service "jose.key_set.all_in_one_public_from_configuration_helper" contains 2 keys + + Scenario: Services are available + And the service "jose.signer.from_configuration_helper" should be an object that implements "\Jose\SignerInterface" + And the service "jose.verifier.from_configuration_helper" should be an object that implements "\Jose\VerifierInterface" + And the service "jose.encrypter.from_configuration_helper" should be an object that implements "\Jose\EncrypterInterface" + And the service "jose.decrypter.from_configuration_helper" should be an object that implements "\Jose\DecrypterInterface" + And the service "jose.checker.from_configuration_helper" should be an object that implements "\Jose\Checker\CheckerManagerInterface" + And the service "jose.jwt_loader.from_configuration_helper" should be an object that implements "\Jose\JWTLoaderInterface" + And the service "jose.jwt_creator.from_configuration_helper" should be an object that implements "\Jose\JWTCreatorInterface" diff --git a/Features/keys_and_key_sets.feature b/Features/keys_and_key_sets.feature index 08008d2..4439c91 100644 --- a/Features/keys_and_key_sets.feature +++ b/Features/keys_and_key_sets.feature @@ -9,8 +9,12 @@ Feature: This bundle is able to use keys and key sets When the service "jose.key.key6" should be an object that implements "\Jose\Object\JWKInterface" Scenario: A Key is available through a service + When the service "jose.key.key10" should be an object that implements "\Jose\Object\StorableInterface" When the service "jose.key.key10" should be an object that implements "\Jose\Object\JWKInterface" + Scenario: A Key from a Key Set can be used as a service + When the service "jose.key.from_keyset" should be an object that implements "\Jose\Object\JWKInterface" + Scenario: A Key Set is available through a service When the service "jose.key_set.jwkset1" should be an object that implements "\Jose\Object\JWKSetInterface" And the keyset in the service "jose.key_set.jwkset1" contains 2 keys @@ -26,3 +30,19 @@ Feature: This bundle is able to use keys and key sets Scenario: A Key Set is available through a service and loaded from an URL When the service "jose.key_set.jwkset4" should be an object that implements "\Jose\Object\JWKSetInterface" And the keyset in the service "jose.key_set.jwkset4" contains keys + + Scenario: A Rotatable Key Set contains the expected number of keys + When the service "jose.key_set.auto_signature" should be an object that implements "\Jose\Object\RotatableInterface" + When the service "jose.key_set.auto_signature" should be an object that implements "\Jose\Object\StorableInterface" + When the service "jose.key_set.auto_signature" should be an object that implements "\Jose\Object\JWKSetInterface" + And the keyset in the service "jose.key_set.auto_signature" contains 5 keys + + Scenario: A Key Set of Key Sets contains the expected number of keys + When the service "jose.key_set.all_in_one" should be an object that implements "\Jose\Object\JWKSets" + When the service "jose.key_set.all_in_one" should be an object that implements "\Jose\Object\JWKSetInterface" + And the keyset in the service "jose.key_set.all_in_one" contains 7 keys + + Scenario: A Public Key Set contains the expected number of keys + When the service "jose.key_set.all_in_one_public" should be an object that implements "\Jose\Object\PublicJWKSet" + When the service "jose.key_set.all_in_one_public" should be an object that implements "\Jose\Object\JWKSetInterface" + And the keyset in the service "jose.key_set.all_in_one_public" contains 7 keys diff --git a/Helper/ConfigurationHelper.php b/Helper/ConfigurationHelper.php index 5f70c64..60af331 100644 --- a/Helper/ConfigurationHelper.php +++ b/Helper/ConfigurationHelper.php @@ -10,113 +10,250 @@ */ namespace SpomkyLabs\JoseBundle\Helper; + use Assert\Assertion; +use Symfony\Component\DependencyInjection\ContainerBuilder; /** - * This helper will help you to create services configuration + * This helper will help you to create services configuration. */ final class ConfigurationHelper { + const BUNDLE_ALIAS = 'jose'; + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param string $name + * @param string[] $header_checkers + * @param string[] $claim_checkers + * @param bool $is_public + */ + public static function addChecker(ContainerBuilder $container, $name, array $header_checkers, array $claim_checkers, $is_public = true) + { + $config = self::getCheckerConfiguration($name, $header_checkers, $claim_checkers, $is_public); + self::updateJoseConfiguration($container, $config, 'checkers'); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param string $name + * @param string[] $signature_algorithms + * @param bool $create_verifier + * @param bool $is_public + */ + public static function addSigner(ContainerBuilder $container, $name, array $signature_algorithms, $create_verifier = false, $is_public = true) + { + $config = self::getSignerConfiguration($name, $signature_algorithms, $create_verifier, $is_public); + self::updateJoseConfiguration($container, $config, 'signers'); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param string $name + * @param string[] $signature_algorithms + * @param bool $is_public + */ + public static function addVerifier(ContainerBuilder $container, $name, array $signature_algorithms, $is_public = true) + { + $config = self::getVerifierConfiguration($name, $signature_algorithms, $is_public); + self::updateJoseConfiguration($container, $config, 'verifiers'); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param string $name + * @param string[] $key_encryption_algorithms + * @param string[] $content_encryption_algorithms + * @param string[] $compression_methods + * @param bool $create_decrypter + * @param bool $is_public + */ + public static function addEncrypter(ContainerBuilder $container, $name, array $key_encryption_algorithms, array $content_encryption_algorithms, array $compression_methods = ['DEF'], $create_decrypter = false, $is_public = true) + { + $config = self::getEncrypterConfiguration($name, $key_encryption_algorithms, $content_encryption_algorithms, $compression_methods, $create_decrypter, $is_public); + self::updateJoseConfiguration($container, $config, 'encrypters'); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param string $name + * @param string[] $key_encryption_algorithms + * @param string[] $content_encryption_algorithms + * @param string[] $compression_methods + * @param bool $is_public + */ + public static function addDecrypter(ContainerBuilder $container, $name, array $key_encryption_algorithms, array $content_encryption_algorithms, array $compression_methods = ['DEF'], $is_public = true) + { + $config = self::getDecrypterConfiguration($name, $key_encryption_algorithms, $content_encryption_algorithms, $compression_methods, $is_public); + self::updateJoseConfiguration($container, $config, 'decrypters'); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param string $name + * @param string $signer + * @param string|null $encrypter + * @param bool $is_public + */ + public static function addJWTCreator(ContainerBuilder $container, $name, $signer, $encrypter = null, $is_public = true) + { + $config = self::getJWTCreatorConfiguration($name, $signer, $encrypter, $is_public); + self::updateJoseConfiguration($container, $config, 'jwt_creators'); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param string $name + * @param string $verifier + * @param string $checker + * @param string|null $decrypter + * @param bool $is_public + */ + public static function addJWTLoader(ContainerBuilder $container, $name, $verifier, $checker, $decrypter = null, $is_public = true) + { + $config = self::getJWTLoaderConfiguration($name, $verifier, $checker, $decrypter, $is_public); + self::updateJoseConfiguration($container, $config, 'jwt_loaders'); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param string $name + * @param string $storage_path + * @param int $nb_keys + * @param array $key_configuration + * @param bool $is_rotatable + * @param bool $is_public + */ + public static function addRandomJWKSet(ContainerBuilder $container, $name, $storage_path, $nb_keys, array $key_configuration, $is_rotatable = false, $is_public = true) + { + $config = self::getRandomJWKSetConfiguration($name, $storage_path, $nb_keys, $key_configuration, $is_rotatable, $is_public); + self::updateJoseConfiguration($container, $config, 'key_sets'); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param string $name + * @param string $jwkset + * @param bool $is_public + */ + public static function addPublicJWKSet(ContainerBuilder $container, $name, $jwkset, $is_public = true) + { + $config = self::getPublicJWKSetConfiguration($name, $jwkset, $is_public); + self::updateJoseConfiguration($container, $config, 'key_sets'); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param string $name + * @param string[] $jwksets + * @param bool $is_public + */ + public static function addJWKSets(ContainerBuilder $container, $name, array $jwksets, $is_public = true) + { + $config = self::getJWKSetsConfiguration($name, $jwksets, $is_public); + self::updateJoseConfiguration($container, $config, 'key_sets'); + } + /** * @param string $name * @param string[] $header_checkers * @param string[] $claim_checkers + * @param bool $is_public * * @return array */ - public static function getCheckerConfiguration($name, array $header_checkers, array $claim_checkers, $is_public = true) + private static function getCheckerConfiguration($name, array $header_checkers, array $claim_checkers, $is_public = true) { - Assertion::string($name); - Assertion::notEmpty($name); + self::checkParameters($name, $is_public); Assertion::allString($header_checkers); Assertion::allString($claim_checkers); + return [ - 'jose' => [ + self::BUNDLE_ALIAS => [ 'checkers' => [ $name => [ 'is_public' => $is_public, 'claims' => $claim_checkers, 'headers' => $header_checkers, - ] - ] - ] + ], + ], + ], ]; } /** - * @param string $name - * @param string[] $signature_algorithms - * - * @param bool $create_verifier + * @param string $name + * @param string[] $signature_algorithms + * @param bool $create_verifier + * @param bool $is_public * * @return array */ - public static function getSignerConfiguration($name, array $signature_algorithms, $create_verifier = false, $is_public = true) + private static function getSignerConfiguration($name, array $signature_algorithms, $create_verifier = false, $is_public = true) { - Assertion::string($name); - Assertion::notEmpty($name); + self::checkParameters($name, $is_public); Assertion::allString($signature_algorithms); Assertion::notEmpty($signature_algorithms); Assertion::boolean($create_verifier); + return [ - 'jose' => [ + self::BUNDLE_ALIAS => [ 'signers' => [ $name => [ 'is_public' => $is_public, 'algorithms' => $signature_algorithms, 'create_verifier' => $create_verifier, - ] - ] - ] + ], + ], + ], ]; } /** - * @param string $name - * @param string[] $signature_algorithms - * + * @param string $name + * @param string[] $signature_algorithms + * @param bool $is_public * * @return array */ - public static function getVerifierConfiguration($name, array $signature_algorithms, $is_public = true) + private static function getVerifierConfiguration($name, array $signature_algorithms, $is_public = true) { - Assertion::string($name); - Assertion::notEmpty($name); + self::checkParameters($name, $is_public); Assertion::allString($signature_algorithms); Assertion::notEmpty($signature_algorithms); + return [ - 'jose' => [ + self::BUNDLE_ALIAS => [ 'verifiers' => [ $name => [ 'is_public' => $is_public, 'algorithms' => $signature_algorithms, - ] - ] - ] + ], + ], + ], ]; } /** - * @param string $name - * @param string[] $key_encryption_algorithms - * @param string[] $content_encryption_algorithms - * @param string[] $compression_methods - * - * @param bool $create_decrypter + * @param string $name + * @param string[] $key_encryption_algorithms + * @param string[] $content_encryption_algorithms + * @param string[] $compression_methods + * @param bool $create_decrypter + * @param bool $is_public * * @return array */ - public static function getEncrypterConfiguration($name, array $key_encryption_algorithms, array $content_encryption_algorithms, array $compression_methods = ['DEF'], $create_decrypter = false, $is_public = true) + private static function getEncrypterConfiguration($name, array $key_encryption_algorithms, array $content_encryption_algorithms, array $compression_methods = ['DEF'], $create_decrypter = false, $is_public = true) { - Assertion::string($name); - Assertion::notEmpty($name); + self::checkParameters($name, $is_public); Assertion::allString($key_encryption_algorithms); Assertion::notEmpty($key_encryption_algorithms); Assertion::allString($content_encryption_algorithms); Assertion::notEmpty($content_encryption_algorithms); Assertion::boolean($create_decrypter); + return [ - 'jose' => [ + self::BUNDLE_ALIAS => [ 'encrypters' => [ $name => [ 'is_public' => $is_public, @@ -124,40 +261,40 @@ public static function getEncrypterConfiguration($name, array $key_encryption_al 'content_encryption_algorithms' => $content_encryption_algorithms, 'compression_methods' => $compression_methods, 'create_decrypter' => $create_decrypter, - ] - ] - ] + ], + ], + ], ]; } /** - * @param string $name - * @param string[] $key_encryption_algorithms - * @param string[] $content_encryption_algorithms - * @param string[] $compression_methods - * + * @param string $name + * @param string[] $key_encryption_algorithms + * @param string[] $content_encryption_algorithms + * @param string[] $compression_methods + * @param bool $is_public * * @return array */ - public static function getDecrypterConfiguration($name, array $key_encryption_algorithms, array $content_encryption_algorithms, array $compression_methods = ['DEF'], $is_public = true) + private static function getDecrypterConfiguration($name, array $key_encryption_algorithms, array $content_encryption_algorithms, array $compression_methods = ['DEF'], $is_public = true) { - Assertion::string($name); - Assertion::notEmpty($name); + self::checkParameters($name, $is_public); Assertion::allString($key_encryption_algorithms); Assertion::notEmpty($key_encryption_algorithms); Assertion::allString($content_encryption_algorithms); Assertion::notEmpty($content_encryption_algorithms); + return [ - 'jose' => [ + self::BUNDLE_ALIAS => [ 'decrypters' => [ $name => [ 'is_public' => $is_public, 'key_encryption_algorithms' => $key_encryption_algorithms, 'content_encryption_algorithms' => $content_encryption_algorithms, 'compression_methods' => $compression_methods, - ] - ] - ] + ], + ], + ], ]; } @@ -165,26 +302,27 @@ public static function getDecrypterConfiguration($name, array $key_encryption_al * @param string $name * @param string $signer * @param string|null $encrypter + * @param bool $is_public * * @return array */ - public static function getJWTCreatorConfiguration($name, $signer, $encrypter = null, $is_public = true) + private static function getJWTCreatorConfiguration($name, $signer, $encrypter = null, $is_public = true) { - Assertion::string($name); - Assertion::notEmpty($name); + self::checkParameters($name, $is_public); Assertion::string($signer); Assertion::notEmpty($signer); Assertion::nullOrString($encrypter); + return [ - 'jose' => [ + self::BUNDLE_ALIAS => [ 'jwt_creators' => [ $name => [ 'is_public' => $is_public, 'signer' => $signer, 'encrypter' => $encrypter, - ] - ] - ] + ], + ], + ], ]; } @@ -193,30 +331,160 @@ public static function getJWTCreatorConfiguration($name, $signer, $encrypter = n * @param string $verifier * @param string $checker * @param string|null $decrypter - * + * @param bool $is_public * * @return array */ - public static function getJWTLoaderConfiguration($name, $verifier, $checker, $decrypter = null, $is_public = true) + private static function getJWTLoaderConfiguration($name, $verifier, $checker, $decrypter = null, $is_public = true) { - Assertion::string($name); - Assertion::notEmpty($name); + self::checkParameters($name, $is_public); Assertion::string($verifier); Assertion::notEmpty($verifier); Assertion::string($checker); Assertion::notEmpty($checker); Assertion::nullOrString($decrypter); + return [ - 'jose' => [ + self::BUNDLE_ALIAS => [ 'jwt_loaders' => [ $name => [ 'is_public' => $is_public, 'verifier' => $verifier, 'checker' => $checker, 'decrypter' => $decrypter, - ] - ] - ] + ], + ], + ], ]; } + + /** + * @param string $name + * @param string $storage_path + * @param int $nb_keys + * @param array $key_configuration + * @param bool $is_rotatable + * @param bool $is_public + * + * @return array + */ + private static function getRandomJWKSetConfiguration($name, $storage_path, $nb_keys, array $key_configuration, $is_rotatable = false, $is_public = true) + { + self::checkParameters($name, $is_public); + Assertion::string($storage_path); + Assertion::notEmpty($storage_path); + Assertion::integer($nb_keys); + Assertion::greaterThan($nb_keys, 0); + Assertion::boolean($is_rotatable); + + return [ + self::BUNDLE_ALIAS => [ + 'key_sets' => [ + $name => [ + 'auto' => [ + 'is_rotatable' => $is_rotatable, + 'is_public' => $is_public, + 'nb_keys' => $nb_keys, + 'key_configuration' => $key_configuration, + 'storage_path' => $storage_path, + ], + ], + ], + ], + ]; + } + + /** + * @param string $name + * @param string $jwkset + * @param bool $is_public + * + * @return array + */ + private static function getPublicJWKSetConfiguration($name, $jwkset, $is_public = true) + { + self::checkParameters($name, $is_public); + Assertion::string($jwkset); + Assertion::notEmpty($jwkset); + + return [ + self::BUNDLE_ALIAS => [ + 'key_sets' => [ + $name => [ + 'public_jwkset' => [ + 'is_public' => $is_public, + 'id' => $jwkset, + ], + ], + ], + ], + ]; + } + + /** + * @param string $name + * @param string[] $jwksets + * @param bool $is_public + * + * @return array + */ + private static function getJWKSetsConfiguration($name, array $jwksets, $is_public = true) + { + self::checkParameters($name, $is_public); + Assertion::isArray($jwksets); + Assertion::allString($jwksets); + Assertion::allNotEmpty($jwksets); + + return [ + self::BUNDLE_ALIAS => [ + 'key_sets' => [ + $name => [ + 'jwksets' => [ + 'is_public' => $is_public, + 'id' => $jwksets, + ], + ], + ], + ], + ]; + } + + /** + * @param string $name + * @param bool $is_public + */ + private static function checkParameters($name, $is_public) + { + Assertion::string($name); + Assertion::notEmpty($name); + Assertion::boolean($is_public); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param array $config + * @param string $element + */ + private static function updateJoseConfiguration(ContainerBuilder $container, array $config, $element) + { + self::checkJoseBundleEnabled($container); + $jose_config = current($container->getExtensionConfig(self::BUNDLE_ALIAS)); + if (!isset($jose_config[$element])) { + $jose_config[$element] = []; + } + $jose_config[$element] = array_merge($jose_config[$element], $config[self::BUNDLE_ALIAS][$element]); + $container->prependExtensionConfig(self::BUNDLE_ALIAS, $jose_config); + } + + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * + * @throws \InvalidArgumentException + */ + private static function checkJoseBundleEnabled(ContainerBuilder $container) + { + $bundles = $container->getParameter('kernel.bundles'); + Assertion::keyExists($bundles, 'SpomkyLabsJoseBundle', 'The "Spomky-Labs/JoseBundle" must be enabled.'); + + } } diff --git a/README.md b/README.md index ddf15bd..3f18ba1 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,6 @@ Jose Bundle This Symfony bundle provides services to create, load, verify or decrypt JWT. It uses [spomky-Labs/jose](https://github.com/Spomky-Labs/jose) to ease encryption/decryption and signature/verification of JWS and JWE. -> Note 1: this bundle is still in development. The first stable release will be tagged as v1.0.x. All tags v0.x.y must be considered as unstable. - # The Release Process The release process [is described here](doc/Release.md). @@ -77,9 +75,20 @@ This bundle needs to be configured. Please [see this page](Resources/doc/Configu Have a look at [this page](Resources/doc/Use.md) to know hot to configure and use this bundle. +# Bundle Integration + +This bundle provides a Configuration Helper. +This helper provides an easy way to create all services through the configuration of another bundle. + +Please read [this page](Resources/doc/config/configuration_helper.md) to know how to easily configure the bundle from another bundle. + + # Contributing -Requests for new features, bug fixed and all other ideas to make this library useful are welcome. [Please follow these best practices](Resources/doc/Contributing.md). +Requests for new features, bug fixed and all other ideas to make this library useful are welcome. +The best contribution you could provide is by fixing the [opened issues where help is wanted](https://github.com/Spomky-Labs/JoseBundle/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) + +Please make sure to [follow these best practices](Resources/doc/Contributing.md). # Licence diff --git a/Resources/config/encryption_algorithms.xml b/Resources/config/encryption_algorithms.xml index 94ffaa9..064e9a2 100644 --- a/Resources/config/encryption_algorithms.xml +++ b/Resources/config/encryption_algorithms.xml @@ -6,74 +6,74 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/Resources/config/jwk_sources.xml b/Resources/config/jwk_sources.xml index ea5a572..2b17bb4 100644 --- a/Resources/config/jwk_sources.xml +++ b/Resources/config/jwk_sources.xml @@ -5,43 +5,47 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - + - + - + - + - + - + - + - + - + - + + + + + diff --git a/Resources/config/jwkset_sources.xml b/Resources/config/jwkset_sources.xml index 5f3e351..0d2e197 100644 --- a/Resources/config/jwkset_sources.xml +++ b/Resources/config/jwkset_sources.xml @@ -5,16 +5,25 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - + - + - + - + + + + + + + + + + diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 9d5c092..f2d4e3b 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -5,21 +5,24 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - - - - - - - - - + + + + - - - - + + + + + + + + + + + + + diff --git a/Resources/config/signature_algorithms.xml b/Resources/config/signature_algorithms.xml index 1cee2e1..e49bc54 100644 --- a/Resources/config/signature_algorithms.xml +++ b/Resources/config/signature_algorithms.xml @@ -6,51 +6,51 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/Resources/doc/Configuration.md b/Resources/doc/Configuration.md index 7b6accf..f946a28 100644 --- a/Resources/doc/Configuration.md +++ b/Resources/doc/Configuration.md @@ -3,17 +3,18 @@ Configuration # Keys and Key Sets -Encryption/Decryption and Signature/Verification require keys or key sets to be done. +Encryption/Decryption and Signature/Verification require **keys** or **key sets** to be done. This bundle is able to load keys and key sets from various sources such as files (encrypted or not), certificates, URLs or values. -When loaded, the keys or key sets are available though services. - -Please read [this page](config/keys.md) to know how to load your keys and [this one](config/key_sets.md) for your key sets. +When loaded, the keys and key sets are available though services. +Please read the following pages: +- [this page](config/keys.md) to know how to load your keys +- [this one](config/key_sets.md) for your key sets. # Signers and Verifiers -The Signers and Verifiers services are used to sign and verify JWS objects. +The Signers and Verifiers services are used to **sign** and **verify** JWS objects. You can create multiple services depending on your needs. For each service, selected algorithms may be different. For example, you need a Signer to sign a JWS to be sent to clients using HS512 algorithm only and you need a Verifier @@ -21,20 +22,18 @@ to verify requests from clients that use RS256 or ES384 algorithms. Please read [this page](config/signers_and_verifiers.md) to know how to create your Signers and Verifiers Services. - # Checkers -The loaded JWS may contain claims such as expiration date, issuer... In this case, you must verify them before use it. +The loaded JWS may contain claims such as expiration date, issuer... In this case, you must **verify** those claims before to use the JWS. Checker managers can be created automatically using this bundle and are available though services. Please read [this page](config/checkers.md) to know how to create your Checker Manager Services. - # Encrypters and Decrypters -The Encrypters and Decrypters services are used to encrypt and decrypt JWE objects. +The Encrypters and Decrypters services are used to **encrypt** and **decrypt** JWE objects. -Like Signers and Verifiers, yu can create multiple services depending on your needs. +Like Signers and Verifiers, you can create multiple services depending on your needs. For each service, selected algorithms and compression methods may be different. Please read [this page](config/encrypters_and_decrypters.md) to know how to create your Encrypters and Decrypters Services. diff --git a/Resources/doc/Keys.md b/Resources/doc/Keys.md deleted file mode 100644 index 26cb758..0000000 --- a/Resources/doc/Keys.md +++ /dev/null @@ -1,193 +0,0 @@ -Keys and Key Sets -================= - -# Load Your Keys - -This bundle allows you to load keys from various sources: a certificate, a public or private key (may be encrypted), values... -Your keys are identified by an ID and, when loaded, are available as a service container so that you can inject or get them easily. - -The service create is `jose.key.YOUR_KEY_ID`. - -## Key From a Public/Private Key File - -The following configuration allows you to load a key from a key file. This key can be a RSA or an EC key. Both PEM and DER formats are supported. -If the private key is encrypted, your configuration must indicate the password to decrypt it. - -```yml -jose: - keys: - YOUR_KEY_ID: # This key ID must be unique for each key - file: # The key is loaded from a key file - path: "%kernel.root_dir%/Keys/EC/private.es256.encrypted.key" # The path to your file - password: "test" # The password of your key (if encrypted) - additional_values: # Additional information to set - kid: "PRIVATE_KEY_#42" # We recommend you to set "kid" (the key ID), "use" ("sig" or "enc") and "alg" (algorithm) - use: "enc" - alg: "RSA-OAEP-256" -``` - -## Key From a Certificate File - -The following configuration allows you to load a key from a certificate file. - -```yml -jose: - keys: - YOUR_KEY_ID: # This key ID must be unique for each key - certificate: # The key is loaded from a certificate file - path: "%kernel.root_dir%/certificates/RSA/DER/8192b-rsa-example-cert.der" - additional_values: # Additional information to set - kid: "abcd-efgh-ijkl-mnop" - use: "sig" - alg: "RS512" -``` - -## Key From a Certificate Chain - -The following configuration allows you to load a key from a certificate chain. - -```yml -jose: - keys: - YOUR_KEY_ID: # This key ID must be unique for each key - x5c: # The key is loaded from a certificate chain - value: | # The certificate chain on multilines - -----BEGIN CERTIFICATE----- - MIID8DCCAtigAwIBAgIDAjqDMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNVBAYTAlVT - MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i - YWwgQ0EwHhcNMTMwNDA1MTUxNTU2WhcNMTYxMjMxMjM1OTU5WjBJMQswCQYDVQQG - EwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy - bmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB - AJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP - VaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv - h8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE - ahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ - EASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC - DTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB5zCB5DAfBgNVHSMEGDAWgBTAephojYn7 - qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wDgYD - VR0PAQH/BAQDAgEGMC4GCCsGAQUFBwEBBCIwIDAeBggrBgEFBQcwAYYSaHR0cDov - L2cuc3ltY2QuY29tMBIGA1UdEwEB/wQIMAYBAf8CAQAwNQYDVR0fBC4wLDAqoCig - JoYkaHR0cDovL2cuc3ltY2IuY29tL2NybHMvZ3RnbG9iYWwuY3JsMBcGA1UdIAQQ - MA4wDAYKKwYBBAHWeQIFATANBgkqhkiG9w0BAQsFAAOCAQEAqvqpIM1qZ4PtXtR+ - 3h3Ef+AlBgDFJPupyC1tft6dgmUsgWM0Zj7pUsIItMsv91+ZOmqcUHqFBYx90SpI - hNMJbHzCzTWf84LuUt5oX+QAihcglvcpjZpNy6jehsgNb1aHA30DP9z6eX0hGfnI - Oi9RdozHQZJxjyXON/hKTAAj78Q1EK7gI4BzfE00LshukNYQHpmEcxpw8u1VDu4X - Bupn7jLrLN1nBz/2i8Jw3lsA5rsb0zYaImxssDVCbJAJPZPpZAkiDoUGn8JzIdPm - X4DkjYUiOnMDsWCOrmji9D6X52ASCWg23jrW4kOVWzeBkoEfu43XrVJkFleW2V40 - fsg12A== - -----END CERTIFICATE----- - -----BEGIN CERTIFICATE----- - MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT - MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i - YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG - EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg - R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 - 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq - fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv - iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU - 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ - bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW - MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA - ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l - uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn - Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS - tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF - PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un - hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV - 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== - -----END CERTIFICATE----- - additional_values: # Additional information to set - kid: "123-456-789" - use: "sig" - alg: "RS256" -``` - -## Key From Values - -The following configuration allows you to load a key from values. - -```yml -jose: - keys: - YOUR_KEY_ID: # This key ID must be unique for each key - values: # The key is loaded from values - values: # Values of the key (hereafter an RSA private key) - kty: "RSA" - n: "tpS1ZmfVKVP5KofIhMBP0tSWc4qlh6fm2lrZSkuKxUjEaWjzZSzs72gEIGxraWusMdoRuV54xsWRyf5KeZT0S-I5Prle3Idi3gICiO4NwvMk6JwSBcJWwmSLFEKyUSnB2CtfiGc0_5rQCpcEt_Dn5iM-BNn7fqpoLIbks8rXKUIj8-qMVqkTXsEKeKinE23t1ykMldsNaaOH-hvGti5Jt2DMnH1JjoXdDXfxvSP_0gjUYb0ektudYFXoA6wekmQyJeImvgx4Myz1I4iHtkY_Cp7J4Mn1ejZ6HNmyvoTE_4OuY1uCeYv4UyXFc1s1uUyYtj4z57qsHGsS4dQ3A2MJsw" - e: "AQAB" - p: "5BGU1c7af_5sFyfsa-onIJgo5BZu8uHvz3Uyb8OA0a-G9UPO1ShLYjX0wUfhZcFB7fwPtgmmYAN6wKGVce9eMAbX4PliPk3r-BcpZuPKkuLk_wFvgWAQ5Hqw2iEuwXLV0_e8c2gaUt_hyMC5-nFc4v0Bmv6NT6Pfry-UrK3BKWc" - d: "Kp0KuZwCZGL1BLgsVM-N0edMNitl9wN5Hf2WOYDoIqOZNAEKzdJuenIMhITJjRFUX05GVL138uyp2js_pqDdY9ipA7rAKThwGuDdNphZHech9ih3DGEPXs-YpmHqvIbCd3GoGm38MKwxYkddEpFnjo8rKna1_BpJthrFxjDRhw9DxJBycOdH2yWTyp62ZENPvneK40H2a57W4QScTgfecZqD59m2fGUaWaX5uUmIxaEmtGoJnd9RE4oywKhgN7_TK7wXRlqA4UoRPiH2ACrdU-_cLQL9Jc0u0GqZJK31LDbOeN95QgtSCc72k3Vtzy3CrVpp5TAA67s1Gj9Skn-CAQ" - q: "zPD-B-nrngwF-O99BHvb47XGKR7ON8JCI6JxavzIkusMXCB8rMyYW8zLs68L8JLAzWZ34oMq0FPUnysBxc5nTF8Nb4BZxTZ5-9cHfoKrYTI3YWsmVW2FpCJFEjMs4NXZ28PBkS9b4zjfS2KhNdkmCeOYU0tJpNfwmOTI90qeUdU" - dp: "aJrzw_kjWK9uDlTeaES2e4muv6bWbopYfrPHVWG7NPGoGdhnBnd70-jhgMEiTZSNU8VXw2u7prAR3kZ-kAp1DdwlqedYOzFsOJcPA0UZhbORyrBy30kbll_7u6CanFm6X4VyJxCpejd7jKNw6cCTFP1sfhWg5NVJ5EUTkPwE66M" - dq: "Swz1-m_vmTFN_pu1bK7vF7S5nNVrL4A0OFiEsGliCmuJWzOKdL14DiYxctvnw3H6qT2dKZZfV2tbse5N9-JecdldUjfuqAoLIe7dD7dKi42YOlTC9QXmqvTh1ohnJu8pmRFXEZQGUm_BVhoIb2_WPkjav6YSkguCUHt4HRd2YwE" - qi: "BocuCOEOq-oyLDALwzMXU8gOf3IL1Q1_BWwsdoANoh6i179psxgE4JXToWcpXZQQqub8ngwE6uR9fpd3m6N_PL4T55vbDDyjPKmrL2ttC2gOtx9KrpPh-Z7LQRo4BE48nHJJrystKHfFlaH2G7JxHNgMBYVADyttN09qEoav8Os" -``` - -## Key From a JWK String - -The following configuration allows you to load a key from a JWK string. - -```yml -jose: - keys: - YOUR_KEY_ID: # This key ID must be unique for each key - jwk: # The key is loaded from a JWK value - value: '{"kty":"EC","crv":"P-521","d":"Fp6KFKRiHIdR_7PP2VKxz6OkS_phyoQqwzv2I89-8zP7QScrx5r8GFLcN5mCCNJt3rN3SIgI4XoIQbNePlAj6vE","x":"AVpvo7TGpQk5P7ZLo0qkBpaT-fFDv6HQrWElBKMxcrJd_mRNapweATsVv83YON4lTIIRXzgGkmWeqbDr6RQO-1cS","y":"AIs-MoRmLaiPyG2xmPwQCHX2CGX_uCZiT3iOxTAJEZuUbeSA828K4WfAA4ODdGiB87YVShhPOkiQswV3LpbpPGhC","foo":"bar"}' -``` - -# Load Your Key Sets - -Key sets are very useful if you want to group all your keys. - - -As keys, this bundle allows you to load key sets from various sources: loaded keys, from an URL... -Your key sets are identified by an ID and, when loaded, are available as a service container so that you can inject or get them easily. - -The service create is `jose.key_set.YOUR_KEYSET_ID`. - -## Key Set From Loaded Keys - -If you loaded keys with key IDs `KEY1`, `KEY2` and `KEY3`. Services aliases are `jose.key.KEY1`, `jose.key.KEY2` and `jose.key.KEY3` respectively. - -```yml -jose: - key_sets: - YOUR_KEYSET_ID: # This key set ID must be unique for each key set - keys: # The key set is created using loaded keys - id: # The list of key IDs - - jose.key.KEY1 - - jose.key.KEY2 - - jose.key.KEY3 -``` - -## Key Set From a JWKSet String - -```yml -jose: - key_sets: - YOUR_KEYSET_ID: # This key set ID must be unique for each key set - jwkset: # The key set is loaded from a JWKSet string - value: '{"keys":[{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","use":"sign","key_ops":["sign"],"alg":"ES256","kid":"0123456789"},{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","d":"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI","use":"sign","key_ops":["verify"],"alg":"ES256","kid":"9876543210"}]}' -``` - -## Key Set From a X509 Url - -```yml -jose: - key_sets: - YOUR_KEYSET_ID: # This key set ID must be unique for each key set - x5u: # The key set is loaded from a X509 Url ("x5u" parameter) - url: "https://www.googleapis.com/oauth2/v1/certs" # Url of the key set - is_secured: true # If true, only secured connections are allowed (HTTPS with trusted certificate) -``` - -## Key Set From a JWKSet Url - -```yml -jose: - key_sets: - YOUR_KEYSET_ID: # This key set ID must be unique for each key set - jku: # The key set is loaded from a JWKSet Url ("jku" parameter) - url: "https://www.googleapis.com/oauth2/v2/certs" # Url of the key set - is_secured: true # If true, only secured connections are allowed (HTTPS with trusted certificate) -``` diff --git a/Resources/doc/Services.md b/Resources/doc/Services.md deleted file mode 100644 index e473d0f..0000000 --- a/Resources/doc/Services.md +++ /dev/null @@ -1,83 +0,0 @@ -Operation Services -================== - -This bundle is able to create services to sing, verify, encrypt and decrypt the JWS/JWE you receive. -For each service, you can set as many algorithms as you need. - -# Signer Service - -The Signer Service will help you to sign claims or a message and create a JWS. - -In the following example, we will create two Signer Services: -* The first one (`SIGNER1`) will support `HS256`, `RS256` and `RS512` algorithms -* The second one (`SIGNER2`) will only support `ES384` - -```yml -jose: - signers: - SIGNER1: - algorithms: - - 'HS256' - - 'RS256' - - 'RS512' - SIGNER2: - algorithms: - - 'ES384' -``` - -Now, the aliases `jose.signer.SIGNER1` and `jose.signer.SIGNER2` are available from the container. -You can call these services to sign payloads and create JWS. - - -# Verifier Service - -The Verifier Service will help you to verify JWS signatures. -*Please note that if the JWS contains claims, these claims are not verified. You must use the checker service to verify those claims*. - -In the following example, we will create two Verifier Services: -* The first one (`VERIFIER1`) will support `HS256`, `RS256` and `RS512` algorithms -* The second one (`VERIFIER2`) will only support `ES384` - -```yml -jose: - verifiers: - VERIFIER1: - algorithms: - - 'HS256' - - 'RS256' - - 'RS512' - VERIFIER2: - algorithms: - - 'ES384' -``` - -Now, the aliases `jose.verifier.VERIFIER1` and `jose.verifier.VERIFIER2` are available from the container. -You can call these services to verify JWS signatures. - -## Create Signer and Verifier Services at Once - -If you want to create Signer and Verifier Services with the same algorithms (see example above) at once, you can do it easily using a simple configuration paratemter: - -```yml -jose: - signers: - SERVICE1: - algorithms: - - 'HS256' - - 'RS256' - - 'RS512' - create_verifier: true - SERVICE2: - algorithms: - - 'ES384' -``` - -Now, the aliases `jose.signer.SERVICE1`, `jose.verifier.SERVICE1` and `jose.verifier.SERVICE2` are available from the container. - -# Checker Manager Service - -# Encrypter Service - -# Decrypter Service - - diff --git a/Resources/doc/Use.md b/Resources/doc/Use.md index cc3cbbc..44fe9b5 100644 --- a/Resources/doc/Use.md +++ b/Resources/doc/Use.md @@ -1,49 +1,2 @@ How to use ========== - -# Configure the Bundle - -## Load Your Keys And Key Sets - -Signature/Verification and Encryption/Decryption need keys to be performed. -The first step is to configure your keys in the configuration file. - -Please read [the dedicated page](Keys.md) to know how to load your keys and key sets. - -## Operation Services - -This bundle will help you to create operation services that exactly fit on your needs. - -For example, you may need to encrypt/decrypt JWE using `A256KW` and `A256GCM` algorithms and want to sign/verify JWS using `HS256` and `PS512` algorithms. -For internal communications, you just sign/verify JWS using `RS384`. - -Read [the dedicated page](Services.md) to know how to perform all these services. - -# JWS/JWE Creation and Loading - -Now you have keys, key sets and services, then let's create/load your first JWS/JWE objects. - -## - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Resources/doc/config/checkers.md b/Resources/doc/config/checkers.md new file mode 100644 index 0000000..62a1d5e --- /dev/null +++ b/Resources/doc/config/checkers.md @@ -0,0 +1,38 @@ +Checkers +======== + +Checkers will verify claims and headers key/value of all JWSInterface objects. + +Each Checker you create is available as a service you can inject in your own services or use from the container. +It will check the claims in the payload (if any) and headers you explicitly defined. + +In the following example, we create a checker that will be available through `jose.encrypter.CHECKER1`: + +```yml +jose: + checkers: + CHECKER1: # ID of the Signer. Must be unique + is_public: true # The service created by the bundle will be public (default) + claim_checkers: # This checker will check the following claims (see below for the complete list) + - 'exp' # Expiration claim + - 'iat' # Issued at claim + - 'nbf' # Not Before claim + header_checkers: # This checker will check the following headers (see below for the complete list) + - 'crit' # Critical header +``` + +# Supported Claim and Header Checkers + +Hereafter the list of all checkers supported by this library. + +You may need to check additional claims or headers, then [read that page](../next/custom_checker.md) to know how to create custom checkers. + +# Supported Claim Checkers + +* [x] `exp` +* [x] `iat` +* [x] `nbf` + +# Supported Header Checkers + +* [x] `crit` diff --git a/Resources/doc/config/configuration_helper.md b/Resources/doc/config/configuration_helper.md new file mode 100644 index 0000000..2815d27 --- /dev/null +++ b/Resources/doc/config/configuration_helper.md @@ -0,0 +1,186 @@ +# Bundle Integration + +This bundle may be used by other bundles to provide JWT support. +If your are in that case, then you will have to configure your bundle and the JoseBundle and your configuration file will become too verbose. + +That is why this bundle provides a [`ConfigurationHelper`](../../Helper/ConfigurationHelper.php) that will help your to modify the configuration of JoseBundle from your bundle. + +Let say you have a bundle that need a key set, a decrypter, a verfier and a claim checker to load and verify encrypted JWS. +Your public keys are share to allow third party applications to send you those JWT. + +Normally your configuration file should be something like that one: + +```yml +... +# The JoseBundle configuration +jose: + easy_jwt_loader: + main: + signature_algorithms: + - 'RS256' + key_encryption_algorithms: + - 'RSA-OAEP-256' + content_encryption_algorithms: + - 'A256GCM' + claim_checkers: + - 'exp' + - 'iat' + - 'nbf' + header_checkers: + - 'crit' + key_sets: + signature_keys: + auto: + storage_path: "%kernel.cache_dir%/signature_keys.keyset" + is_rotatable: true + nb_keys: 2 + key_configuration: + kty: 'RSA' + size: 4096 + alg: "RS256" + use: "sig" + encryption_keys: + auto: + storage_path: "%kernel.cache_dir%/encryption_keys.keyset" + is_rotatable: true + nb_keys: 2 + key_configuration: + kty: 'RSA' + size: 4096 + alg: "RSA-OAEP-256" + use: "enc" + all_keys: + jwksets: + id: + - 'jose.key_set.signature_keys' + - 'jose.key_set.encryption_keys' + all_public_keys: + public_jwkset: + id: 'jose.key_set.all_keys' +``` + +As you can see the configuration file is too heavy for the job to do. +Let's shrink all to lines. + +## Update your Bundle Extension + +To allow your bundle to get the advantages of the Configuration Helper, you have to implement the `Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface` in your bundle extension class. + +```php + 'RSA', + 'size' => 4096, + 'alg' => "RS256", + 'use' => "sig", + ], + true, // Is rotatable? + true // Is public? + ); + + //We add our encryption keys + ConfigurationHelper::addRandomJWKSet($container, 'encryption_keys', '%kernel.cache_dir%/encryption_keys.keyset', 2, ['kty' => 'RSA', 'size' => 4096, 'alg' => "RSA-OAEP-256", 'use' => "enc"], true, true); + + //And then we merge our key sets and get a public key set + ConfigurationHelper::addJWKSets($container, 'all_keys', ['jose.key_set.signature_keys', 'jose.key_set.encryption_keys']); + ConfigurationHelper::addPublicJWKSet($container, 'all_public_keys', 'jose.key_set.all_keys'); +} +``` + +## Checker, Verifier and JWT Loader + +The `easy_jwt_loader` configuration parameter is not available using the Configuration Helper. +We have to create services for the checker, the verifier and then the JWT Loader + +```php +use SpomkyLabs\JoseBundle\Helper\ConfigurationHelper; + +... +public function prepend(ContainerBuilder $container) +{ + //We add the checker (last argument set the service as private as we should not use it directly). + ConfigurationHelper::addChecker($container, 'main', ['crit'], ['exp', 'iat', 'nbf'], false); + + //We add our verifier (private) + ConfigurationHelper::addVerifier($container, 'main', ['RS256'], false); + + //We add our decrypter (private) + ConfigurationHelper::addDecrypter($container, 'main', ['RSA-OAEP-256'], ['A256GCM'], ['DEF'], false); + + //And then we can create our JWT Loader using all previously defined services. + //The loader is public as we could need it in our controller for example + ConfigurationHelper::addJWTLoader($container, 'main', 'jose.verifier.main', 'jose.checker.main', 'jose.decrypter.main'); +} +``` + +# Conclusion + +When it is possible, use the Configuration Helper as often as possible. The complete `prepend` method looks like: + +```php +public function prepend(ContainerBuilder $container) +{ + ConfigurationHelper::addRandomJWKSet($container, 'signature_keys', '%kernel.cache_dir%/signature_keys.keyset', 2, ['kty' => 'RSA', 'size' => 4096, 'alg' => "RS256", 'use' => "sig"], true, true); + ConfigurationHelper::addRandomJWKSet($container, 'encryption_keys', '%kernel.cache_dir%/encryption_keys.keyset', 2, ['kty' => 'RSA', 'size' => 4096, 'alg' => "RSA-OAEP-256", 'use' => "enc"], true, true); + ConfigurationHelper::addJWKSets($container, 'all_keys', ['jose.key_set.signature_keys', 'jose.key_set.encryption_keys']); + ConfigurationHelper::addPublicJWKSet($container, 'all_public_keys', 'jose.key_set.all_keys'); + ConfigurationHelper::addChecker($container, 'main', ['crit'], ['exp', 'iat', 'nbf'], false); + ConfigurationHelper::addVerifier($container, 'main', ['RS256'], false); + ConfigurationHelper::addDecrypter($container, 'main', ['RSA-OAEP-256'], ['A256GCM'], ['DEF'], false); + ConfigurationHelper::addJWTLoader($container, 'main', 'jose.verifier.main', 'jose.checker.main', 'jose.decrypter.main'); +} +``` + +And how the JoseBundle configuration section looks like? + +```yml +``` + +**It's empty!** + +Those 10 lines of code are better thant the 40+ configuration lines the user has to set. + +As you noted, we use hardcoded values, but you are free to use your own configurable values. In this case, all those values can be options defined in your bundle configuration. diff --git a/Resources/doc/config/encrypters_and_decrypters.md b/Resources/doc/config/encrypters_and_decrypters.md index 3d5ca82..c42a80d 100644 --- a/Resources/doc/config/encrypters_and_decrypters.md +++ b/Resources/doc/config/encrypters_and_decrypters.md @@ -3,6 +3,126 @@ Encrypters and Decrypters Services # Encrypters +An Encrypter is a service that provides methods to encrypt payloads according to the headers (protected or unprotected) and public or shared keys. + +Each Encrypter you create is available as a service you can inject in your own services or use from the container. +It is allowed to use a set of algorithms you explicitly defined. + +In the following example, we create an Encrypter that will be available through `jose.encrypter.ENCRYPTER1`: + +```yml +jose: + encrpters: + ENCRYPTER1: # ID of the Encrypter. Must be unique + is_public: true # The service created by the bundle will be public (default) + key_encryption_algorithms: # A list of algorithms used for the encryption of the key (see below for the complete list) + - '128GCMKW' + - 'A256GCMKW' + - 'RSA-OAEP' + content_encryption_algorithms: # A list of algorithms used for the encryption of the content (see below for the complete list) + - 'A128GCM' + - 'A256GCM' + compression_methods: # A list of compression methods (see below for the complete list) + - 'DEF' # Deflate compression mode (set by default) +``` # Decrypters +A Decrypter is a service that provides functions to decrypt JWE you received using private or shared keys. + +As Encrypters, each Decrypter you create is available as a service you can inject in your own services or use from the container. +It is allowed to use a set of algorithms you explicitly defined. + +In the following example, we create a Decrypter. It will be available through `jose.verifier.DECRYPTER1`: + +```yml +jose: + decrypters: + DECRYPTER1: # ID of the Decrypter. Must be unique + is_public: true # The service created by the bundle will be public (default) + key_encryption_algorithms: # A list of algorithms used for the encryption of the key (see below for the complete list) + - '128GCMKW' + - 'A256GCMKW' + - 'RSA-OAEP' + content_encryption_algorithms: # A list of algorithms used for the encryption of the content (see below for the complete list) + - 'A128GCM' + - 'A256GCM' + compression_methods: # A list of compression methods (see below for the complete list) + - 'DEF' # Deflate compression mode (set by default) +``` + +# Encrypter & Decrypter at Once + +In some cases, you will need a encrypt and a decrypt with the same set of algorithms. +There is a configuration option `create_decrypter` you can use to create both services at once: + +```yml +jose: + encrypters: + SERVICE1: # ID of the Encrypter. Must be unique + is_public: true # The service created by the bundle will be public (default) + create_decrypter: true + key_encryption_algorithms: # A list of algorithms used for the encryption of the key (see below for the complete list) + - '128GCMKW' + - 'A256GCMKW' + - 'RSA-OAEP' + content_encryption_algorithms: # A list of algorithms used for the encryption of the content (see below for the complete list) + - 'A128GCM' + - 'A256GCM' + compression_methods: # A list of compression methods (see below for the complete list) + - 'DEF' # Deflate compression mode (set by default) +``` + +This will automatically create the services `jose.encrypter.SERVICE1` and `jose.decrypter.SERVICE1`. +Both services will support the same encryption algorithms and compression methods. + +# Supported Encryption Algorithms + +Hereafter the list of all algorithms supported by this library. + +You may need an additional algorithm, then [read that page](../next/custom_algorithm.md) to know how to create custom algorithms. + +## Key Encryption Algorithms + +* [x] `dir` +* [x] `RSA1_5` +* [x] `RSA-OAEP` +* [x] `RSA-OAEP-256` +* [x] `ECDH-ES` +* [x] `ECDH-ES+A128KW` +* [x] `ECDH-ES+A192KW` +* [x] `ECDH-ES+A256KW` +* [x] `A128KW` +* [x] `A192KW` +* [x] `A256KW` +* [x] `PBES2-HS256+A128KW` +* [x] `PBES2-HS384+A192KW` +* [x] `PBES2-HS512+A256KW` +* [x] `A128GCMKW` (for performance, this [third party extension is highly recommended](https://github.com/bukka/php-crypto)) +* [x] `A192GCMKW` (for performance, this [third party extension is highly recommended](https://github.com/bukka/php-crypto)) +* [x] `A256GCMKW` (for performance, this [third party extension is highly recommended](https://github.com/bukka/php-crypto)) +* [x] `EdDSA` + * [x] With `X25519` curve ([third party extension required](https://github.com/encedo/php-curve25519-ext)) + * [ ] With `X448` curve + +*Please note that the [EdDSA encryption algorithm specification](https://tools.ietf.org/html/draft-ietf-jose-cfrg-curves) +is not not yet approved. Support for the algorithm `EdDSA` with `X25518` and `X448` curves may change. Use with caution.* + +## Supported Content Encryption Algorithms + +* [x] `A128CBC-HS256` +* [x] `A192CBC-HS384` +* [x] `A256CBC-HS512` +* [x] `A128GCM` (for performance, this [third party extension is highly recommended](https://github.com/bukka/php-crypto)) +* [x] `A192GCM` (for performance, this [third party extension is highly recommended](https://github.com/bukka/php-crypto)) +* [x] `A256GCM` (for performance, this [third party extension is highly recommended](https://github.com/bukka/php-crypto)) + +# Supported Compression Methods + +Hereafter the list of all compression methods supported by this library. + +You may need an additional compression method, then [read that page](../next/custom_compression_method.md) to know how to create custom compression methods. + +* [x] `DEF`: Deflate +* [x] `ZLIB`: ZLib (not described in the RFCs, for internal use only) +* [x] `GZ`: GZip (not described in the RFCs, for internal use only) diff --git a/Resources/doc/config/jwtloader_and_jwtcreator.md b/Resources/doc/config/jwtloader_and_jwtcreator.md index 41668c6..d167a0f 100644 --- a/Resources/doc/config/jwtloader_and_jwtcreator.md +++ b/Resources/doc/config/jwtloader_and_jwtcreator.md @@ -3,6 +3,91 @@ JWT Creator and JWT Loader Services # JWT Creator +A JWT Creator service is a service that provides a `Jose\JWTCreatorInterface` object that allow you to easily +sign or encrypt (or both) at once and get a JWT in Compact Serialization Mode (the most common JWT representation). + +In fact, it is just a service that needs a `Signer` and, if encryption is needed, an `Encrypter`. + +In the following example, the JWT Creator service will be available through `jose.jwt_creator.jwt_CREATOR1`: + +```yml +jose: + jwt_creators: + CREATOR1: # ID of the JWT Creator. Must be unique + is_public: true # The service created by the bundle will be public (default) + signer: 'jose.signer.SIGNER1' # The name of the Signer service + encrypter: 'jose.encrypter.ENCRYPER1' # Optional. The name of the Encryper service. Only needed if you want to create Compact JWE +``` + +As you noted, you need to have a valid Signer service and optionally an Encrypter service. +This bundle provides another way to create a JWT Creator by setting the algorithms you need. +It will automatically create the Signer and the Encrypter (if needed) with all algorithms you selected. + +In the following example, the JWT Creator service will be available through `jose.jwt_creator.jwt_CREATOR1`: + +```yml +jose: + easy_jwt_creator: + CREATOR1: + is_public: true + signature_algorithms: + - 'HS256' + - 'HS384' + - 'HS512' + key_encryption_algorithms: # Optional + - 'A256GCMKW' + - 'RSA-OAEP' + content_encryption_algorithms: # Optional + - 'A256GCM' + compression_methods: # Optional + - 'DEF' +``` # JWT Loader +A JWT Loader service is a service that provides a `Jose\JWTLoaderInterface` object that allow you to easily +decrypt (if decryption is supported) and verify at once and get a `Jose\JWSInterface` object. + +In fact, it is just a service that needs a `Decryper` (optional), a `Verifier` and a `Checker`. + +In the following example, the JWT Loader service will be available through `jose.jwt_loader.jwt_LOADER1`: + +```yml +jose: + jwt_loaders: + LOADER1: # ID of the JWT Loader. Must be unique + is_public: true # The service created by the bundle will be public (default) + verifier: 'jose.verifier.VERIFIER1' # The name of the Verifier service + checker: 'jose.checker.CHECKER1' # The name of the Checker service + decrypter: 'jose.decrypter.DECRYPER1' # Optional. The name of the Decryper service. Only needed if you want to load Compact JWE +``` + +As you noted, you need to have a valid Verifier and Checker services and optionally an Decrypter service. +This bundle provides another way to create a JWT Loader by setting the algorithms you need. +It will automatically create the Verifier, a Checker and the Decrypter (if needed) with all algorithms you selected. + +In the following example, the JWT Loader service will be available through `jose.jwt_loader.jwt_LOADER1`: + +```yml +jose: + easy_jwt_loader: + LOADER1: + is_public: true + signature_algorithms: + - 'HS256' + - 'HS384' + - 'HS512' + key_encryption_algorithms: # Optional + - 'A256GCMKW' + - 'RSA-OAEP' + content_encryption_algorithms: # Optional + - 'A256GCM' + header_checkers: # Optional + - 'crit' + claim_checkers: # Optional + - 'exp' + - 'iat' + - 'nbf' + compression_methods: # Optional + - 'DEF' +``` diff --git a/Resources/doc/config/key_sets.md b/Resources/doc/config/key_sets.md index 093aed4..ee1ba78 100644 --- a/Resources/doc/config/key_sets.md +++ b/Resources/doc/config/key_sets.md @@ -11,9 +11,9 @@ jose: keyset_id: # ID of the key set. When loaded, the service "jose.key_set.keyset_id" will be created keys: # Type of key set. In this case, the key set is created using keys previously loaded. id: - - key_id1 - - key_id2 - - key_id3 + - 'jose.key.key_id1' + - 'jose.key.key_id2' + - 'jose.key.key_id3' ``` # From a JWKSet @@ -36,7 +36,7 @@ The following example shows you how to load a key set from an URL that contains jose: key_sets: keyset_id: # ID of the key set. When loaded, the service "jose.key_set.keyset_id" will be created - jku: # Type of key. In this case, the key set is created from a serialized JWK. + jku: # Type of key set. In this case, the key set is created from a serialized JWK. url: "https://www.googleapis.com/oauth2/v2/certs" is_secured: true # If false, unsecured connections are allowed. Default is true ``` @@ -49,7 +49,123 @@ The following example shows you how to load a key set from an URL that contains jose: key_sets: keyset_id: # ID of the key set. When loaded, the service "jose.key_set.keyset_id" will be created - x5u: # Type of key. In this case, the key set is created from a serialized JWK. + x5u: # Type of key set. In this case, the key set is created from a serialized JWK. url: "https://www.googleapis.com/oauth2/v1/certs" is_secured: true # If false, unsecured connections are allowed. Default is true ``` + +# Random Key Set + +The following example shows you how to create a Key Set with random keys. +This key set is stored so that you can reuse it. + +All types of keys are supported, however it can only contains one type of keys. + +```yml +jose: + key_sets: + keyset_id: # ID of the key set. When loaded, the service "jose.key_set.keyset_id" will be created + auto: # Type of key set. In this case, the key set is created using random keys + storage_path: "%kernel.cache_dir%/auto_encryption.keyset" # The file where the key set is stored + is_rotatable: true # If true, then the key set can rotate its keys after a period of time + nb_keys: 2 # Number of keys in the key set + key_configuration: # Key configuration parameters. See the documentation for the random keys for more information. + kty: 'EC' # Type of the key. Supported types are RSA, EC, oct, none and OKP (if third party libraries are installed) + crv: 'P-521' # Parameters specific to the key type + alg: "ECDH-ES" + use: "enc" +``` + +Please read [this page](../use/commands.md) to know how to use console commands with this kind of key set. + +# Key Set of Key Sets + +In some context you may need to get a key set that contains several key sets. +For example you want to merge your signature an encryption keys. + +```yml +jose: + key_sets: + keyset_id: # ID of the key set. When loaded, the service "jose.key_set.keyset_id" will be created + jwksets: # Type of key set. In this case, the key set is created using key sets + id: + - 'jose.key_set.signature_keys' + - 'jose.key_set.encryption_keys' +``` + +# Public Keys of a Key Set + +In some context you may need to share your public keys with third party applications. +To avoid mistakes and to be sure that no private or symmetric keys are shared, you may need that kind od key set. + +```yml +jose: + key_sets: + keyset_id: # ID of the key set. When loaded, the service "jose.key_set.keyset_id" will be created + public_jwkset: # Type of key set. In this case, the key set is created using another key set and will show only public keys + id: 'jose.key_set.signature_keys' +``` + +# Key Sets Combinations + +The following example will show you how to combine random key sets to share signature and encryption public keys at once. + +```yml +jose: + key_sets: + signature_keys: # Our signature keys + auto: + storage_path: "%kernel.cache_dir%/signature_keys.keyset" + is_rotatable: true + nb_keys: 2 + key_configuration: + kty: 'RSA' + size: 4096 + alg: "RS256" + use: "sig" + encryption_keys: # Our encryption keys + auto: + storage_path: "%kernel.cache_dir%/encryption_keys.keyset" + is_rotatable: true + nb_keys: 2 + key_configuration: + kty: 'EC' + crv: 'P-521' + alg: "ECDH-ES" + use: "enc" + all_in_one: + jwksets: # Combine our signature and encryption keys + id: + - 'jose.key_set.signature_keys' + - 'jose.key_set.encryption_keys' + all_in_one_public: # Allow use to share all public keys at once + public_jwkset: + id: 'jose.key_set.all_in_one' +``` + +Now the service `jose.key_set.all_in_one_public` will contain all public keys and can be shared with third party applications. + +The recommended way is to provide a route to get that keys. + +Example within a controller action: + +```php +namespace AppBundle\Controller; + +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\JsonResponse; + +class KeySetController extends Controller +{ + /** + * @Route("/jwku") + */ + public function jwkuAction() + { + $jwkset = $this->get('jose.key_set.all_in_one_public'); + + return JsonResponse::create($jwkset); + } +} +``` diff --git a/Resources/doc/config/keys.md b/Resources/doc/config/keys.md index 3020ba1..3874834 100644 --- a/Resources/doc/config/keys.md +++ b/Resources/doc/config/keys.md @@ -13,6 +13,7 @@ jose: keys: key_id: # ID of the key. When loaded, the service "jose.key.key_id" will be created values: # Type of key. In this case, we create it using its values + is_public: true # Indicates the service will be public or private. This option is availble to all key sources values: # The list of values kty: "oct" kid: "018c0ae5-4d9b-471b-bfd6-eef314bc7037" @@ -28,6 +29,7 @@ jose: keys: key_id: # ID of the key. When loaded, the service "jose.key.key_id" will be created file: # Type of key. In this case, the key is stored in an file. + is_public: true # Indicates the service will be public or private. This option is availble to all key sources path: "/Path/To/The/file.key" # Path of the file password: "secret" # If the key is encrypted, this parameter is mandatory additional_values: # You can add custom values @@ -44,6 +46,7 @@ jose: keys: key_id: # ID of the key. When loaded, the service "jose.key.key_id" will be created certificate: # Type of key. In this case, the key is stored in an certificate. + is_public: true # Indicates the service will be public or private. This option is availble to all key sources path: "/Path/To/The/certificate.crt" # Path of the certificate additional_values: # You can add custom values kid: "CERT_ABCDE" @@ -59,9 +62,24 @@ jose: keys: key_id: # ID of the key. When loaded, the service "jose.key.key_id" will be created jwk: # Type of key. In this case, the key from a serialized JWK. + is_public: true # Indicates the service will be public or private. This option is availble to all key sources value: '{"kty":"EC","crv":"P-521","d":"Fp6KFKRiHIdR_7PP2VKxz6OkS_phyoQqwzv2I89-8zP7QScrx5r8GFLcN5mCCNJt3rN3SIgI4XoIQbNePlAj6vE","x":"AVpvo7TGpQk5P7ZLo0qkBpaT-fFDv6HQrWElBKMxcrJd_mRNapweATsVv83YON4lTIIRXzgGkmWeqbDr6RQO-1cS","y":"AIs-MoRmLaiPyG2xmPwQCHX2CGX_uCZiT3iOxTAJEZuUbeSA828K4WfAA4ODdGiB87YVShhPOkiQswV3LpbpPGhC","foo":"bar"}' ``` +# From a JWKSet + +The following example shows you how to load a key from a JWKSet. + +```yml +jose: + keys: + key_id: # ID of the key. When loaded, the service "jose.key.key_id" will be created + jwkset: # Type of key. In this case, the key from a serialized JWK. + is_public: true # Indicates the service will be public or private. This option is availble to all key sources + id: 'jose.key_set.my_key_set' # The key we want to load is in that JWKSet + index: 0 # Index of the key in the JWKSet. The key MUST exist otherwise an exception will be thrown +``` + # From a Certificate Chain The following example shows you how to load a key from a certificate chain. @@ -71,6 +89,7 @@ jose: keys: key_id: # ID of the key. When loaded, the service "jose.key.key_id" will be created x5c: # Type of key. In this case, the key from a a certificate chain. + is_public: true # Indicates the service will be public or private. This option is availble to all key sources value: | -----BEGIN CERTIFICATE----- MIID8DCCAtigAwIBAgIDAjqDMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNVBAYTAlVT @@ -121,13 +140,14 @@ jose: # Random Key Creation This bundle is able to create and rotate keys for you. -These keys are stored in a file and served on demand. When expired, they are automatically re-created. +These keys are stored in a file and served on demand. When expired, they are updated through a dedicated console command. If you need, a key may have no expiration time. -Please note that parameters `storage_path`, `ttl` and `additional_values` are common for all keys. -When `ttl` value is `0` (zero), this means the key will never expire. +Please note that parameters `storage_path` and `key_configuration` are common for all keys. + +The key ID (`kid`) is always set. If you add it to the `additional_configuration` list, then this value is ignored. -The key ID (`kid`) is always set. If you add it to the `additional_values` list, then this value is ignored. +Please read [this page](../use/commands.md) to know how to use console commands with these kind of keys. ## RSA Key @@ -138,8 +158,8 @@ jose: rsa: # Type of key. In this case, the key is a random RSA key. size: 4096 # Key size in bits storage_path: "/Path/To/The/Storage/File.key" # Path of the file - ttl: 3600 # TTL in second - additional_values: # You can add custom values + is_public: true # Indicates the service will be public or private. This option is availble to all key sources + key_configuration: # You can add custom values alg: 'RS256' use: 'sig' ``` @@ -153,8 +173,8 @@ jose: ec: # Type of key. In this case, the key is a random EC key. curve: 'P-256' # Curve of the key. P-256, P-384 and P-521 are supported storage_path: "/Path/To/The/Storage/File.key" # Path of the file - ttl: 3600 # TTL in second - additional_values: # You can add custom values + is_public: true # Indicates the service will be public or private. This option is availble to all key sources + key_configuration: # You can add custom values alg: 'ES256' use: 'sig' ``` @@ -168,8 +188,8 @@ jose: oct: # Type of key. In this case, the key is a random Octet key. size: 256 # Key size in bits storage_path: "/Path/To/The/Storage/File.key" # Path of the file - ttl: 3600 # TTL in second - additional_values: # You can add custom values + is_public: true # Indicates the service will be public or private. This option is availble to all key sources + key_configuration: # You can add custom values alg: 'HS256' use: 'sig' ``` @@ -183,8 +203,8 @@ jose: okp: # Type of key. In this case, the key is a random OKP key. curve: 'X25519' # Curve of the key. X25519 and Ed25519 are supported storage_path: "/Path/To/The/Storage/File.key" # Path of the file - ttl: 3600 # TTL in second - additional_values: # You can add custom values + is_public: true # Indicates the service will be public or private. This option is availble to all key sources + key_configuration: # You can add custom values alg: 'ECDH-ES' use: 'enc' ``` @@ -197,8 +217,22 @@ jose: key_id: # ID of the key. When loaded, the service "jose.key.key_id" will be created none: # Type of key. In this case, the key is a none key. storage_path: "/Path/To/The/Storage/File.key" # Path of the file - ttl: 3600 # TTL in second - additional_values: # You can add custom values - alg: 'ECDH-ES' - use: 'enc' + is_public: true # Indicates the service will be public or private. This option is availble to all key sources + key_configuration: # You can add custom values + kid: 'MY_NONE_KEY' + use: 'sig' + alg: 'none' +``` + +## Key Rotation + +All random keys can be refreshed through a console command after a period of time. + +In the following example, the command will generate a new key if the actual key is older than 86400 seconds (24 hrs): + +```sh +bin/console spomky-labs:jose:rotate-keys --key="jose.key.key_id" --ttl=86400 ``` + +_Please note that the service `jose.key.key_id` must be a valid random key._ + diff --git a/Resources/doc/config/signers_and_verifiers.md b/Resources/doc/config/signers_and_verifiers.md index 135a08c..a4aa8bc 100644 --- a/Resources/doc/config/signers_and_verifiers.md +++ b/Resources/doc/config/signers_and_verifiers.md @@ -5,85 +5,83 @@ Signers and Verifiers Services A Signer is a service that provides functions to sign payloads according to the headers (protected or unprotected) and private or shared keys. -Each Signer you create is available as a service you can inject in your own services or use from the container. It is allowed to use a set of algorithms you explicitly defined. +Each Signer you create is available as a service you can inject in your own services or use from the container. +It is allowed to use a set of algorithms you explicitly defined. -In the following example, we create two Signers. They will be available through `jose.signer.SIGNER1` and `jose.signer.SIGNER2` respectively: +In the following example, we create two Signers. +They will be available through `jose.signer.SIGNER1` and `jose.signer.SIGNER2` respectively: ```yml jose: signers: SIGNER1: # ID of the Signer. Must be unique - algorithms: # A list of algorithms + is_public: true # The service created by the bundle will be public (default) + algorithms: # A list of algorithms (see below for the complete list) - 'HS256' - 'HS384' - 'HS512' SIGNER2: # ID of the Signer. Must be unique - algorithms: # A list of algorithms + is_public: true # The service created by the bundle will be public (default) + algorithms: # A list of algorithms (see below for the complete list) - 'RS256' - 'RS512' - 'PS256' - 'PS512' ``` -Now you will be able to sign payloads (claims or messages) using these services: - -```php -use Jose\Factory\JWSFactory; - -// We get the key and the signer (we suppose that MY_KEY1 is a valid key) -$key = $container->get('jose.key.MY_KEY1'); -$signer = $container->get('jose.signer.SIGNER1'); - -// The payload to sign -$payload = 'Hello World!'; - -// We create a JWS object -$jws = JWSFactory::createJWS($payload); - -// We set the information for the signature -$jws->addSignatureInformation( - $key, - [ - 'alg' => 'HS512', - ] -); - -// We sign it -$signer->sign($jws); - -//We can get the Compact, Flattened or General Serialization Representation of that JWS -// 0 is the signature index (the first signature in this case) -$jws->toCompactJSON(0); -$jws->toFlattenedJSON(0); -$jws->toJSON(); -``` - # Verifiers -A Verifier is a service that provides functions to verify JWS you received using public or shared keys. +A Verifier is a service that provides functions to verify the JWS you received using public or shared keys. As Signers, each Verifier you create is available as a service you can inject in your own services or use from the container. It is allowed to use a set of algorithms you explicitly defined. -In the following example, we create two Verifiers. They will be available through `jose.verifier.VERFIER1` and `jose.verifier.VERFIER2` respectively: +In the following example, we create one Verifier. It will be available through `jose.verifier.VERFIER1`: ```yml jose: verifiers: VERFIER1: # ID of the Verifier. Must be unique - algorithms: # A list of algorithms + is_public: true # The service created by the bundle will be public (default) + algorithms: # A list of algorithms (see below for the complete list) - 'HS256' - 'HS384' - 'HS512' - VERFIER2: # ID of the Verifier. Must be unique - algorithms: # A list of algorithms - - 'RS256' - - 'RS512' - - 'PS256' - - 'PS512' ``` -Now you will be able to verify JWS using these services. Please note a Verifier verifies the signatures, not the claims of the JWS. +# Signer & Verifier at Once -```php -// To be written +In some cases, you will need a signer and a verifier with the same set of algorithms. +There is a configuration option `create_verifier` you can use to create both services at once: + +```yml +jose: + signers: + SERVICE1: # ID of the Signer. Must be unique + is_public: true # The service created by the bundle will be public (default) + create_verifier: true + algorithms: # A list of algorithms (see below for the complete list) + - 'HS256' + - 'HS384' + - 'HS512' ``` + +This will automatically create the services `jose.signer.SERVICE1` and `jose.verifier.SERVICE1`. +Both services will support `HS256`, `HS384` and `HS512` algorithms. + +# Supported Signature Algorithms + +Hereafter the list of all algorithms supported by this library. + +You may need an additional algorithm, then [read that page](../next/custom_algorithm.md) to know how to create custom algorithms. + +* [x] `HS256`, `HS384`, `HS512` +* [x] `ES256`, `ES384`, `ES512` +* [x] `RS256`, `RS384`, `RS512` +* [x] `PS256`, `PS384`, `PS512` +* [x] `none` (**Please note that this is not a secured algorithm. DO NOT USE IT PRODUCTION!**) +* [x] `EdDSA` + * [x] With `Ed25519` curve ([third party extension required](https://github.com/encedo/php-ed25519-ext)) + * [ ] With `Ed448` curve + +*Please note that the [EdDSA signature algorithm specification](https://tools.ietf.org/html/draft-ietf-jose-cfrg-curves) +is not not yet approved. Support for the algorithm `EdDSA` with `Ed25518` and `Ed448` curves may change. Use with caution.* diff --git a/Resources/doc/next/custom_algorithm.md b/Resources/doc/next/custom_algorithm.md new file mode 100644 index 0000000..6265fa3 --- /dev/null +++ b/Resources/doc/next/custom_algorithm.md @@ -0,0 +1,110 @@ +Custom Algorithm +================ + +The [spomky-Labs/jose](https://github.com/Spomky-Labs/jose) library already provides dozen of algorithms, but you may need to use your own algorithm. + +In the following example, we will create a dummy signature algorithm that produces signature with the `MD5` hash of the input and adds `--SIGNED--` at the end. + +# Create it! + +Depending on the algorithm, you have to implement one of the following interfaces: + +- `Jose\Algorithm\SignatureAlgorithmInterface` for a signature algorithm. +- `Jose\Algorithm\ContentEncryptionAlgorithmInterface` for a content encryption algorithm. +- `Jose\Algorithm\KeyEncryption\DirectEncryptionInterface` for a direct key encryption algorithm (should not be needed). +- `Jose\Algorithm\KeyEncryption\KeyAgreementInterface` for a key agreement algorithm. +- `Jose\Algorithm\KeyEncryption\KeyAgreementWrappingInterface` for a key agreement with key wrapping algorithm. +- `Jose\Algorithm\KeyEncryption\KeyEncryptionInterface` for a key encryption algorithm. +- `Jose\Algorithm\KeyEncryption\KeyWrappingInterface` for a key wrapping algorithm. + +In our example, we implement the first one: + +```php +computeSignature($input); // We compute the signature + } + + /** + * {@inheritdoc} + */ + public function verify(JWKInterface $key, $input, $signature) + { + // We compare the signature and the computed one + return hash_equals($signature, $this->computeSignature($input)); + } + + /** + * @return string + */ + private function computeSignature($input) + { + // Our signature is just a MD5 of the input + --SIGNED-- + return sprintf('%s --SIGNED--', hash('md5', $input)); + } +} +``` + +# Add it! + +Now we have a new algorithm, we have to create a tagged service that will be added to the algorithm manager algorithm list. + +```xml + + + + + + + + + + +``` + +# Use it! + +Now we can produce JWT using our custom algorithm: + +```yml +jose: + easy_jwt_creator: + custom: # We create a JWT Creator service that supports our custom algorithm + is_public: true + signature_algorithms: + - 'Dummy' # The algorithm name is the same the one one returned by the method getAlgorithmName() +``` + +```php +get('jose.jwt_creator.custom')->createJWSToCompactJSON('Hello', ['alg' => 'Dummy']); +``` + +The jwt should look like `eyJhbGciOiJEdW1teSJ9.SGVsbG8.MTg2ZjZjMTY3MWYyMWMzMzFhODE5ZjAxOGIyNGYxNGIgLS1TSUdORUQtLQ`. The last part is the `186f6c1671f21c331a819f018b24f14b --SIGNED--` encoded in Base64 Url Safe +which corresponds to the computed signature. diff --git a/Resources/doc/next/custom_checker.md b/Resources/doc/next/custom_checker.md new file mode 100644 index 0000000..84cfa24 --- /dev/null +++ b/Resources/doc/next/custom_checker.md @@ -0,0 +1,94 @@ +Custom Checker +============== + +The [spomky-Labs/jose](https://github.com/Spomky-Labs/jose) library provides some claim and header checkers, +but you may need to check claims described in the RFC that are not automatically checked or custom claims. + +# Available checkers + +Services from the following checkers are available. +However the library also provides (abstract)classes to check other useful claims: + +- The token ID (`jti`): `Jose\Checker\JtiChecker` +- The audience (`aud`): `Jose\Checker\AudienceChecker` +- The subject (`sub`): `Jose\Checker\SubjectChecker` + +All you have to do is to extends the class if needed and create a service. + +In the example below we will create a token ID claim checker from the beginning. +Our application, we want to be sure that all tokens have a token ID (`jti` claim) and that ID is only used once. +We have a token ID manager that provides a method `hasTokenIdBeenSeenBefore($jti)` that returns `true` or `false`. + +Every claim checkers have to implement the `Jose\Checker\ClaimCheckerInterface` interface. +For header checkers, you have to implement `Jose\Checker\HeaderCheckerInterface` + +```php +token_manager = $token_manager; + } + + /** + * {@inheritdoc} + */ + public function checkClaim(JWTInterface $jwt) + { + // We verify the claim is available (mandatory) + Assertion::true($jwt->hasClaim('jti'), 'The claim "jti" is missing.'); + + //We get the claim and verified if it has been used before + $jti = $jwt->getClaim('jti'); + Assertion::false($this->token_manager->hasTokenIdBeenSeenBefore($jti), sprintf('Invalid token ID "%s".', $jti)); + + //We return an array with all claims we checked (in this example we only checked 'jti') + return ['jti']; + } +} +``` + +Then create your service definition and do not forget to inject the token manager. +The alias in the tag is mandatory. This alias will be used to serve the checker to your checker manager + +```xml + + + + + + + + + + + +``` + +Now create your checker manager and enable the `my_jti_checker` you created. + +```yml +jose: + checkers: + my_checker: + headers: + ... + claims: + - exp + - nbf + - iat + - my_jti_checker +``` diff --git a/Resources/doc/next/custom_compression_method.md b/Resources/doc/next/custom_compression_method.md new file mode 100644 index 0000000..a9b5a97 --- /dev/null +++ b/Resources/doc/next/custom_compression_method.md @@ -0,0 +1,82 @@ +Custom Compression Method +========================= + +The [spomky-Labs/jose](https://github.com/Spomky-Labs/jose) library provides three compression methods: +- [x] `DEF`: Deflate +- [x] `ZLIB`: ZLib (not described in the RFCs, for internal use only) +- [x] `GZ`: GZip (not described in the RFCs, for internal use only) + +But you may need to use your own method (e.g. using 7ZIP, LZMA...). + +In the following example, we will create a custom method that uses a (non-existent) `method_compress` and `method_decompress` PHP functions. + +# Create it! + +First, you have to implement the interface `Jose\Compression\CompressionInterface`. + +```php + + + + + + + + + + +``` + +# Use it! + +Now you can use your compression method with your encrypters or decrypters + +```yml +jose: + decrypters: + DECRYPTER1: # ID of the Decrypter. Must be unique + ... + compression_methods: # A list of compression methods (see below for the complete list) + - 'CUSTOM' # Our custom method. This name is the same as returned by the 'getMethodName' method +``` diff --git a/Resources/doc/use/checkers.md b/Resources/doc/use/checkers.md new file mode 100644 index 0000000..a6e78ef --- /dev/null +++ b/Resources/doc/use/checkers.md @@ -0,0 +1,2 @@ +Checkers +======== diff --git a/Resources/doc/use/commands.md b/Resources/doc/use/commands.md new file mode 100644 index 0000000..675c188 --- /dev/null +++ b/Resources/doc/use/commands.md @@ -0,0 +1,2 @@ +Commands +======== diff --git a/Resources/doc/use/encrypters_and_decrypters.md b/Resources/doc/use/encrypters_and_decrypters.md new file mode 100644 index 0000000..6c298fc --- /dev/null +++ b/Resources/doc/use/encrypters_and_decrypters.md @@ -0,0 +1,68 @@ +Encrypters and Decrypters Services +================================== + +# Encrypters + +```php +use Jose\Factory\JWEFactory; + +// We get the key of the recipient (we suppose that MY_KEY1 is a valid key) +$key = $container->get('jose.key.MY_KEY1'); +$encrypter = $container->get('jose.encrypter.ENCRYPTER1'); // Only if the service is public + +// The payload to sign +$payload = 'Hello World!'; + +// We have to create a JWE class using the JWEFactory. +// The payload of this object contains our message. +$jwe = JWEFactory::createJWE( + $payload, // The payload + [ // The shared protected header + 'enc' => 'A128GCM', // The content encryption algorithm + 'alg' => 'A256GCMKW', // The key encryption algorithm + 'zip' => 'DEF', // We want to compress the payload before encryption (not mandatory, but useful for a large payload + ] +); + +// We add the recipient public key. +$jwe = $jwe->addRecipientInformation(key1); + +// We sign it +$encrypter->encrypt($jwe); + +//We can get the Compact, Flattened or General Serialization Representation of that JWE +// 0 is the recipient index (the first recipient in this case) +$jwe->toCompactJSON(0); +$jwe->toFlattenedJSON(0); +$jwe->toJSON(); +``` + +# Decrypters + +```php +// The JWE +$input = 'eyJhbGciOiJSU0EtT0FFUCIsImtpZCI6InNhbXdpc2UuZ2FtZ2VlQGhvYmJpdG9uLmV4YW1wbGUiLCJlbmMiOiJBMjU2R0NNIn0.rT99rwrBTbTI7IJM8fU3Eli7226HEB7IchCxNuh7lCiud48LxeolRdtFF4nzQibeYOl5S_PJsAXZwSXtDePz9hk-BbtsTBqC2UsPOdwjC9NhNupNNu9uHIVftDyucvI6hvALeZ6OGnhNV4v1zx2k7O1D89mAzfw-_kT3tkuorpDU-CpBENfIHX1Q58-Aad3FzMuo3Fn9buEP2yXakLXYa15BUXQsupM4A1GD4_H4Bd7V3u9h8Gkg8BpxKdUV9ScfJQTcYm6eJEBz3aSwIaK4T3-dwWpuBOhROQXBosJzS1asnuHtVMt2pKIIfux5BC6huIvmY7kzV7W7aIUrpYm_3H4zYvyMeq5pGqFmW2k8zpO878TRlZx7pZfPYDSXZyS0CfKKkMozT_qiCwZTSz4duYnt8hS4Z9sGthXn9uDqd6wycMagnQfOTs_lycTWmY-aqWVDKhjYNRf03NiwRtb5BE-tOdFwCASQj3uuAgPGrO2AWBe38UjQb0lvXn1SpyvYZ3WFc7WOJYaTa7A8DRn6MC6T-xDmMuxC0G7S2rscw5lQQU06MvZTlFOt0UvfuKBa03cxA_nIBIhLMjY2kOTxQMmpDPTr6Cbo8aKaOnx6ASE5Jx9paBpnNmOOKH35j_QlrQhDWUN6A2Gg8iFayJ69xDEdHAVCGRzN3woEI2ozDRs.-nBoKLH0YkLZPSI9.o4k2cnGN8rSSw3IDo1YuySkqeS_t2m1GXklSgqBdpACm6UJuJowOHC5ytjqYgRL-I-soPlwqMUf4UgRWWeaOGNw6vGW-xyM01lTYxrXfVzIIaRdhYtEMRBvBWbEwP7ua1DRfvaOjgZv6Ifa3brcAM64d8p5lhhNcizPersuhw5f-pGYzseva-TUaL8iWnctc-sSwy7SQmRkfhDjwbz0fz6kFovEgj64X1I5s7E6GLp5fnbYGLa1QUiML7Cc2GxgvI7zqWo0YIEc7aCflLG1-8BboVWFdZKLK9vNoycrYHumwzKluLWEbSVmaPpOslY2n525DxDfWaVFUfKQxMF56vn4B9QMpWAbnypNimbM8zVOw.UCGiqJxhBI3IFVdPalHHvA'; + +// We get the key and the decrypter (we suppose that MY_KEY1 is a valid key) +$key = $container->get('jose.key.MY_KEY1'); +$decrypter = $container->get('jose.signer.DECRYPTER1'); // Only if the service is public + +// We load the input. +$loader = $this->getContainer()->get('jose.loader'); +$jwe = $loader->load($input); +// The variable $jwe is now a JWEInterface object. +// It could be a JWSInterface object. Please check it before continuing. + +// We decrypt the JWE with our key. +// The third argument will be populate if the recipient decyprtion succeeded. The value corresponds to the decrypted recipient index. +// The value 0 (zero) is a valid value! +$decrypter->public function decryptUsingKey($jwe, $key, $index); +``` + +If you have multiple keys, you can group them in a JWKSet to decrypt the JWE object: + +```php +... +$key_set = $container->get('jose.key_set.MY_KEYSET'); +$decrypter->decryptUsingKeySet($jwe $key_set, $index); +``` diff --git a/Resources/doc/use/jwtloader_and_jwtcreator.md b/Resources/doc/use/jwtloader_and_jwtcreator.md new file mode 100644 index 0000000..41668c6 --- /dev/null +++ b/Resources/doc/use/jwtloader_and_jwtcreator.md @@ -0,0 +1,8 @@ +JWT Creator and JWT Loader Services +=================================== + +# JWT Creator + + +# JWT Loader + diff --git a/Resources/doc/use/signers_and_verifiers.md b/Resources/doc/use/signers_and_verifiers.md new file mode 100644 index 0000000..6e33539 --- /dev/null +++ b/Resources/doc/use/signers_and_verifiers.md @@ -0,0 +1,66 @@ +Signers and Verifiers Services +============================== + +# Signers + +```php +use Jose\Factory\JWSFactory; + +// We get the key and the signer (we suppose that MY_KEY1 is a valid key) +$key = $container->get('jose.key.MY_KEY1'); +$signer = $container->get('jose.signer.SIGNER1'); // Only if the service is public + +// The payload to sign +$payload = 'Hello World!'; + +// We create a JWS object +$jws = JWSFactory::createJWS($payload); + +// We set the information for the signature +$jws->addSignatureInformation( + $key, + [ + 'alg' => 'HS512', + ] +); + +// We sign it +$signer->sign($jws); + +//We can get the Compact, Flattened or General Serialization Representation of that JWS +// 0 is the signature index (the first signature in this case) +$jws->toCompactJSON(0); +$jws->toFlattenedJSON(0); +$jws->toJSON(); +``` + +# Verifiers + +```php +// The JWS +$input = 'eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LWVlZjMxNGJjNzAzNyJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4.s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0'; + +// We get the key and the verifier (we suppose that MY_KEY1 is a valid key) +$key = $container->get('jose.key.MY_KEY1'); +$verifier = $container->get('jose.signer.VERFIER1'); // Only if the service is public + +// We load the input. +$loader = $this->getContainer()->get('jose.loader'); +$jws = $loader->load($input); +// The variable $jws is now a JWSInterface object. +// It could be a JWEInterface object. Please check it before continuing. + +// We verify the signature with our key. +// The third argument is the detached payload. In that case the payload is not detached and null is passed as argument value. +// The fourth argument will be populate if the signature verification succeeded. The value corresponds to the verified signature index. +// The value 0 (zero) is a valid value! +$verifier->verifyWithKey($jws $key, null, $index); +``` + +If you have multiple keys, you can group them in a JWKSet to verify the JWS object: + +```php +... +$key_set = $container->get('jose.key_set.MY_KEYSET'); +$verifier->verifyWithKeySet($jws $key_set, null, $index); +``` diff --git a/Service/ServiceFactory.php b/Service/ServiceFactory.php index 7d75dd8..48441dd 100644 --- a/Service/ServiceFactory.php +++ b/Service/ServiceFactory.php @@ -56,9 +56,9 @@ public function __construct(AlgorithmManager $algorithm_manager, CompressionMana } /** - * @param string[] $selected_key_encryption_algorithms - * @param string[] $selected_content_encryption_algorithms - * @param string[] $selected_compression_methods + * @param string[] $selected_key_encryption_algorithms + * @param string[] $selected_content_encryption_algorithms + * @param string[] $selected_compression_methods * * @return \Jose\EncrypterInterface */ @@ -72,9 +72,9 @@ public function createEncrypter(array $selected_key_encryption_algorithms, array } /** - * @param string[] $selected_key_encryption_algorithms - * @param string[] $selected_content_encryption_algorithms - * @param string[] $selected_compression_methods + * @param string[] $selected_key_encryption_algorithms + * @param string[] $selected_content_encryption_algorithms + * @param string[] $selected_compression_methods * * @return \Jose\DecrypterInterface */ diff --git a/SpomkyLabsJoseBundle.php b/SpomkyLabsJoseBundle.php index a10cc4b..ede87fb 100644 --- a/SpomkyLabsJoseBundle.php +++ b/SpomkyLabsJoseBundle.php @@ -20,11 +20,17 @@ final class SpomkyLabsJoseBundle extends Bundle { + /** + * {@inheritdoc} + */ public function getContainerExtension() { return new SpomkyLabsJoseBundleExtension('jose', __DIR__); } + /** + * {@inheritdoc} + */ public function build(ContainerBuilder $container) { parent::build($container); diff --git a/Tests/Context/ApplicationContext.php b/Tests/Context/ApplicationContext.php index e0eae41..5e85d80 100644 --- a/Tests/Context/ApplicationContext.php +++ b/Tests/Context/ApplicationContext.php @@ -11,8 +11,9 @@ namespace SpomkyLabs\JoseBundle\Features\Context; +use Assert\Assertion; use Behat\Gherkin\Node\PyStringNode; -use SpomkyLabs\JoseBundle\Command\KeyRotationCommand; +use SpomkyLabs\JoseBundle\Command; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -128,7 +129,9 @@ protected function getApplication() { if (null === $this->application) { $this->application = new Application($this->getKernel()); - $this->application->add(new KeyRotationCommand()); + $this->application->add(new Command\RegenCommand()); + $this->application->add(new Command\RotateCommand()); + $this->application->add(new Command\DeleteCommand()); } return $this->application; @@ -139,10 +142,9 @@ protected function getApplication() */ public function iWaitSeconds($time) { - sleep((int)$time); + sleep((int) $time); } - /** * @When I run command :line */ @@ -187,9 +189,8 @@ public function iRunACommandWithParameters($line, PyStringNode $parameterJson) */ public function iShouldSee(PyStringNode $result) { - if ($this->getCommandOutput() !== $result->getRaw()) { - throw new \Exception('The output of the command is not the same as expected. I got '.$this->getCommandOutput().''); - } + $output = $this->getCommandOutput(); + Assertion::eq($output, $result->getRaw(), sprintf('The output of the command is not the same as expected. I got "%".', $output)); } /** @@ -228,7 +229,7 @@ public function theCommandExceptionShouldBeThrown($exception) */ public function theCommandExitCodeShouldBe($code) { - if ($this->getCommandExitCode() !== is_int($code) ? (int) $code : $code) { + if ($this->getCommandExitCode() !== (int) $code) { throw new \Exception(sprintf('The exit code is %u.', $this->getCommandExitCode())); } } diff --git a/Tests/Context/JWECreationContext.php b/Tests/Context/JWECreationContext.php index 27e3332..73c5f7b 100644 --- a/Tests/Context/JWECreationContext.php +++ b/Tests/Context/JWECreationContext.php @@ -52,6 +52,8 @@ abstract protected function getPayload(); /** * @Given I have the following values in the JWE shared protected header + * + * @param \Behat\Gherkin\Node\PyStringNode $string */ public function iHaveTheFollowingValuesInTheJweSharedProtectedHeader(PyStringNode $string) { @@ -60,6 +62,8 @@ public function iHaveTheFollowingValuesInTheJweSharedProtectedHeader(PyStringNod /** * @Given I have the following values in the JWE shared header + * + * @param \Behat\Gherkin\Node\PyStringNode $string */ public function iHaveTheFollowingValuesInTheJweSharedHeader(PyStringNode $string) { @@ -68,6 +72,8 @@ public function iHaveTheFollowingValuesInTheJweSharedHeader(PyStringNode $string /** * @Given I have the following values in the recipient header + * + * @param \Behat\Gherkin\Node\PyStringNode $string */ public function iHaveTheFollowingValuesInTheRecipientHeader(PyStringNode $string) { @@ -76,6 +82,8 @@ public function iHaveTheFollowingValuesInTheRecipientHeader(PyStringNode $string /** * @Given I have the following value as AAD + * + * @param \Behat\Gherkin\Node\PyStringNode $string */ public function iHaveTheFollowingValueAsAad(PyStringNode $string) { @@ -84,6 +92,9 @@ public function iHaveTheFollowingValueAsAad(PyStringNode $string) /** * @When I try to create a JWE in JSON Compact Serialization Mode with recipient key :key_service and I store the result in the variable :variable + * + * @param string $key_service + * @param string $variable */ public function iTryToCreateAJweInJsonCompactSerializationModeWithRecipientKeyAndIStoreTheResultInTheVariable($key_service, $variable) { @@ -97,6 +108,9 @@ public function iTryToCreateAJweInJsonCompactSerializationModeWithRecipientKeyAn /** * @When I try to create a JWE in JSON Flattened Serialization Mode with recipient key :key_service and I store the result in the variable :variable + * + * @param string $key_service + * @param string $variable */ public function iTryToCreateAJweInJsonFlattenedSerializationModeWithRecipientKeyAndIStoreTheResultInTheVariable($key_service, $variable) { diff --git a/Tests/Context/JWSCreationContext.php b/Tests/Context/JWSCreationContext.php index acb901b..c6a1392 100644 --- a/Tests/Context/JWSCreationContext.php +++ b/Tests/Context/JWSCreationContext.php @@ -42,6 +42,8 @@ abstract protected function getPayload(); /** * @Given I have the following values in the signature protected header + * + * @param \Behat\Gherkin\Node\PyStringNode $string */ public function iHaveTheFollowingValuesInTheSignatureProtectedHeader(PyStringNode $string) { @@ -50,6 +52,8 @@ public function iHaveTheFollowingValuesInTheSignatureProtectedHeader(PyStringNod /** * @Given I have the following values in the signature header + * + * @param \Behat\Gherkin\Node\PyStringNode $string */ public function iHaveTheFollowingValuesInTheSignatureHeader(PyStringNode $string) { @@ -58,6 +62,9 @@ public function iHaveTheFollowingValuesInTheSignatureHeader(PyStringNode $string /** * @When I try to create a JWS in JSON Compact Serialization Mode with signature key :key_service and I store the result in the variable :variable + * + * @param string $key_service + * @param string $variable */ public function iTryToCreateAJwsInJsonCompactSerializationModeWithSignatureKeyAndIStoreTheResultInTheVariable($key_service, $variable) { @@ -71,6 +78,9 @@ public function iTryToCreateAJwsInJsonCompactSerializationModeWithSignatureKeyAn /** * @When I try to create a JWS in JSON Flattened Serialization Mode with signature key :key_service and I store the result in the variable :variable + * + * @param string $key_service + * @param string $variable */ public function iTryToCreateAJwsInJsonFlattenedSerializationModeWithSignatureKeyAndIStoreTheResultInTheVariable($key_service, $variable) { diff --git a/Tests/Context/KeysAndKeySetsContext.php b/Tests/Context/KeysAndKeySetsContext.php index 138d1cd..2a59508 100644 --- a/Tests/Context/KeysAndKeySetsContext.php +++ b/Tests/Context/KeysAndKeySetsContext.php @@ -12,7 +12,6 @@ namespace SpomkyLabs\JoseBundle\Features\Context; use Jose\Object\JWKSetInterface; -use Jose\Object\StorableJWK; /** * Behat context trait. @@ -95,4 +94,14 @@ public function theServiceShouldBeAnObjectThatImplements($service, $interface) )); } } + + /** + * @When I show JWKSet :id + */ + public function iShowJWKSet($id) + { + $jwkset = $this->getContainer()->get($id); + + dump(json_encode($jwkset)); + } } diff --git a/Tests/TestJoseBundle/DependencyInjection/Configuration.php b/Tests/TestJoseBundle/DependencyInjection/Configuration.php deleted file mode 100644 index 7fe0d11..0000000 --- a/Tests/TestJoseBundle/DependencyInjection/Configuration.php +++ /dev/null @@ -1,39 +0,0 @@ -alias = $alias; - } - - /** - * {@inheritdoc} - */ - public function getConfigTreeBuilder() - { - $treeBuilder = new TreeBuilder(); - $treeBuilder->root($this->alias); - - return $treeBuilder; - } -} diff --git a/Tests/TestJoseBundle/DependencyInjection/TestExtension.php b/Tests/TestJoseBundle/DependencyInjection/TestExtension.php index c8a0869..f586adc 100644 --- a/Tests/TestJoseBundle/DependencyInjection/TestExtension.php +++ b/Tests/TestJoseBundle/DependencyInjection/TestExtension.php @@ -11,9 +11,7 @@ namespace SpomkyLabs\TestJoseBundle\DependencyInjection; -use Assert\Assertion; use SpomkyLabs\JoseBundle\Helper\ConfigurationHelper; -use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -22,6 +20,9 @@ final class TestExtension extends Extension implements PrependExtensionInterface { + /** + * @var string + */ private $alias; /** @@ -32,37 +33,40 @@ public function __construct($alias) $this->alias = $alias; } + /** + * {@inheritdoc} + */ public function load(array $configs, ContainerBuilder $container) { - $processor = new Processor(); - $configuration = new Configuration($this->getAlias()); - - /*$config = */$processor->processConfiguration($configuration, $configs); - $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.xml'); } /** - * @return string + * {@inheritdoc} */ public function getAlias() { return $this->alias; } + + /** + * {@inheritdoc} + */ public function prepend(ContainerBuilder $container) { - $bundles = $container->getParameter('kernel.bundles'); - Assertion::keyExists($bundles, 'SpomkyLabsJoseBundle', 'The "Spomky-Labs/JoseBundle" must be enabled.'); - $jose_config = current($container->getExtensionConfig('jose')); - - $checker_config = ConfigurationHelper::getCheckerConfiguration('test', ['crit'], ['iat', 'nbf', 'exp']); - array_merge( - $jose_config, - $checker_config - ); + ConfigurationHelper::addChecker($container, 'test', ['crit'], ['iat', 'nbf', 'exp']); + ConfigurationHelper::addRandomJWKSet($container, 'from_configuration_helper', '%kernel.cache_dir%/from_configuration_helper.keyset', 2, ['kty'=>'RSA', 'size'=>1024], true); + ConfigurationHelper::addJWKSets($container, 'all_in_one_from_configuration_helper', ['jose.key_set.from_configuration_helper']); + ConfigurationHelper::addPublicJWKSet($container, 'all_in_one_public_from_configuration_helper', 'jose.key_set.from_configuration_helper'); - $container->prependExtensionConfig('jose', $jose_config); + ConfigurationHelper::addChecker($container, 'from_configuration_helper', ['crit'], ['exp', 'iat', 'nbf']); + ConfigurationHelper::addSigner($container, 'from_configuration_helper', ['RS256']); + ConfigurationHelper::addVerifier($container, 'from_configuration_helper', ['RS256']); + ConfigurationHelper::addEncrypter($container, 'from_configuration_helper', ['RSA-OAEP-256'], ['A256GCM'], ['DEF']); + ConfigurationHelper::addDecrypter($container, 'from_configuration_helper', ['RSA-OAEP-256'], ['A256GCM'], ['DEF']); + ConfigurationHelper::addJWTLoader($container, 'from_configuration_helper', 'jose.verifier.from_configuration_helper', 'jose.checker.from_configuration_helper', 'jose.decrypter.from_configuration_helper'); + ConfigurationHelper::addJWTCreator($container, 'from_configuration_helper', 'jose.signer.from_configuration_helper', 'jose.encrypter.from_configuration_helper'); } } diff --git a/Tests/app/config/config_test.yml b/Tests/app/config/config_test.yml index 5e652ed..30210c9 100644 --- a/Tests/app/config/config_test.yml +++ b/Tests/app/config/config_test.yml @@ -11,8 +11,6 @@ framework: router: resource: "%kernel.root_dir%/config/routing.yml" strict_requirements: ~ - trusted_hosts: ~ - trusted_proxies: ~ fragments: ~ http_method_override: true @@ -64,7 +62,39 @@ jose: x5u: url: "https://www.googleapis.com/oauth2/v1/certs" is_secured: true + auto_signature: + auto: + storage_path: "%kernel.cache_dir%/auto_signature.keyset" + is_rotatable: true + nb_keys: 5 + key_configuration: + kty: 'RSA' + size: 4096 + alg: "RS256" + use: "sig" + auto_encryption: + auto: + storage_path: "%kernel.cache_dir%/auto_encryption.keyset" + is_rotatable: true + nb_keys: 2 + key_configuration: + kty: 'EC' + crv: 'P-521' + alg: "ECDH-ES" + use: "enc" + all_in_one: + jwksets: + id: + - 'jose.key_set.auto_signature' + - 'jose.key_set.auto_encryption' + all_in_one_public: + public_jwkset: + id: 'jose.key_set.all_in_one' keys: + from_keyset: + jwkset: + key_set: 'jose.key_set.all_in_one' + index: 0 key0: values: values: @@ -73,13 +103,13 @@ jose: k: "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg" key1: file: - path: "%kernel.root_dir%/keys/EC/private.es256.encrypted.key" + path: "%kernel.root_dir%/Keys/EC/private.es256.encrypted.key" password: "test" additional_values: foo: "bar" key2: file: - path: "%kernel.root_dir%/keys/EC/public.es512.key" + path: "%kernel.root_dir%/Keys/EC/public.es512.key" key3: certificate: path: "%kernel.root_dir%/certificates/RSA/DER/8192b-rsa-example-cert.der" @@ -154,22 +184,22 @@ jose: rsa: size: 4096 storage_path: "%kernel.cache_dir%/rsa" - additional_values: + key_configuration: alg: "RS256" key8: none: storage_path: "%kernel.cache_dir%/none" - additional_values: + key_configuration: alg: "none" key9: ec: curve: 'P-256' storage_path: "%kernel.cache_dir%/ec" - additional_values: + key_configuration: alg: "ES256" key10: oct: size: 256 storage_path: "%kernel.cache_dir%/oct" - additional_values: + key_configuration: alg: "HS256" diff --git a/composer.json b/composer.json index a836182..d87532d 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "type": "symfony-bundle", "description": "Symfony2 Jose Bundle", "keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jose", "Jot", "Bundle", "Symfony"], - "homepage": "https://github.com/Spomky-Labs/JoseBundle", + "homepage": "https://github.com/Spomky-Labs/jose-bundle", "license": "MIT", "authors": [ { @@ -11,18 +11,20 @@ } ], "require": { - "spomky-labs/jose": "^5.1", + "spomky-labs/jose": "^6.0@dev", + "symfony/console": "^2.7|^3.0", "symfony/http-kernel": "^2.7|^3.0", + "symfony/framework-bundle": "^2.7|^3.0", "symfony/dependency-injection": "^2.7|^3.0", "symfony/config": "^2.7|^3.0" }, "require-dev": { "symfony/finder": "^2.7|^3.0", "symfony/var-dumper": "^2.7|^3.0", - "behat/behat": "^3.0", - "behat/mink": "*", - "behat/mink-extension": "^2.1", - "behat/mink-browserkit-driver": "*", + "behat/behat": "^3.1", + "behat/mink": "^1.5", + "behat/mink-extension": "^2.0", + "behat/mink-browserkit-driver": "^1.1", "behat/symfony2-extension": "^2.1", "ext-curl": "*" }, @@ -42,7 +44,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } } }