Skip to content

Commit 1200abc

Browse files
wolpertclaude
andcommitted
feat(examples): wire offered/accepted COSE algorithm config through adapters + demos
Plumb the crypto-agility algorithm lists (ADR 0019) through all three adapter config surfaces and set them explicitly in each example app, completing the ADR's "wire through external host config" follow-up. - Spring: pkauth.ceremony.offered-algorithms / accepted-algorithms bound on PkAuthProperties.Ceremony and forwarded via the 7-arg CeremonyConfig.from. - Micronaut: same keys on PkAuthConfiguration.Ceremony (getter/setter pairs), forwarded in PkAuthFactory. - Dropwizard: offeredAlgorithms / acceptedAlgorithms record components on PkAuthConfig.Ceremony (TTL-only and no-arg constructors kept for back-compat), forwarded in PkAuthModule. - Demos: spring + micronaut application.yml set both lists; the Java-configured dropwizard DemoConfiguration sets them on PkAuthConfig.Ceremony. All set to the core defaults as living documentation; nulls still fall back to those defaults. - Docs: operator-guide section on the two knobs; ADR 0019 follow-up marked done. Verified: ./gradlew check passes, and all three demos boot with the new config and pass their Playwright e2e suites (the enum-list binding is exercised by a real registration ceremony in each). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a85c309 commit 1200abc

11 files changed

Lines changed: 127 additions & 16 deletions

File tree

docs/adr/0019-crypto-agility-post-quantum-readiness.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,12 @@ verification and *are* Shor-vulnerable, with exposure bounded by the short token
8585

8686
## Open follow-ups
8787

88-
- Wire `offeredAlgorithms` / `acceptedAlgorithms` through each adapter's external (YAML) host config.
89-
Today they are overridable via `CeremonyConfig` / the `PasskeyAuthenticationServices` builder; full
90-
per-adapter property plumbing is deferred until there is a concrete reason to narrow the lists from
91-
configuration.
88+
- ~~Wire `offeredAlgorithms` / `acceptedAlgorithms` through each adapter's external host config.~~
89+
Done: bound under `pkauth.ceremony.offered-algorithms` / `pkauth.ceremony.accepted-algorithms` in
90+
the Spring starter (`PkAuthProperties.Ceremony`) and Micronaut adapter (`PkAuthConfiguration`),
91+
and as the `PkAuthConfig.Ceremony(offeredAlgorithms, acceptedAlgorithms)` record components in the
92+
Dropwizard adapter. Each adapter null-coalesces to the core defaults and passes the lists to the
93+
seven-argument `CeremonyConfig.from(...)`. All three example apps set them explicitly (to the
94+
defaults) as living documentation.
9295
- Revisit when a COSE-registered post-quantum signature algorithm lands in WebAuthn4J; adding it is
9396
the localized change this ADR was written to enable.

docs/operator-guide.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,27 @@ authenticator (mere user-presence, no biometric/PIN). A passkey then degrades to
4646
UV-incapable roaming hardware security keys; if you do, scope it deliberately and
4747
understand the trade-off.
4848

49+
### COSE signature algorithms (crypto-agility)
50+
51+
Two ceremony knobs control which COSE signature algorithms are used (ADR 0019):
52+
53+
- `pkauth.ceremony.offered-algorithms` — advertised to the authenticator in
54+
registration create-options. Default: `[ES256, EdDSA, RS256]`.
55+
- `pkauth.ceremony.accepted-algorithms` — the verify allow-list; a credential
56+
whose algorithm is absent is rejected on registration. Default (and the
57+
enforced superset): `[ES256, EdDSA, RS256, ES384, RS384]`.
58+
59+
`offered` must be a subset of `accepted`. The defaults are the historical union,
60+
so leaving them unset changes nothing. You may **narrow** either (e.g. drop RSA),
61+
but narrowing `accepted` can reject **already-registered** credentials that use a
62+
removed algorithm — drive a re-enrollment campaign first (see
63+
`AdminService.listCredentialsByAlgorithm`). In Spring and Micronaut these bind
64+
from `application.yml`; in Dropwizard they are the `PkAuthConfig.Ceremony`
65+
record's `offeredAlgorithms` / `acceptedAlgorithms` components. All three example
66+
apps set them explicitly (to the defaults) as living documentation. There is no
67+
post-quantum signature algorithm to select yet — see `docs/threat-model.md`
68+
(Post-quantum readiness).
69+
4970
## 3. Persistence migrations
5071

5172
### JDBI / Postgres

examples/dropwizard-demo/src/main/java/com/codeheadsystems/pkauth/demo/dropwizard/DemoConfiguration.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// SPDX-License-Identifier: MIT
22
package com.codeheadsystems.pkauth.demo.dropwizard;
33

4+
import com.codeheadsystems.pkauth.config.CoseAlgorithm;
45
import com.codeheadsystems.pkauth.dropwizard.HasPkAuthConfig;
56
import com.codeheadsystems.pkauth.dropwizard.config.PkAuthConfig;
67
import io.dropwizard.core.Configuration;
8+
import java.util.List;
79
import java.util.Set;
810

