Skip to content

Commit 5c3ca2a

Browse files
tvdijenkayjoosten
andauthored
Add rollover metadata containing all available keys (#1899)
* Add rollover metadata containing all available keys * Fix CI * Add tests for rollover metadata key support - Fix broken unit tests in decorator classes after array change - Add buildAll() unit test to KeyPairFactoryTest - Add Behat scenarios verifying all keys appear in default metadata - Update FrontPage test to expect rollover key links - Add CI and dev env parameters with rollover key for testing - Remove unused X509Certificate import from EngineBlockServiceProvider --------- Co-authored-by: Kay Joosten <kay.joosten@dawn.tech>
1 parent 3e43126 commit 5c3ca2a

13 files changed

Lines changed: 115 additions & 22 deletions

File tree

config/packages/ci/parameters.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
parameters:
2+
encryption_keys:
3+
default:
4+
publicFile: /config/engine/engineblock.crt
5+
privateFile: /config/engine/engineblock.pem
6+
rollover:
7+
publicFile: '%kernel.project_dir%/src/OpenConext/EngineBlockFunctionalTestingBundle/Resources/keys/rolled-over.crt'
8+
privateFile: '%kernel.project_dir%/src/OpenConext/EngineBlockFunctionalTestingBundle/Resources/keys/rolled-over.key'

config/packages/dev/parameters.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
parameters:
2+
encryption_keys:
3+
default:
4+
publicFile: /config/engine/engineblock.crt
5+
privateFile: /config/engine/engineblock.pem
6+
rollover:
7+
publicFile: '%kernel.project_dir%/src/OpenConext/EngineBlockFunctionalTestingBundle/Resources/keys/rolled-over.crt'
8+
privateFile: '%kernel.project_dir%/src/OpenConext/EngineBlockFunctionalTestingBundle/Resources/keys/rolled-over.key'

config/packages/parameters.yml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ parameters:
3636
## The Signing / Encryption keys used for the SAML2 authentication and metadata
3737
## When EngineBlock signs responses (when it acts as an Idp)
3838
## or requests (when it acts as an SP) it uses these X.509 certs.
39+
## During a key rollover, add the new key as 'rollover'. The default metadata will then
40+
## contain both keys, allowing IdPs to accept responses signed with either key.
41+
## Once all IdPs have updated their metadata, remove the old key or swap default/rollover.
3942
encryption_keys:
4043
default:
4144
publicFile: /config/engine/engineblock.crt

src/OpenConext/EngineBlock/Metadata/Factory/Decorator/EngineBlockIdentityProvider.php

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@
3030
class EngineBlockIdentityProvider extends AbstractIdentityProvider
3131
{
3232
/**
33-
* @var X509KeyPair
33+
* @var array<X509KeyPair>
3434
*/
35-
private $keyPair;
35+
private array $keyPairs;
36+
3637
/**
3738
* @var UrlProvider
3839
*/
@@ -46,20 +47,23 @@ class EngineBlockIdentityProvider extends AbstractIdentityProvider
4647
public function __construct(
4748
IdentityProviderEntityInterface $entity,
4849
?string $keyId,
49-
X509KeyPair $keyPair,
50+
array $keyPairs,
5051
UrlProvider $urlProvider
5152
) {
5253
parent::__construct($entity);
5354
$this->keyId = $keyId;
54-
$this->keyPair = $keyPair;
55+
$this->keyPairs = $keyPairs;
5556
$this->urlProvider = $urlProvider;
5657
}
5758

5859
public function getCertificates(): array
5960
{
60-
return [
61-
$this->keyPair->getCertificate(),
62-
];
61+
$certificates = [];
62+
foreach ($this->keyPairs as $keyPair) {
63+
$certificates[] = $keyPair->getCertificate();
64+
}
65+
66+
return $certificates;
6367
}
6468

6569
public function getSupportedNameIdFormats(): array

src/OpenConext/EngineBlock/Metadata/Factory/Decorator/EngineBlockServiceProvider.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,42 @@
3232
class EngineBlockServiceProvider extends AbstractServiceProvider
3333
{
3434
/**
35-
* @var X509KeyPair
35+
* @var array<X509KeyPair>
3636
*/
37-
private $keyPair;
37+
private array $keyPairs;
38+
3839
/**
3940
* @var AttributesMetadata
4041
*/
4142
private $attributes;
43+
4244
/**
4345
* @var UrlProvider
4446
*/
4547
private $urlProvider;
4648

4749
public function __construct(
4850
ServiceProviderEntityInterface $entity,
49-
X509KeyPair $keyPair,
51+
array $keyPairs,
5052
AttributesMetadata $attributes,
5153
UrlProvider $urlProvider
5254
) {
5355
parent::__construct($entity);
5456

55-
$this->keyPair = $keyPair;
57+
$this->keyPairs = $keyPairs;
5658
$this->attributes = $attributes;
5759
$this->urlProvider = $urlProvider;
5860
}
5961

6062

6163
public function getCertificates(): array
6264
{
63-
return [$this->keyPair->getCertificate()];
65+
$certificates = [];
66+
foreach ($this->keyPairs as $keyPair) {
67+
$certificates[] = $keyPair->getCertificate();
68+
}
69+
70+
return $certificates;
6471
}
6572

6673
/**

src/OpenConext/EngineBlock/Metadata/Factory/Factory/IdentityProviderFactory.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ private function buildEngineBlockEntityFromEntity(IdentityProvider $entity, ?str
100100
$this->engineBlockConfiguration
101101
),
102102
$keyId,
103-
$this->keyPairFactory->buildFromIdentifier($keyId),
103+
($keyId === KeyPairFactory::DEFAULT_KEY_PAIR_IDENTIFIER || $keyId === null)
104+
? $this->keyPairFactory->buildAll()
105+
: [$this->keyPairFactory->buildFromIdentifier($keyId)],
104106
$this->urlProvider
105107
);
106108
}

src/OpenConext/EngineBlock/Metadata/Factory/Factory/ServiceProviderFactory.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class ServiceProviderFactory
5353
* @var EngineBlockConfiguration
5454
*/
5555
private $engineBlockConfiguration;
56+
5657
/**
5758
* @var UrlProvider
5859
*/
@@ -95,7 +96,9 @@ public function createEngineBlockEntityFrom(string $keyId): ServiceProviderEntit
9596
new ServiceProviderEntity($entity),
9697
$this->engineBlockConfiguration
9798
),
98-
$this->keyPairFactory->buildFromIdentifier($keyId),
99+
($keyId === KeyPairFactory::DEFAULT_KEY_PAIR_IDENTIFIER || $keyId === null)
100+
? $this->keyPairFactory->buildAll()
101+
: [$this->keyPairFactory->buildFromIdentifier($keyId)],
99102
$this->attributes,
100103
$this->urlProvider
101104
);

src/OpenConext/EngineBlock/Metadata/X509/KeyPairFactory.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class KeyPairFactory
2626
{
2727
const DEFAULT_KEY_PAIR_IDENTIFIER = 'default';
2828

29-
private $keyPairConfiguration = [];
29+
private array $keyPairConfiguration = [];
3030

3131
/**
3232
* @param array $keyPairConfiguration
@@ -42,7 +42,7 @@ public function __construct(array $keyPairConfiguration)
4242
*
4343
* @throws RuntimeException
4444
*/
45-
public function buildFromIdentifier(?string $identifier) : X509KeyPair
45+
public function buildFromIdentifier(?string $identifier): X509KeyPair
4646
{
4747
if ($identifier === null) {
4848
$identifier = self::DEFAULT_KEY_PAIR_IDENTIFIER;
@@ -57,4 +57,20 @@ public function buildFromIdentifier(?string $identifier) : X509KeyPair
5757
}
5858
throw new UnknownKeyIdException($identifier);
5959
}
60+
61+
/**
62+
* @return array<X509KeyPair>
63+
*
64+
* @throws RuntimeException
65+
*/
66+
public function buildAll(): array
67+
{
68+
$pairs = [];
69+
70+
foreach (array_keys($this->keyPairConfiguration) as $keyId) {
71+
$pairs[] = $this->buildFromIdentifier((string)$keyId);
72+
}
73+
74+
return $pairs;
75+
}
6076
}

src/OpenConext/EngineBlockFunctionalTestingBundle/Features/FrontPage.feature

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,25 @@ Feature:
1010

1111
Scenario: The user can see all available metadata links
1212
When I go to Engineblock URL "/"
13-
Then I should see 8 links on the front page
13+
Then I should see 12 links on the front page
1414
And I should see text matching "The Public SAML Signing certificate of the OpenConext IdP"
1515
And I should see URL "/authentication/idp/certificate"
1616
# The default key should not get a separate URL next to the key-less url
1717
And I should not see URL "/authentication/idp/certificate/key:default"
18+
# The rollover key should get its own URL
19+
And I should see URL "/authentication/idp/certificate/key:rollover"
1820
And I should see text matching "The Public SAML metadata \(the entity descriptor\) of the OpenConext IdP Proxy"
1921
And I should see URL "/authentication/idp/metadata"
2022
And I should not see URL "/authentication/idp/metadata/key:default"
23+
And I should see URL "/authentication/idp/metadata/key:rollover"
2124
And I should see text matching "The Public SAML metadata \(the entities descriptor\) for all the OpenConext IdPs"
2225
And I should see URL "/authentication/proxy/idps-metadata"
2326
And I should not see URL "/authentication/proxy/idps-metadata/key:default"
27+
And I should see URL "/authentication/proxy/idps-metadata/key:rollover"
2428
And I should see text matching "The Public SAML metadata \(the entities descriptor\) of the OpenConext IdPs which"
2529
And I should see URL "/authentication/proxy/idps-metadata?sp-entity-id=urn:example.org"
2630
And I should not see URL "/authentication/proxy/idps-metadata/key:default?sp-entity-id=urn:example.org"
31+
And I should see URL "/authentication/proxy/idps-metadata/key:rollover?sp-entity-id=urn:example.org"
2732
# The test debug link is present on the page
2833
And I should see text matching "Test authentication with an identity provider."
2934
And I should see URL "/authentication/sp/debug"

src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Metadata.feature

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,33 @@ Feature:
220220
# Verify the used signing key is EB key
221221
And the response should match xpath '//ds:Signature//ds:X509Certificate[starts-with(.,"MIIDuDCCAqCgAwIBAgIJAPdqJ9JQKN6vMA0GCSqGSIb3DQEBBQUAMEYxDzANBgNVBAMT")]'
222222

223+
Scenario: The default SP metadata contains all configured keys (rollover support)
224+
When I go to Engineblock URL "/authentication/sp/metadata"
225+
# Verify the default (current) signing key is present
226+
Then the response should match xpath '//md:KeyDescriptor[@use="signing"]//ds:X509Certificate[starts-with(.,"MIIDuDCCAqCgAwIBAgIJAPdqJ9JQKN6vMA0GCSqGSIb3DQEBBQUAMEYxDzANBgNVBAMT")]'
227+
# Verify the rollover signing key is also present
228+
And the response should match xpath '//md:KeyDescriptor[@use="signing"]//ds:X509Certificate[starts-with(.,"MIIDhTCCAm2gAwIBAgIJALJlbT5u9cXzMA0GCSqGSIb3DQEBBQUAMFkxCzAJ")]'
229+
# Verify the metadata document is signed with the default key
230+
And the response should match xpath '//ds:Signature//ds:X509Certificate[starts-with(.,"MIIDuDCCAqCgAwIBAgIJAPdqJ9JQKN6vMA0GCSqGSIb3DQEBBQUAMEYxDzANBgNVBAMT")]'
231+
232+
Scenario: The default IdP metadata contains all configured keys (rollover support)
233+
When I go to Engineblock URL "/authentication/idp/metadata"
234+
# Verify the default (current) signing key is present
235+
Then the response should match xpath '//md:KeyDescriptor[@use="signing"]//ds:X509Certificate[starts-with(.,"MIIDuDCCAqCgAwIBAgIJAPdqJ9JQKN6vMA0GCSqGSIb3DQEBBQUAMEYxDzANBgNVBAMT")]'
236+
# Verify the rollover signing key is also present
237+
And the response should match xpath '//md:KeyDescriptor[@use="signing"]//ds:X509Certificate[starts-with(.,"MIIDhTCCAm2gAwIBAgIJALJlbT5u9cXzMA0GCSqGSIb3DQEBBQUAMFkxCzAJ")]'
238+
# Verify the metadata document is signed with the default key
239+
And the response should match xpath '//ds:Signature//ds:X509Certificate[starts-with(.,"MIIDuDCCAqCgAwIBAgIJAPdqJ9JQKN6vMA0GCSqGSIb3DQEBBQUAMEYxDzANBgNVBAMT")]'
240+
241+
Scenario: Requesting metadata with a specific key id returns only that key
242+
When I go to Engineblock URL "/authentication/sp/metadata/key:rollover"
243+
# Verify only the rollover key is present
244+
Then the response should match xpath '//md:KeyDescriptor[@use="signing"]//ds:X509Certificate[starts-with(.,"MIIDhTCCAm2gAwIBAgIJALJlbT5u9cXzMA0GCSqGSIb3DQEBBQUAMFkxCzAJ")]'
245+
# Verify the default key is not present
246+
And the response should not match xpath '//md:KeyDescriptor[@use="signing"]//ds:X509Certificate[starts-with(.,"MIIDuDCCAqCgAwIBAgIJAPdqJ9JQKN6vMA0GCSqGSIb3DQEBBQUAMEYxDzANBgNVBAMT")]'
247+
# Verify the metadata document is signed with the rollover key
248+
And the response should match xpath '//ds:Signature//ds:X509Certificate[starts-with(.,"MIIDhTCCAm2gAwIBAgIJALJlbT5u9cXzMA0GCSqGSIb3DQEBBQUAMFkxCzAJ")]'
249+
223250
Scenario: A user can request the EngineBlock SP public certificate
224251
When I go to Engineblock URL "/authentication/sp/certificate"
225252
Then the response should contain '-----BEGIN CERTIFICATE-----'

0 commit comments

Comments
 (0)