99
1010namespace OC \OCM ;
1111
12+ use Firebase \JWT \JWK ;
13+ use Firebase \JWT \JWT ;
14+ use Firebase \JWT \Key ;
1215use JsonException ;
1316use OC \Security \IdentityProof \Manager ;
14- use OC \Security \Jwks \ Jwk ;
17+ use OC \Security \Signature \ Rfc9421 \ Algorithm ;
1518use OC \Security \Signature \Rfc9421 \IJwkResolvingSignatoryManager ;
1619use OCP \Http \Client \IClientService ;
1720use OCP \IAppConfig ;
@@ -51,7 +54,7 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
5154 /**
5255 * Each Ed25519 keypair lives in a numbered "pool" appkey. Three slot
5356 * pointers (active/pending/retiring) reference pools by id, so rotation
54- * is just pointer reshuffling — no copying of keypair bytes.
57+ * is just pointer reshuffling, no copying of keypair bytes.
5558 */
5659 private const APPKEY_ED25519_POOL_PREFIX = 'ocm_ed25519_pool_ ' ;
5760 private const APPCONFIG_ED25519_POOL_COUNTER = 'ocm_ed25519_pool_counter ' ;
@@ -173,10 +176,10 @@ public function getLocalEd25519Signatory(): ?Signatory {
173176 * for their JWKS cache to expire.
174177 *
175178 * The active key is provisioned on demand if it does not yet exist, so
176- * the first request to JWKS always returns at least one key — peers
179+ * the first request to JWKS always returns at least one key. Peers
177180 * must be able to discover us before we have ever signed anything.
178181 *
179- * @return list<Jwk >
182+ * @return list<array<string, string> >
180183 */
181184 public function getLocalEd25519Jwks (): array {
182185 if ($ this ->getSlotPool (self ::SLOT_ACTIVE ) === null ) {
@@ -191,7 +194,7 @@ public function getLocalEd25519Jwks(): array {
191194 }
192195 $ signatory = $ this ->signatoryFromPool ($ poolId );
193196 if ($ signatory !== null ) {
194- $ jwks [] = Jwk:: fromEd25519PublicKey ($ signatory ->getPublicKey (), $ signatory ->getKeyId ());
197+ $ jwks [] = self :: buildEd25519JwkArray ($ signatory ->getPublicKey (), $ signatory ->getKeyId ());
195198 }
196199 }
197200 return $ jwks ;
@@ -209,7 +212,7 @@ public function stageEd25519Key(): Signatory {
209212 if ($ this ->getSlotPool (self ::SLOT_PENDING ) !== null ) {
210213 throw new \RuntimeException ('a pending Ed25519 key already exists; activate or retire it first ' );
211214 }
212- // Make sure we have an active key to begin with — otherwise staging
215+ // Make sure we have an active key to begin with, otherwise staging
213216 // a "next" key with nothing to switch from would be odd.
214217 if ($ this ->getSlotPool (self ::SLOT_ACTIVE ) === null ) {
215218 $ this ->getLocalEd25519Signatory ();
@@ -303,7 +306,7 @@ public function listEd25519Keys(): array {
303306 * Generate a fresh Ed25519 keypair into a new pool, recording the kid
304307 * alongside. The kid is run through {@see Signatory::setKeyId} first so
305308 * the stored value matches the canonical form (no `/index.php/`, https
306- * scheme) used on the wire — otherwise admin output from
309+ * scheme) used on the wire, otherwise admin output from
307310 * `occ ocm:keys:list` would diverge from the kid actually published in
308311 * JWKS and used to sign requests.
309312 *
@@ -332,8 +335,8 @@ private function canonicalKid(string $kid): string {
332335 /**
333336 * Build a kid for a newly-generated key. The identity portion is derived
334337 * once (from the first request that creates a key) and persisted, so
335- * later rotations — including those triggered from CLI where there is
336- * no Host header — produce kids on the same hostname instead of falling
338+ * later rotations ( including those triggered from CLI where there is
339+ * no Host header) produce kids on the same hostname instead of falling
337340 * back to `overwrite.cli.url`.
338341 *
339342 * @throws \RuntimeException if no instance identity can be derived
@@ -349,7 +352,7 @@ private function nextEd25519PoolKid(): string {
349352 * every Ed25519 kid this instance has ever published. Resolution order:
350353 *
351354 * 1. {@see APPCONFIG_ED25519_KID_BASE} if previously stored.
352- * 2. The active pool's kid (with the `-N` suffix stripped) — handles
355+ * 2. The active pool's kid (with the `-N` suffix stripped); handles
353356 * instances upgraded from a single-key world without an explicit
354357 * base appconfig.
355358 * 3. {@see buildLocalKeyId} as a fresh derivation from the request
@@ -492,13 +495,13 @@ public function getRemoteSignatory(string $remote): ?Signatory {
492495 * Results are cached per-origin for {@see JWKS_CACHE_TTL} seconds so the
493496 * common case of repeated inbound requests from the same peer doesn't
494497 * trigger a fresh HTTPS round-trip each time. On a cache hit where the
495- * requested kid is missing, we refetch once — that lets a remote key
498+ * requested kid is missing, we refetch once. That lets a remote key
496499 * rotation propagate without waiting out the TTL.
497500 *
498- * @return Jwk |null null when the fetch fails or no key with that kid is published
501+ * @return Key |null null when the fetch fails or no key with that kid is published
499502 */
500503 #[\Override]
501- public function getRemoteJwk (string $ origin , string $ keyId ): ?Jwk {
504+ public function getRemoteKey (string $ origin , string $ keyId ): ?Key {
502505 $ keys = $ this ->readCachedJwks ($ origin );
503506 $ fromCache = $ keys !== null ;
504507 if (!$ fromCache ) {
@@ -508,12 +511,12 @@ public function getRemoteJwk(string $origin, string $keyId): ?Jwk {
508511 }
509512 }
510513
511- $ jwk = $ this ->findKid ($ keys , $ keyId );
512- if ($ jwk !== null ) {
513- return $ jwk ;
514+ $ key = $ this ->findKid ($ keys , $ keyId );
515+ if ($ key !== null ) {
516+ return $ key ;
514517 }
515518 // Only refetch if the answer came from the cache. A fresh fetch is
516- // already authoritative — refetching it just hammers the peer.
519+ // already authoritative; refetching it just hammers the peer.
517520 if (!$ fromCache ) {
518521 return null ;
519522 }
@@ -584,15 +587,35 @@ private function fetchJwks(string $origin): ?array {
584587 /**
585588 * @param list<array<string, mixed>>|null $keys
586589 */
587- private function findKid (?array $ keys , string $ keyId ): ?Jwk {
590+ private function findKid (?array $ keys , string $ keyId ): ?Key {
588591 if ($ keys === null ) {
589592 return null ;
590593 }
591594 foreach ($ keys as $ entry ) {
592- if (($ entry ['kid ' ] ?? null ) === $ keyId ) {
593- return Jwk::fromArray ($ entry );
595+ if (($ entry ['kid ' ] ?? null ) !== $ keyId ) {
596+ continue ;
597+ }
598+ try {
599+ return JWK ::parseKey ($ entry , Algorithm::deriveJoseAlgFromJwk ($ entry ));
600+ } catch (Throwable $ e ) {
601+ $ this ->logger ->warning ('failed to parse remote JWK ' , ['exception ' => $ e , 'kid ' => $ keyId ]);
602+ return null ;
594603 }
595604 }
596605 return null ;
597606 }
607+
608+ /**
609+ * @return array<string, string>
610+ */
611+ private static function buildEd25519JwkArray (string $ rawPublicKey , string $ kid ): array {
612+ return [
613+ 'kty ' => 'OKP ' ,
614+ 'crv ' => 'Ed25519 ' ,
615+ 'kid ' => $ kid ,
616+ 'alg ' => 'EdDSA ' ,
617+ 'use ' => 'sig ' ,
618+ 'x ' => JWT ::urlsafeB64Encode ($ rawPublicKey ),
619+ ];
620+ }
598621}
0 commit comments