911
/**
@@ -18,7 +20,21 @@ public final class DemoConfiguration extends Configuration implements HasPkAuthC
1820
new PkAuthConfig.RelyingParty(
1921
"localhost", "pk-auth demo", Set.of("http://localhost:8080")),
2022
new PkAuthConfig.Jwt("https://issuer.local", "pkauth-demo", defaultDevSecret(), null),
21-
new PkAuthConfig.Ceremony(),
23+
// Crypto-agility (ADR 0019): the COSE algorithm lists are explicit here for illustration.
24+
// These match the core defaults — `offered` is advertised to the authenticator,
25+
// `accepted`
26+
// is the (superset) verify allow-list. An operator can narrow either; the defaults are
27+
// the
28+
// historical union so no already-registered credential can fail verification.
29+
new PkAuthConfig.Ceremony(
30+
null,
31+
List.of(CoseAlgorithm.ES256, CoseAlgorithm.EdDSA, CoseAlgorithm.RS256),
32+
List.of(
33+
CoseAlgorithm.ES256,
34+
CoseAlgorithm.EdDSA,
35+
CoseAlgorithm.RS256,
36+
CoseAlgorithm.ES384,
37+
CoseAlgorithm.RS384)),
2238
new PkAuthConfig.Otp(defaultDevOtpPepper()),
2339
new PkAuthConfig.MagicLink("http://localhost:8080"),
2440
new PkAuthConfig.BackupCode());

examples/micronaut-demo/src/main/resources/application.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,10 @@ pkauth:
1717
issuer: pk-auth-micronaut-demo
1818
audience: pk-auth-micronaut-demo-clients
1919
secret: change-me-in-prod-use-a-32-byte-secret!!
20+
ceremony:
21+
# Crypto-agility (ADR 0019): COSE signature algorithms. `offered-algorithms` is advertised to
22+
# the authenticator in create-options; `accepted-algorithms` is the verify allow-list and must
23+
# be a superset. Listed here as the explicit core defaults; an operator can narrow either
24+
# (narrowing `accepted` can reject already-registered credentials, so change deliberately).
25+
offered-algorithms: [ES256, EdDSA, RS256]
26+
accepted-algorithms: [ES256, EdDSA, RS256, ES384, RS384]

examples/spring-boot-demo/src/main/resources/application.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ pkauth:
2424
token-ttl: PT1H
2525
ceremony:
2626
challenge-ttl: PT5M
27+
# Crypto-agility (ADR 0019): COSE signature algorithms. `offered-algorithms` is advertised to
28+
# the authenticator in create-options; `accepted-algorithms` is the verify allow-list and must
29+
# be a superset. Listed here as the explicit core defaults; an operator can narrow either
30+
# (narrowing `accepted` can reject already-registered credentials, so change deliberately).
31+
offered-algorithms: [ES256, EdDSA, RS256]
32+
accepted-algorithms: [ES256, EdDSA, RS256, ES384, RS384]
2733
logging:
2834
level:
2935
com.codeheadsystems.pkauth: INFO

pk-auth-dropwizard/src/main/java/com/codeheadsystems/pkauth/dropwizard/config/PkAuthConfig.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// SPDX-License-Identifier: MIT
22
package com.codeheadsystems.pkauth.dropwizard.config;
33

4+
import com.codeheadsystems.pkauth.config.CoseAlgorithm;
45
import com.codeheadsystems.pkauth.refresh.RefreshTokenConfig;
56
import java.time.Duration;
67
import java.util.LinkedHashMap;
78
import java.util.LinkedHashSet;
9+
import java.util.List;
810
import java.util.Map;
911
import java.util.Objects;
1012
import java.util.Set;
@@ -148,10 +150,22 @@ public byte[] secret() {
148150
* com.codeheadsystems.pkauth.config.CeremonyConfig#defaults()}.
149151
*
150152
* @param challengeTtl Override for challenge TTL; null = use the brief's 5-minute default.
153+
* @param offeredAlgorithms COSE algorithms advertised in create-options; null = core default
154+
* (ES256, EdDSA, RS256). Must be a subset of {@code acceptedAlgorithms}.
155+
* @param acceptedAlgorithms COSE algorithms accepted on verify; null = core default union (ES256,
156+
* EdDSA, RS256, ES384, RS384). Narrowing can reject already-registered credentials.
151157
*/
152-
public record Ceremony(@Nullable Duration challengeTtl) {
158+
public record Ceremony(
159+
@Nullable Duration challengeTtl,
160+
@Nullable List<CoseAlgorithm> offeredAlgorithms,
161+
@Nullable List<CoseAlgorithm> acceptedAlgorithms) {
153162
public Ceremony() {
154-
this(null);
163+
this(null, null, null);
164+
}
165+
166+
/** Back-compat convenience for the TTL-only construction used before crypto-agility config. */
167+
public Ceremony(@Nullable Duration challengeTtl) {
168+
this(challengeTtl, null, null);
155169
}
156170
}
157171

pk-auth-dropwizard/src/main/java/com/codeheadsystems/pkauth/dropwizard/dagger/PkAuthModule.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,11 @@ RelyingPartyConfig provideRelyingPartyConfig(PkAuthConfig cfg) {
104104
@Provides
105105
@Singleton
106106
CeremonyConfig provideCeremonyConfig(PkAuthConfig cfg) {
107-
// Only challengeTtl is host-settable here; the remaining knobs take the conservative core
108-
// defaults (UV=REQUIRED, counter=REJECT) via the null fallbacks.
109-
return CeremonyConfig.from(cfg.ceremony().challengeTtl(), null, null, null, null);
107+
// challengeTtl and the COSE algorithm lists are host-settable; the remaining knobs take the
108+
// conservative core defaults (UV=REQUIRED, counter=REJECT) via the null fallbacks.
109+
PkAuthConfig.Ceremony c = cfg.ceremony();
110+
return CeremonyConfig.from(
111+
c.challengeTtl(), null, null, null, null, c.offeredAlgorithms(), c.acceptedAlgorithms());
110112
}
111113

112114
/**

pk-auth-micronaut/src/main/java/com/codeheadsystems/pkauth/micronaut/PkAuthConfiguration.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
package com.codeheadsystems.pkauth.micronaut;
33

4+
import com.codeheadsystems.pkauth.config.CoseAlgorithm;
45
import io.micronaut.context.annotation.ConfigurationProperties;
56
import java.time.Duration;
67
import java.util.List;
@@ -103,6 +104,8 @@ public void setDevMode(boolean devMode) {
103104
@ConfigurationProperties("ceremony")
104105
public static final class Ceremony {
105106
private Duration challengeTtl = Duration.ofMinutes(5);
107+
@Nullable private List<CoseAlgorithm> offeredAlgorithms;
108+
@Nullable private List<CoseAlgorithm> acceptedAlgorithms;
106109

107110
public Duration getChallengeTtl() {
108111
return challengeTtl;
@@ -111,6 +114,26 @@ public Duration getChallengeTtl() {
111114
public void setChallengeTtl(Duration challengeTtl) {
112115
this.challengeTtl = challengeTtl;
113116
}
117+
118+
/** COSE algorithms advertised in create-options; null = core default (ES256, EdDSA, RS256). */
119+
@Nullable
120+
public List<CoseAlgorithm> getOfferedAlgorithms() {
121+
return offeredAlgorithms;
122+
}
123+
124+
public void setOfferedAlgorithms(@Nullable List<CoseAlgorithm> offeredAlgorithms) {
125+
this.offeredAlgorithms = offeredAlgorithms;
126+
}
127+
128+
/** COSE algorithms accepted on verify; null = core default union (adds ES384, RS384). */
129+
@Nullable
130+
public List<CoseAlgorithm> getAcceptedAlgorithms() {
131+
return acceptedAlgorithms;
132+
}
133+
134+
public void setAcceptedAlgorithms(@Nullable List<CoseAlgorithm> acceptedAlgorithms) {
135+
this.acceptedAlgorithms = acceptedAlgorithms;
136+
}
114137
}
115138

116139
/**

pk-auth-micronaut/src/main/java/com/codeheadsystems/pkauth/micronaut/PkAuthFactory.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,17 @@ RelyingPartyConfig relyingPartyConfig(PkAuthConfiguration config) {
6565

6666
@Singleton
6767
CeremonyConfig ceremonyConfig(PkAuthConfiguration config) {
68-
// Only challengeTtl is host-settable here; the remaining knobs take the conservative core
69-
// defaults (UV=REQUIRED, counter=REJECT) via the null fallbacks.
70-
return CeremonyConfig.from(config.getCeremony().getChallengeTtl(), null, null, null, null);
68+
// challengeTtl and the COSE algorithm lists are host-settable; the remaining knobs take the
69+
// conservative core defaults (UV=REQUIRED, counter=REJECT) via the null fallbacks.
70+
PkAuthConfiguration.Ceremony c = config.getCeremony();
71+
return CeremonyConfig.from(
72+
c.getChallengeTtl(),
73+
null,
74+
null,
75+
null,
76+
null,
77+
c.getOfferedAlgorithms(),
78+
c.getAcceptedAlgorithms());
7179
}
7280

7381
@Singleton

pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/autoconfigure/PkAuthAutoConfiguration.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ public CeremonyConfig pkAuthCeremonyConfig(PkAuthProperties props) {
118118
c.userVerification(),
119119
c.residentKey(),
120120
c.attestation(),
121-
c.counterRegression());
121+
c.counterRegression(),
122+
c.offeredAlgorithms(),
123+
c.acceptedAlgorithms());
122124
}
123125

124126
/**

0 commit comments

Comments
 (0)