Skip to content

Commit ad29af5

Browse files
committed
Ship a distributed configuration a documented install can actually load
1 parent 477bfa2 commit ad29af5

6 files changed

Lines changed: 203 additions & 22 deletions

File tree

config/module_oidc.php.dist

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,18 @@ $config = [
8888
// ModuleConfig::KEY_PRIVATE_KEY_PASSWORD => 'private-key-password', // Optional
8989
// ModuleConfig::KEY_KEY_ID => 'rsa-connect-signing-key-2026', // Optional
9090
],
91-
// Example for additionally supported ES256 algorithm with EC keys.
92-
// Delete it if not needed:
93-
[
94-
ModuleConfig::KEY_ALGORITHM => \SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum::ES256,
95-
ModuleConfig::KEY_PRIVATE_KEY_FILENAME => 'oidc_module_connect_ec_p256_01.key',
96-
ModuleConfig::KEY_PUBLIC_KEY_FILENAME => 'oidc_module_connect_ec_p256_01.pub',
91+
// Example for an additionally supported ES256 algorithm with EC keys. Uncomment it
92+
// once the corresponding EC key pair exists, as described in the installation guide.
93+
// Note that every entry in this list is resolved before any JWS can be signed, so an
94+
// entry referring to a key file that is not there disables protocol signing
95+
// altogether, not only the algorithm that entry describes.
96+
// [
97+
// ModuleConfig::KEY_ALGORITHM => \SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum::ES256,
98+
// ModuleConfig::KEY_PRIVATE_KEY_FILENAME => 'oidc_module_connect_ec_p256_01.key',
99+
// ModuleConfig::KEY_PUBLIC_KEY_FILENAME => 'oidc_module_connect_ec_p256_01.pub',
97100
// ModuleConfig::KEY_PRIVATE_KEY_PASSWORD => 'private-key-password', // Optional
98101
// ModuleConfig::KEY_KEY_ID => 'ec-connect-signing-key-01', // Optional
99-
],
102+
// ],
100103
],
101104

