Skip to content

Commit 0522042

Browse files
committed
Add key rollover options
1 parent 9989c82 commit 0522042

File tree

3 files changed

+145
-36
lines changed

3 files changed

+145
-36
lines changed

config/module_oidc.php.dist

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,30 @@ $config = [
2727
* is a case-sensitive URL using the https scheme that contains scheme, host, and optionally, port number and
2828
* path components and no query or fragment components."
2929
*/
30-
//ModuleConfig::OPTION_ISSUER => 'https://op.example.org',
30+
// ModuleConfig::OPTION_ISSUER => 'https://op.example.org',
3131

3232
/**
3333
* PKI (public / private key) settings related to OIDC protocol. These keys will be used, for example, to
3434
* sign ID Token JWT.
3535
*/
3636
// (optional) The private key passphrase.
37-
//ModuleConfig::OPTION_PKI_PRIVATE_KEY_PASSPHRASE => 'secret',
37+
// ModuleConfig::OPTION_PKI_PRIVATE_KEY_PASSPHRASE => 'secret',
3838
// The certificate and private key filenames, with given defaults.
3939
ModuleConfig::OPTION_PKI_PRIVATE_KEY_FILENAME => ModuleConfig::DEFAULT_PKI_PRIVATE_KEY_FILENAME,
4040
ModuleConfig::OPTION_PKI_CERTIFICATE_FILENAME => ModuleConfig::DEFAULT_PKI_CERTIFICATE_FILENAME,
4141

42+
/**
43+
* (optional) Key rollover settings related to OIDC protocol. If set, this new private / public key pair will only
44+
* be published on JWKS endpoint as available, so Relying Parties can pick them up for future use. The signing
45+
* of artifacts will still be done using the 'current' private key (settings above). After some time, when all
46+
* RPs have fetched all public keys from JWKS endpoint, simply set these new keys as active values for above
47+
* PKI options.
48+
*/
49+
// // (optional) The (new) private key passphrase.
50+
// ModuleConfig::OPTION_PKI_NEW_PRIVATE_KEY_PASSPHRASE => 'new-secret',
51+
// ModuleConfig::OPTION_PKI_NEW_PRIVATE_KEY_FILENAME => 'new_oidc_module.key',
52+
// ModuleConfig::OPTION_PKI_NEW_CERTIFICATE_FILENAME => 'new_oidc_module.crt',
53+
4254
/**
4355
* Token related options.
4456
*/
@@ -51,8 +63,8 @@ $config = [
5163
// Token signer, with given default.
5264
// See Lcobucci\JWT\Signer algorithms in https://github.com/lcobucci/jwt/tree/master/src/Signer
5365
ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Rsa\Sha256::class,
54-
//ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Hmac\Sha256::class,
55-
//ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Ecdsa\Sha256::class,
66+
// ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Hmac\Sha256::class,
67+
// ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Ecdsa\Sha256::class,
5668

5769
/**
5870
* Authentication related options.
@@ -347,7 +359,7 @@ $config = [
347359
// Federation authority hints. An array of strings representing the Entity Identifiers of Intermediate Entities
348360
// (or Trust Anchors). Required if this entity has a Superior entity above it.
349361
ModuleConfig::OPTION_FEDERATION_AUTHORITY_HINTS => [
350-
//'https://intermediate.example.org/',
362+
// 'https://intermediate.example.org/',
351363
],
352364

353365
// (optional) Federation Trust Mark tokens. An array of tokens (signed JWTs), each representing a Trust Mark
@@ -411,13 +423,22 @@ $config = [
411423
* entity statements. Note that these keys SHOULD NOT be the same as the ones used in OIDC protocol itself.
412424
*/
413425
// The federation private key passphrase (optional).
414-
//ModuleConfig::OPTION_PKI_FEDERATION_PRIVATE_KEY_PASSPHRASE => 'secret',
426+
// ModuleConfig::OPTION_PKI_FEDERATION_PRIVATE_KEY_PASSPHRASE => 'secret',
415427
// The federation certificate and private key filenames, with given defaults.
416428
ModuleConfig::OPTION_PKI_FEDERATION_PRIVATE_KEY_FILENAME =>
417429
ModuleConfig::DEFAULT_PKI_FEDERATION_PRIVATE_KEY_FILENAME,
418430
ModuleConfig::OPTION_PKI_FEDERATION_CERTIFICATE_FILENAME =>
419431
ModuleConfig::DEFAULT_PKI_FEDERATION_CERTIFICATE_FILENAME,
420432

433+
/**
434+
* (optional) Key rollover settings related to OpenID Federation. Check the OIDC protocol key rollover description
435+
* on how this works.
436+
*/
437+
// The federation (new) private key passphrase (optional).
438+
ModuleConfig::OPTION_PKI_FEDERATION_NEW_PRIVATE_KEY_PASSPHRASE => 'new-secret',
439+
ModuleConfig::OPTION_PKI_FEDERATION_NEW_PRIVATE_KEY_FILENAME => 'new_oidc_module_federation.key',
440+
ModuleConfig::OPTION_PKI_FEDERATION_NEW_CERTIFICATE_FILENAME => 'new_oidc_module_federation.crt',
441+
421442
// Federation token signer, with given default.
422443
ModuleConfig::OPTION_FEDERATION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Rsa\Sha256::class,
423444

src/ModuleConfig.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ class ModuleConfig
8484
final public const OPTION_FEDERATION_PARTICIPATION_LIMIT_BY_TRUST_MARKS =
8585
'federation_participation_limit_by_trust_marks';
8686

87+
final public const OPTION_PKI_NEW_PRIVATE_KEY_PASSPHRASE = 'new_private_key_passphrase';
88+
final public const OPTION_PKI_NEW_PRIVATE_KEY_FILENAME = 'new_privatekey';
89+
final public const OPTION_PKI_NEW_CERTIFICATE_FILENAME = 'new_certificate';
90+
91+
final public const OPTION_PKI_FEDERATION_NEW_PRIVATE_KEY_PASSPHRASE = 'federation_new_private_key_passphrase';
92+
final public const OPTION_PKI_FEDERATION_NEW_PRIVATE_KEY_FILENAME = 'federation_new_private_key_filename';
93+
final public const OPTION_PKI_FEDERATION_NEW_CERTIFICATE_FILENAME = 'federation_new_certificate_filename';
94+
8795
protected static array $standardScopes = [
8896
ScopesEnum::OpenId->value => [
8997
self::KEY_DESCRIPTION => 'openid',
@@ -365,6 +373,22 @@ public function getProtocolCertPath(): string
365373
return $this->sspBridge->utils()->config()->getCertPath($certName);
366374
}
367375

376+
/**
377+
* Get the path to the new public certificate to be used in OIDC protocol.
378+
* @return ?string Null if not set, or file system path
379+
* @throws \Exception
380+
*/
381+
public function getProtocolNewCertPath(): ?string
382+
{
383+
$certName = $this->config()->getOptionalString(self::OPTION_PKI_NEW_CERTIFICATE_FILENAME, null);
384+
385+
if (is_string($certName)) {
386+
return $this->sspBridge->utils()->config()->getCertPath($certName);
387+
}
388+
389+
return null;
390+
}
391+
368392
/**
369393
* Get supported Authentication Context Class References (ACRs).
370394
*
@@ -522,7 +546,6 @@ public function getFederationPrivateKeyPassPhrase(): ?string
522546

523547
/**
524548
* Return the path to the federation public certificate
525-
* @return string The file system path or null if not set.
526549
* @throws \Exception
527550
*/
528551
public function getFederationCertPath(): string
@@ -535,6 +558,25 @@ public function getFederationCertPath(): string
535558
return $this->sspBridge->utils()->config()->getCertPath($certName);
536559
}
537560

561+
/**
562+
* Return the path to the new federation public certificate
563+
* @return ?string The file system path or null if not set.
564+
* @throws \Exception
565+
*/
566+
public function getFederationNewCertPath(): ?string
567+
{
568+
$certName = $this->config()->getOptionalString(
569+
self::OPTION_PKI_FEDERATION_NEW_CERTIFICATE_FILENAME,
570+
null,
571+
);
572+
573+
if (is_string($certName)) {
574+
return $this->sspBridge->utils()->config()->getCertPath($certName);
575+
}
576+
577+
return null;
578+
}
579+
538580
/**
539581
* @throws \Exception
540582
*/

src/Services/JsonWebKeySetService.php

Lines changed: 75 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,42 +27,20 @@
2727
class JsonWebKeySetService
2828
{
2929
/** @var JWKSet JWKS for OIDC protocol. */
30-
private readonly JWKSet $protocolJwkSet;
30+
protected JWKSet $protocolJwkSet;
3131
/** @var JWKSet|null JWKS for OpenID Federation. */
32-
private ?JWKSet $federationJwkSet = null;
32+
protected ?JWKSet $federationJwkSet = null;
3333

3434
/**
3535
* @throws \SimpleSAML\Error\Exception
3636
* @throws \Exception
3737
*/
38-
public function __construct(ModuleConfig $moduleConfig)
39-
{
40-
$publicKeyPath = $moduleConfig->getProtocolCertPath();
41-
if (!file_exists($publicKeyPath)) {
42-
throw new Error\Exception("OIDC protocol public key file does not exists: $publicKeyPath.");
43-
}
38+
public function __construct(
39+
protected readonly ModuleConfig $moduleConfig,
40+
) {
41+
$this->prepareProtocolJwkSet();
4442

45-
$jwk = JWKFactory::createFromKeyFile($publicKeyPath, null, [
46-
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($publicKeyPath),
47-
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
48-
ClaimsEnum::Alg->value => $moduleConfig->getProtocolSigner()->algorithmId(),
49-
]);
50-
51-
$this->protocolJwkSet = new JWKSet([$jwk]);
52-
53-
if (
54-
($federationPublicKeyPath = $moduleConfig->getFederationCertPath()) &&
55-
file_exists($federationPublicKeyPath) &&
56-
($federationSigner = $moduleConfig->getFederationSigner())
57-
) {
58-
$federationJwk = JWKFactory::createFromKeyFile($federationPublicKeyPath, null, [
59-
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($federationPublicKeyPath),
60-
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
61-
ClaimsEnum::Alg->value => $federationSigner->algorithmId(),
62-
]);
63-
64-
$this->federationJwkSet = new JWKSet([$federationJwk]);
65-
}
43+
$this->prepareFederationJwkSet();
6644
}
6745

6846
/**
@@ -84,4 +62,72 @@ public function federationKeys(): array
8462

8563
return $this->federationJwkSet->all();
8664
}
65+
66+
/**
67+
* @throws \ReflectionException
68+
* @throws \SimpleSAML\Error\Exception
69+
*/
70+
protected function prepareProtocolJwkSet(): void
71+
{
72+
$protocolPublicKeyPath = $this->moduleConfig->getProtocolCertPath();
73+
74+
if (!file_exists($protocolPublicKeyPath)) {
75+
throw new Error\Exception("OIDC protocol public key file does not exists: $protocolPublicKeyPath.");
76+
}
77+
78+
$jwk = JWKFactory::createFromKeyFile($protocolPublicKeyPath, null, [
79+
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($protocolPublicKeyPath),
80+
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
81+
ClaimsEnum::Alg->value => $this->moduleConfig->getProtocolSigner()->algorithmId(),
82+
]);
83+
84+
$keys = [$jwk];
85+
86+
if (
87+
($protocolNewPublicKeyPath = $this->moduleConfig->getProtocolNewCertPath()) &&
88+
file_exists($protocolNewPublicKeyPath)
89+
) {
90+
$newJwk = JWKFactory::createFromKeyFile($protocolNewPublicKeyPath, null, [
91+
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($protocolNewPublicKeyPath),
92+
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
93+
ClaimsEnum::Alg->value => $this->moduleConfig->getProtocolSigner()->algorithmId(),
94+
]);
95+
96+
$keys[] = $newJwk;
97+
}
98+
99+
$this->protocolJwkSet = new JWKSet($keys);
100+
}
101+
102+
protected function prepareFederationJwkSet(): void
103+
{
104+
$federationPublicKeyPath = $this->moduleConfig->getFederationCertPath();
105+
106+
if (!file_exists($federationPublicKeyPath)) {
107+
return;
108+
}
109+
110+
$federationJwk = JWKFactory::createFromKeyFile($federationPublicKeyPath, null, [
111+
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($federationPublicKeyPath),
112+
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
113+
ClaimsEnum::Alg->value => $this->moduleConfig->getFederationSigner()->algorithmId(),
114+
]);
115+
116+
$keys = [$federationJwk];
117+
118+
if (
119+
($federationNewPublicKeyPath = $this->moduleConfig->getFederationNewCertPath()) &&
120+
file_exists($federationNewPublicKeyPath)
121+
) {
122+
$federationNewJwk = JWKFactory::createFromKeyFile($federationNewPublicKeyPath, null, [
123+
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($federationNewPublicKeyPath),
124+
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
125+
ClaimsEnum::Alg->value => $this->moduleConfig->getFederationSigner()->algorithmId(),
126+
]);
127+
128+
$keys[] = $federationNewJwk;
129+
}
130+
131+
$this->federationJwkSet = new JWKSet($keys);
132+
}
87133
}

0 commit comments

Comments
 (0)