102105
/**
@@ -1032,6 +1035,18 @@ $config = [
10321035
* (option ModuleConfig::OPTION_PROTOCOL_SIGNATURE_KEY_PAIRS)
10331036
*
10341037
* NOTE: for the time being, only one key-pair is supported.
1038+
*
1039+
* Unlike the protocol (Connect) default, this defaults to ES256 with an EC
1040+
* key pair, since that is what wallets commonly expect of an issuer. A
1041+
* deployment that has never enabled VCI does not need the key pair named
1042+
* below; if you do enable VCI, create it first (see the installation guide)
1043+
* or point these entries at keys you already have.
1044+
*
1045+
* Note that disabling VCI afterwards does not release these keys. Status
1046+
* Lists keep being served so that already issued credentials stay
1047+
* verifiable, and they are signed with keys resolved through this option, so
1048+
* an entry has to remain here for as long as any Status List it signed is
1049+
* still served.
10351050
*/
10361051
ModuleConfig::OPTION_VCI_SIGNATURE_KEY_PAIRS => [
10371052
[

docs/1-oidc.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ Currently supported OIDFed features:
9999
- Automatic client registration using a Request Object
100100
- Federation participation limiting based on Trust Marks
101101
- Endpoint for issuing a configuration entity statement (about itself)
102-
- Fetch endpoint for issuing statements about subordinates (clients)
103-
- Subordinate listing endpoint
102+
103+
The OP participates as a leaf entity, so it deliberately does not serve a fetch
104+
endpoint or a subordinate listing endpoint.
104105

105106
OIDFed is implemented using the
106107
[SimpleSAMLphp OpenID library](https://github.com/simplesamlphp/openid).
@@ -115,13 +116,15 @@ Currently implemented OpenID4VCI features:
115116
- Implemented Endpoints
116117
- Credential Issuer Metadata | `.well-known/openid-credential-issuer` -
117118
Advertises supported credentials, algorithms, and endpoints.
118-
- Credential Endpoint | `/vci/credential`- Handles credential requests with
119-
proof of possession.
120-
- Nonce Endpoint - `/vci/nonce` - Provides nonces (`c_nonce`).
121-
- Credential Offer (API) | `/api/vci/offer` - Allows triggering credential
122-
offers via administrative API.
123-
- JSON-LD Context | `/vci/context/{id}` - Serves custom JSON-LD contexts for
124-
`vc+sd-jwt` credentials.
119+
- Credential Endpoint | `credential-issuer/credential` - Handles credential
120+
requests with proof of possession.
121+
- Nonce Endpoint | `credential-issuer/nonce` - Provides nonces (`c_nonce`).
122+
- Credential Offer (API) | `api/vci/credential-offer` - Allows triggering
123+
credential offers via administrative API.
124+
- Credential Status (API) | `api/vci/credential-status` - Withdraws, suspends
125+
or reinstates an issued credential through its Token Status List entry.
126+
- JSON-LD Context | `credential-issuer/context/{credentialConfigurationId}` -
127+
Serves custom JSON-LD contexts for `vc+sd-jwt` credentials.
125128
- Supported Flows & Grant Types
126129
- Authorization Code Flow: Fully supported
127130
- Pre-Authorized Code Flow: Fully supported
@@ -134,7 +137,7 @@ Currently implemented OpenID4VCI features:
134137
Disclosure JWT.
135138
- Digital Credentials SD-JWT (`dc+sd-jwt`): IETF Draft 14+ Selective
136139
Disclosure JWT.
137-
Proof of Possession & Binding
140+
- Proof of Possession & Binding
138141
- Proof Type: `jwt` (JSON Web Token proofs).
139142
- Cryptographic Binding Methods:
140143
- `did:key`: Supported for proof validation and subject binding.

docs/2-oidc-installation.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@ keys for protocol (Connect), Federation and Verifiable Credential (VCI)
4343
operations. You must have at least one algorithm / key-pair for protocol
4444
(Connect), and for Federation and VCI if you use those features.
4545

46+
Create the key pairs that your `config/module_oidc.php` actually refers to, and
47+
no others. As distributed, that file activates a single protocol (Connect) key
48+
pair, `RS256` with `oidc_module_connect_rsa_01`, so following the RSA
49+
instructions below is enough to get the module running. The other algorithms
50+
shown here are examples that ship commented out; uncomment an entry only once
51+
its key pair exists.
52+
53+
Getting that wrong is not a partial failure. Every activated key pair is
54+
resolved before any JWS is signed, so an entry naming a key file that is not
55+
there stops the module from signing at all, rather than only disabling the
56+
algorithm that entry describes.
57+
58+
Federation key pairs are read only when Federation is enabled. VCI ships with an
59+
`ES256` entry rather than an RSA one, so enabling VCI means creating the EC key
60+
pair from the section below. Keep that key pair even if you later disable VCI:
61+
Status Lists continue to be served so that credentials you have already issued
62+
stay verifiable, and they are signed with it.
63+
4664
### RSA key pair generation, for `RS256/384/512` and `PS256/384/512` algorithms
4765

4866
Generate private keys without a password:
@@ -83,7 +101,10 @@ Enter algorithm, key file names, and a password (if used) in `config/module_oidc
83101

84102
### EC key pair generation, per curve for different algorithms
85103

86-
If you prefer to use Elliptic Curve Cryptography (ECC) instead of RSA.
104+
Use these if you prefer Elliptic Curve Cryptography (ECC). Since each key-pair
105+
option is a list, EC keys can replace the RSA ones or be advertised alongside
106+
them, which is what the commented-out `ES256` example in the distributed
107+
configuration does.
87108

88109
Generate private EC P‑256 keys without a password, usable for `ES256` algorithm:
89110

docs/3-oidc-configuration.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,9 @@ If not set, you will see warnings about this in the logs.
206206

207207
## Private scopes
208208

209-
The module supports the standard scopes: `openid`, `email`, `address`,
210-
`phone`, and `profile`. You can add private scopes in `module_oidc.php`:
209+
The module supports the standard scopes: `openid`, `offline_access`, `email`,
210+
`address`, `phone`, and `profile`. You can add private scopes in
211+
`module_oidc.php`:
211212

212213
```php
213214
<?php

docs/8-api.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ use SimpleSAML\Module\oidc\ModuleConfig;
1111
ModuleConfig::OPTION_API_ENABLED => true,
1212
```
1313

14+
This is the master switch, and on its own it exposes nothing. Every endpoint
15+
also has its own switch, and some additionally depend on the feature they belong
16+
to being enabled. All of these default to `false`, so an endpoint answers only
17+
once this option and the ones listed with that endpoint below are all `true`.
18+
1419

1520
## API Authentication and Authorization
1621

@@ -76,6 +81,16 @@ SimpleSAMLphp installation.
7681

7782
Enables fetching a credential offer as per OpenID4VCI specification.
7883

84+
Enable it in `config/module_oidc.php`, together with the VCI feature itself,
85+
which this endpoint depends on:
86+
87+
```php
88+
use SimpleSAML\Module\oidc\ModuleConfig;
89+
90+
ModuleConfig::OPTION_VCI_ENABLED => true,
91+
ModuleConfig::OPTION_API_VCI_CREDENTIAL_OFFER_ENDPOINT_ENABLED => true,
92+
```
93+
7994
#### Path
8095

8196
`/api/vci/credential-offer`
@@ -157,9 +172,9 @@ curl --location 'https://idp.mivanci.incubator.hexaa.eu/ssp/module.php/oidc/api/
157172
"use_tx_code": true,
158173
"users_email_attribute_name": "mail",
159174
"user_attributes": {
160-
"uid": [testuseruid"],
175+
"uid": ["testuseruid"],
161176
"mail": ["testuser@example.com"],
162-
"...": [..."]
177+
"...": ["..."]
163178
}
164179
}'
165180
```
@@ -296,6 +311,14 @@ Response:
296311
Enables token introspection for OAuth2 access tokens and refresh tokens as per
297312
[RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662).
298313

314+
Enable it in `config/module_oidc.php`:
315+
316+
```php
317+
use SimpleSAML\Module\oidc\ModuleConfig;
318+
319+
ModuleConfig::OPTION_API_OAUTH2_TOKEN_INTROSPECTION_ENDPOINT_ENABLED => true,
320+
```
321+
299322
#### Path
300323

301324
`/api/oauth2/token-introspection`
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Test\Module\oidc\unit;
6+
7+
use PHPUnit\Framework\Attributes\CoversNothing;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use PHPUnit\Framework\TestCase;
10+
use SimpleSAML\Module\oidc\ModuleConfig;
11+
use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum;
12+
13+
/**
14+
* Keeps the configuration file that ships with the module in step with the installation guide.
15+
*
16+
* Nothing else in the suite reads `config/module_oidc.php.dist`: both `tests/config/module_oidc.php`
17+
* and `docker/ssp/module_oidc.php` declare their own key pairs. The distributed file is therefore
18+
* exercised for the first time by whoever installs the module, which is how it came to activate an
19+
* ES256 entry pointing at EC key files that the guide creates only in an optional section.
20+
*/
21+
#[CoversNothing]
22+
class DistributedConfigTest extends TestCase
23+
{
24+
/** @var array<string,mixed> */
25+
protected array $distributedConfig;
26+
27+
protected string $installationGuide;
28+
29+
protected function setUp(): void
30+
{
31+
$repositoryRoot = dirname(__DIR__, 3);
32+
33+
// The distributed configuration follows the SimpleSAMLphp convention of assigning $config
34+
// instead of returning it, so include it for that side effect and pick the variable up.
35+
require $repositoryRoot . '/config/module_oidc.php.dist';
36+
/** @var array<string,mixed> $config */
37+
$this->distributedConfig = $config;
38+
39+
$this->installationGuide = (string)file_get_contents(
40+
$repositoryRoot . '/docs/2-oidc-installation.md',
41+
);
42+
}
43+
44+
/**
45+
* The protocol key pairs are resolved before any JWS can be signed, and all of them are, so an
46+
* entry naming a key file that a fresh installation does not have stops the module from signing
47+
* altogether rather than just dropping its own algorithm. Only the RSA pair that the guide
48+
* creates in its baseline instructions may therefore ship active; anything else belongs in the
49+
* file commented out, next to the key generation it needs.
50+
*/
51+
public function testDistributedProtocolKeyPairsMatchTheBaselineInstallation(): void
52+
{
53+
/** @var array<array<string,mixed>> $keyPairs */
54+
$keyPairs = $this->distributedConfig[ModuleConfig::OPTION_PROTOCOL_SIGNATURE_KEY_PAIRS];
55+
56+
$this->assertCount(
57+
1,
58+
$keyPairs,
59+
'The distributed configuration must activate exactly the one protocol key pair that the ' .
60+
'baseline installation instructions create. Ship additional algorithms commented out.',
61+
);
62+
63+
$keyPair = $keyPairs[0];
64+
65+
$this->assertSame(SignatureAlgorithmEnum::RS256, $keyPair[ModuleConfig::KEY_ALGORITHM]);
66+
$this->assertSame(
67+
'oidc_module_connect_rsa_01.key',
68+
$keyPair[ModuleConfig::KEY_PRIVATE_KEY_FILENAME],
69+
);
70+
$this->assertSame(
71+
'oidc_module_connect_rsa_01.pub',
72+
$keyPair[ModuleConfig::KEY_PUBLIC_KEY_FILENAME],
73+
);
74+
}
75+
76+
/**
77+
* Catches the milder version of the same drift: an active entry naming a key file that the guide
78+
* never tells anyone to create, in either direction, such as a renamed or mistyped filename.
79+
*/
80+
#[DataProvider('keyPairOptionProvider')]
81+
public function testActiveKeyFilenamesAreCreatedByTheInstallationGuide(string $option): void
82+
{
83+
/** @var array<array<string,mixed>> $keyPairs */
84+
$keyPairs = $this->distributedConfig[$option];
85+
86+
foreach ($keyPairs as $keyPair) {
87+
foreach (
88+
[ModuleConfig::KEY_PRIVATE_KEY_FILENAME, ModuleConfig::KEY_PUBLIC_KEY_FILENAME] as $filenameKey
89+
) {
90+
/** @var string $filename */
91+
$filename = $keyPair[$filenameKey];
92+
93+
$this->assertStringContainsString(
94+
$filename,
95+
$this->installationGuide,
96+
sprintf(
97+
'Option %s activates key file %s, which docs/2-oidc-installation.md does not ' .
98+
'tell anyone to create.',
99+
$option,
100+
$filename,
101+
),
102+
);
103+
}
104+
}
105+
}
106+
107+
/**
108+
* @return array<string,array{string}>
109+
*/
110+
public static function keyPairOptionProvider(): array
111+
{
112+
return [
113+
'protocol' => [ModuleConfig::OPTION_PROTOCOL_SIGNATURE_KEY_PAIRS],
114+
'federation' => [ModuleConfig::OPTION_FEDERATION_SIGNATURE_KEY_PAIRS],
115+
'vci' => [ModuleConfig::OPTION_VCI_SIGNATURE_KEY_PAIRS],
116+
];
117+
}
118+
}

0 commit comments

Comments
 (0)