diff --git a/apps/settings/lib/SetupChecks/PhpModules.php b/apps/settings/lib/SetupChecks/PhpModules.php
index d4c3d2c5c2a..4ee4d9165cc 100644
--- a/apps/settings/lib/SetupChecks/PhpModules.php
+++ b/apps/settings/lib/SetupChecks/PhpModules.php
@@ -24,7 +24,6 @@ class PhpModules implements ISetupCheck {
'openssl',
'posix',
'session',
- 'sodium',
'xml',
'xmlreader',
'xmlwriter',
@@ -36,6 +35,7 @@ class PhpModules implements ISetupCheck {
'exif',
'gmp',
'intl',
+ 'sodium',
'sysvsem',
];
@@ -58,6 +58,7 @@ class PhpModules implements ISetupCheck {
protected function getRecommendedModuleDescription(string $module): string {
return match($module) {
'intl' => $this->l10n->t('increases language translation performance and fixes sorting of non-ASCII characters'),
+ 'sodium' => $this->l10n->t('for Argon2 for password hashing and Ed25519 signature verification for RFC 9421 http message signatures'),
'gmp' => $this->l10n->t('required for SFTP storage and recommended for WebAuthn performance'),
'exif' => $this->l10n->t('for picture rotation in server and metadata extraction in the Photos app'),
default => '',
diff --git a/composer.json b/composer.json
index 849c052ab1f..03881a15fc0 100644
--- a/composer.json
+++ b/composer.json
@@ -41,13 +41,15 @@
"ext-posix": "*",
"ext-session": "*",
"ext-simplexml": "*",
- "ext-sodium": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*"
},
+ "suggest": {
+ "ext-sodium": "Argon2 password hashing and Ed25519 signature verification for RFC 9421 HTTP message signatures."
+ },
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4"
},
diff --git a/core/Command/OCM/ActivateKey.php b/core/Command/OCM/ActivateKey.php
index 9efc7110a24..582763880a7 100644
--- a/core/Command/OCM/ActivateKey.php
+++ b/core/Command/OCM/ActivateKey.php
@@ -24,13 +24,13 @@ class ActivateKey extends Base {
protected function configure(): void {
$this
->setName('ocm:keys:activate')
- ->setDescription('promote the staged Ed25519 key to active; the previous active key moves to retiring');
+ ->setDescription('promote the staged JWKS key to active; the previous active key moves to retiring');
}
#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int {
try {
- $this->signatoryManager->activateStagedEd25519Key();
+ $this->signatoryManager->activateStagedJwksKey();
} catch (\RuntimeException $e) {
$output->writeln('' . $e->getMessage() . '');
return self::FAILURE;
diff --git a/core/Command/OCM/ListKeys.php b/core/Command/OCM/ListKeys.php
index 221beca5580..b4eb4715fe9 100644
--- a/core/Command/OCM/ListKeys.php
+++ b/core/Command/OCM/ListKeys.php
@@ -25,13 +25,13 @@ class ListKeys extends Base {
protected function configure(): void {
$this
->setName('ocm:keys:list')
- ->setDescription('list Ed25519 keys used by OCM RFC 9421 HTTP Message Signatures');
+ ->setDescription('list JWKS-published signing keys');
parent::configure();
}
#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int {
- $keys = $this->signatoryManager->listEd25519Keys();
+ $keys = $this->signatoryManager->listJwksKeys();
$format = $input->getOption('output');
if ($format === self::OUTPUT_FORMAT_JSON || $format === self::OUTPUT_FORMAT_JSON_PRETTY) {
$output->writeln(json_encode($keys, $format === self::OUTPUT_FORMAT_JSON_PRETTY ? JSON_PRETTY_PRINT : 0));
@@ -39,7 +39,7 @@ class ListKeys extends Base {
}
if ($keys === []) {
- $output->writeln('No Ed25519 keys yet; one will be generated on first OCM request.');
+ $output->writeln('No JWKS keys yet; one will be generated on first OCM request.');
return self::SUCCESS;
}
diff --git a/core/Command/OCM/RetireKey.php b/core/Command/OCM/RetireKey.php
index 8a3c23d29c1..6c26014e5a1 100644
--- a/core/Command/OCM/RetireKey.php
+++ b/core/Command/OCM/RetireKey.php
@@ -24,13 +24,13 @@ class RetireKey extends Base {
protected function configure(): void {
$this
->setName('ocm:keys:retire')
- ->setDescription('delete the retiring Ed25519 key; signatures that referenced its kid can no longer be verified');
+ ->setDescription('delete the retiring JWKS key; signatures that referenced its kid can no longer be verified');
}
#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int {
try {
- $this->signatoryManager->retireEd25519Key();
+ $this->signatoryManager->retireJwksKey();
} catch (\RuntimeException $e) {
$output->writeln('' . $e->getMessage() . '');
return self::FAILURE;
diff --git a/core/Command/OCM/StageKey.php b/core/Command/OCM/StageKey.php
index f79d09c876a..69f2167ba43 100644
--- a/core/Command/OCM/StageKey.php
+++ b/core/Command/OCM/StageKey.php
@@ -24,18 +24,18 @@ class StageKey extends Base {
protected function configure(): void {
$this
->setName('ocm:keys:stage')
- ->setDescription('generate a new Ed25519 key and advertise it via JWKS without using it for signing yet');
+ ->setDescription('generate a new JWKS key and advertise it via JWKS without using it for signing yet');
}
#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int {
try {
- $signatory = $this->signatoryManager->stageEd25519Key();
+ $signatory = $this->signatoryManager->stageJwksKey();
} catch (\RuntimeException $e) {
$output->writeln('' . $e->getMessage() . '');
return self::FAILURE;
}
- $output->writeln('Staged new Ed25519 key: ' . $signatory->getKeyId() . '');
+ $output->writeln('Staged new JWKS key: ' . $signatory->getKeyId() . '');
$output->writeln('Wait for federated peers to refresh their JWKS cache before activating.');
return self::SUCCESS;
}
diff --git a/lib/private/OCM/OCMJwksHandler.php b/lib/private/OCM/OCMJwksHandler.php
index 281c3eaab2d..0013b38b1b4 100644
--- a/lib/private/OCM/OCMJwksHandler.php
+++ b/lib/private/OCM/OCMJwksHandler.php
@@ -18,7 +18,7 @@ use OCP\IAppConfig;
use Psr\Log\LoggerInterface;
use Throwable;
-/** Serves `/.well-known/jwks.json` (RFC 7517) for the RFC 9421 keys. */
+/** Serves `/.well-known/jwks.json` (RFC 7517) with the OCM signing keys. */
class OCMJwksHandler implements IHandler {
public function __construct(
private readonly IAppConfig $appConfig,
@@ -36,11 +36,11 @@ class OCMJwksHandler implements IHandler {
$keys = [];
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
try {
- foreach ($this->signatoryManager->getLocalEd25519Jwks() as $jwk) {
+ foreach ($this->signatoryManager->getLocalJwks() as $jwk) {
$keys[] = $jwk;
}
} catch (Throwable $e) {
- $this->logger->warning('failed to build local Ed25519 JWKs', ['exception' => $e]);
+ $this->logger->warning('failed to build local JWKs', ['exception' => $e]);
}
}
diff --git a/lib/private/OCM/OCMSignatoryManager.php b/lib/private/OCM/OCMSignatoryManager.php
index d60dc845e4a..1bde75e552b 100644
--- a/lib/private/OCM/OCMSignatoryManager.php
+++ b/lib/private/OCM/OCMSignatoryManager.php
@@ -50,18 +50,18 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
public const APPCONFIG_SIGN_ENFORCED = 'ocm_signed_request_enforced';
private const APPKEY_CAVAGE = 'ocm_external';
private const KEYID_FRAGMENT_CAVAGE = 'signature';
- private const KEYID_FRAGMENT_ED25519 = 'ed25519';
- /** Ed25519 keypairs live in numbered pool appkeys; slots point to them by id. */
- private const APPKEY_ED25519_POOL_PREFIX = 'ocm_ed25519_pool_';
- private const APPCONFIG_ED25519_POOL_COUNTER = 'ocm_ed25519_pool_counter';
- private const APPCONFIG_ED25519_POOL_KID_PREFIX = 'ocm_ed25519_pool_kid_';
+ private const KEYID_FRAGMENT_JWKS = 'ecdsa-p256-sha256';
+ /** JWKS-published keypairs live in numbered pool appkeys; slots point to them by id. */
+ private const APPKEY_JWKS_POOL_PREFIX = 'ocm_jwks_pool_';
+ private const APPCONFIG_JWKS_POOL_COUNTER = 'ocm_jwks_pool_counter';
+ private const APPCONFIG_JWKS_POOL_KID_PREFIX = 'ocm_jwks_pool_kid_';
/** Stable kid identity portion, reused across rotations so kids stay on one hostname. */
- private const APPCONFIG_ED25519_KID_BASE = 'ocm_ed25519_kid_base';
+ private const APPCONFIG_JWKS_KID_BASE = 'ocm_jwks_kid_base';
public const SLOT_ACTIVE = 'active';
public const SLOT_PENDING = 'pending';
public const SLOT_RETIRING = 'retiring';
/** All slots in advertise order. */
- public const ED25519_SLOTS = [self::SLOT_ACTIVE, self::SLOT_PENDING, self::SLOT_RETIRING];
+ public const JWKS_SLOTS = [self::SLOT_ACTIVE, self::SLOT_PENDING, self::SLOT_RETIRING];
/** Remote JWKS cache TTL (seconds). */
private const JWKS_CACHE_TTL = 3600;
@@ -142,11 +142,11 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
}
- /** Active Ed25519 signing key, lazily provisioned. */
- public function getLocalEd25519Signatory(): ?Signatory {
+ /** Active JWKS-published signing key (ECDSA P-256), lazily provisioned. */
+ public function getLocalJwksSignatory(): ?Signatory {
$poolId = $this->getSlotPool(self::SLOT_ACTIVE);
if ($poolId === null) {
- $poolId = $this->generatePool($this->nextEd25519PoolKid());
+ $poolId = $this->generatePool($this->nextPoolKid());
$this->setSlotPool(self::SLOT_ACTIVE, $poolId);
}
return $this->signatoryFromPool($poolId);
@@ -158,61 +158,61 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
*
* @return list>
*/
- public function getLocalEd25519Jwks(): array {
+ public function getLocalJwks(): array {
if ($this->getSlotPool(self::SLOT_ACTIVE) === null) {
- $this->getLocalEd25519Signatory();
+ $this->getLocalJwksSignatory();
}
$jwks = [];
- foreach (self::ED25519_SLOTS as $slot) {
+ foreach (self::JWKS_SLOTS as $slot) {
$poolId = $this->getSlotPool($slot);
if ($poolId === null) {
continue;
}
$signatory = $this->signatoryFromPool($poolId);
if ($signatory !== null) {
- $jwks[] = self::buildEd25519JwkArray($signatory->getPublicKey(), $signatory->getKeyId());
+ $jwks[] = self::buildEcdsaP256JwkArray($signatory->getPublicKey(), $signatory->getKeyId());
}
}
return $jwks;
}
/**
- * Generate a pending Ed25519 keypair (advertised in JWKS, not yet used
- * for outbound signing).
+ * Generate a pending keypair (advertised in JWKS, not yet used for
+ * outbound signing).
*
* @throws \RuntimeException if pending is already populated
*/
- public function stageEd25519Key(): Signatory {
+ public function stageJwksKey(): Signatory {
if ($this->getSlotPool(self::SLOT_PENDING) !== null) {
- throw new \RuntimeException('a pending Ed25519 key already exists; activate or retire it first');
+ throw new \RuntimeException('a pending JWKS key already exists; activate or retire it first');
}
// Need an active key first; staging a next from nothing makes no sense.
if ($this->getSlotPool(self::SLOT_ACTIVE) === null) {
- $this->getLocalEd25519Signatory();
+ $this->getLocalJwksSignatory();
}
- $poolId = $this->generatePool($this->nextEd25519PoolKid());
+ $poolId = $this->generatePool($this->nextPoolKid());
$this->setSlotPool(self::SLOT_PENDING, $poolId);
$signatory = $this->signatoryFromPool($poolId);
if ($signatory === null) {
- throw new \RuntimeException('failed to materialise newly staged Ed25519 key');
+ throw new \RuntimeException('failed to materialise newly staged JWKS key');
}
return $signatory;
}
/**
* pending -> active, previous active -> retiring. The retiring slot
- * stays in JWKS until {@see retireEd25519Key} is run.
+ * stays in JWKS until {@see retireJwksKey} is run.
*
* @throws \RuntimeException if no pending key is staged, or retiring is occupied
*/
- public function activateStagedEd25519Key(): void {
+ public function activateStagedJwksKey(): void {
$pending = $this->getSlotPool(self::SLOT_PENDING);
if ($pending === null) {
- throw new \RuntimeException('no pending Ed25519 key to activate; run `ocm:keys:stage` first');
+ throw new \RuntimeException('no pending JWKS key to activate; run `ocm:keys:stage` first');
}
if ($this->getSlotPool(self::SLOT_RETIRING) !== null) {
- throw new \RuntimeException('a retiring Ed25519 key still exists; retire it before activating a new one');
+ throw new \RuntimeException('a retiring JWKS key still exists; retire it before activating a new one');
}
$active = $this->getSlotPool(self::SLOT_ACTIVE);
@@ -229,13 +229,13 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
*
* @throws \RuntimeException if retiring is empty
*/
- public function retireEd25519Key(): void {
+ public function retireJwksKey(): void {
$poolId = $this->getSlotPool(self::SLOT_RETIRING);
if ($poolId === null) {
- throw new \RuntimeException('no retiring Ed25519 key to remove');
+ throw new \RuntimeException('no retiring JWKS key to remove');
}
- $this->identityProofManager->deleteAppKey('core', self::APPKEY_ED25519_POOL_PREFIX . $poolId);
- $this->appConfig->deleteKey('core', self::APPCONFIG_ED25519_POOL_KID_PREFIX . $poolId);
+ $this->identityProofManager->deleteAppKey('core', self::APPKEY_JWKS_POOL_PREFIX . $poolId);
+ $this->appConfig->deleteKey('core', self::APPCONFIG_JWKS_POOL_KID_PREFIX . $poolId);
$this->clearSlot(self::SLOT_RETIRING);
}
@@ -244,25 +244,25 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
*
* @return list
*/
- public function listEd25519Keys(): array {
+ public function listJwksKeys(): array {
$bySlot = [];
- foreach (self::ED25519_SLOTS as $slot) {
+ foreach (self::JWKS_SLOTS as $slot) {
$id = $this->getSlotPool($slot);
if ($id !== null) {
$bySlot[$id] = $slot;
}
}
- $max = $this->appConfig->getValueInt('core', self::APPCONFIG_ED25519_POOL_COUNTER, 0);
+ $max = $this->appConfig->getValueInt('core', self::APPCONFIG_JWKS_POOL_COUNTER, 0);
$entries = [];
for ($id = 1; $id <= $max; $id++) {
- if (!$this->identityProofManager->hasAppKey('core', self::APPKEY_ED25519_POOL_PREFIX . $id)) {
+ if (!$this->identityProofManager->hasAppKey('core', self::APPKEY_JWKS_POOL_PREFIX . $id)) {
continue;
}
$entries[] = [
'poolId' => $id,
'kid' => $this->canonicalKid(
- $this->appConfig->getValueString('core', self::APPCONFIG_ED25519_POOL_KID_PREFIX . $id, ''),
+ $this->appConfig->getValueString('core', self::APPCONFIG_JWKS_POOL_KID_PREFIX . $id, ''),
),
'slot' => $bySlot[$id] ?? null,
];
@@ -275,11 +275,11 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
* {@see Signatory::setKeyId} so admin output and wire form agree.
*/
private function generatePool(string $kid): int {
- $poolId = $this->appConfig->getValueInt('core', self::APPCONFIG_ED25519_POOL_COUNTER, 0) + 1;
- $this->appConfig->setValueInt('core', self::APPCONFIG_ED25519_POOL_COUNTER, $poolId);
+ $poolId = $this->appConfig->getValueInt('core', self::APPCONFIG_JWKS_POOL_COUNTER, 0) + 1;
+ $this->appConfig->setValueInt('core', self::APPCONFIG_JWKS_POOL_COUNTER, $poolId);
- $this->identityProofManager->generateEd25519AppKey('core', self::APPKEY_ED25519_POOL_PREFIX . $poolId);
- $this->appConfig->setValueString('core', self::APPCONFIG_ED25519_POOL_KID_PREFIX . $poolId, $this->canonicalKid($kid));
+ $this->identityProofManager->generateEcdsaP256AppKey('core', self::APPKEY_JWKS_POOL_PREFIX . $poolId);
+ $this->appConfig->setValueString('core', self::APPCONFIG_JWKS_POOL_KID_PREFIX . $poolId, $this->canonicalKid($kid));
return $poolId;
}
@@ -296,22 +296,22 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
*
* @throws \RuntimeException if no instance identity can be derived
*/
- private function nextEd25519PoolKid(): string {
- $base = $this->resolveEd25519KidBase();
- $next = $this->appConfig->getValueInt('core', self::APPCONFIG_ED25519_POOL_COUNTER, 0) + 1;
+ private function nextPoolKid(): string {
+ $base = $this->resolveKidBase();
+ $next = $this->appConfig->getValueInt('core', self::APPCONFIG_JWKS_POOL_COUNTER, 0) + 1;
return $base . '-' . $next;
}
/**
* Stable identity portion (before the `-N` suffix). Resolution order:
- * stored APPCONFIG_ED25519_KID_BASE > active pool's kid sans suffix >
+ * stored APPCONFIG_JWKS_KID_BASE > active pool's kid sans suffix >
* fresh from {@see buildLocalKeyId}. Persisted so CLI rotations stay
* on one hostname.
*
* @throws \RuntimeException if no instance identity can be derived
*/
- private function resolveEd25519KidBase(): string {
- $base = $this->appConfig->getValueString('core', self::APPCONFIG_ED25519_KID_BASE, '');
+ private function resolveKidBase(): string {
+ $base = $this->appConfig->getValueString('core', self::APPCONFIG_JWKS_KID_BASE, '');
if ($base !== '') {
return $base;
}
@@ -319,7 +319,7 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
$activePool = $this->getSlotPool(self::SLOT_ACTIVE);
if ($activePool !== null) {
$kid = $this->canonicalKid(
- $this->appConfig->getValueString('core', self::APPCONFIG_ED25519_POOL_KID_PREFIX . $activePool, ''),
+ $this->appConfig->getValueString('core', self::APPCONFIG_JWKS_POOL_KID_PREFIX . $activePool, ''),
);
$pos = strrpos($kid, '-');
if ($pos !== false) {
@@ -329,18 +329,18 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
if ($base === '') {
try {
- $base = $this->canonicalKid($this->buildLocalKeyId(self::KEYID_FRAGMENT_ED25519));
+ $base = $this->canonicalKid($this->buildLocalKeyId(self::KEYID_FRAGMENT_JWKS));
} catch (IdentityNotFoundException $e) {
- throw new \RuntimeException('cannot derive instance identity for Ed25519 kid', 0, $e);
+ throw new \RuntimeException('cannot derive instance identity for JWKS kid', 0, $e);
}
}
- $this->appConfig->setValueString('core', self::APPCONFIG_ED25519_KID_BASE, $base);
+ $this->appConfig->setValueString('core', self::APPCONFIG_JWKS_KID_BASE, $base);
return $base;
}
private function getSlotPool(string $slot): ?int {
- $key = 'ocm_ed25519_slot_' . $slot;
+ $key = 'ocm_jwks_slot_' . $slot;
if (!$this->appConfig->hasKey('core', $key)) {
return null;
}
@@ -349,20 +349,20 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
}
private function setSlotPool(string $slot, int $poolId): void {
- $this->appConfig->setValueInt('core', 'ocm_ed25519_slot_' . $slot, $poolId);
+ $this->appConfig->setValueInt('core', 'ocm_jwks_slot_' . $slot, $poolId);
}
private function clearSlot(string $slot): void {
- $this->appConfig->deleteKey('core', 'ocm_ed25519_slot_' . $slot);
+ $this->appConfig->deleteKey('core', 'ocm_jwks_slot_' . $slot);
}
/** Returns null if the underlying appkey was manually deleted. */
private function signatoryFromPool(int $poolId): ?Signatory {
- $appKey = self::APPKEY_ED25519_POOL_PREFIX . $poolId;
+ $appKey = self::APPKEY_JWKS_POOL_PREFIX . $poolId;
if (!$this->identityProofManager->hasAppKey('core', $appKey)) {
return null;
}
- $kid = $this->appConfig->getValueString('core', self::APPCONFIG_ED25519_POOL_KID_PREFIX . $poolId, '');
+ $kid = $this->appConfig->getValueString('core', self::APPCONFIG_JWKS_POOL_KID_PREFIX . $poolId, '');
if ($kid === '') {
return null;
}
@@ -375,7 +375,7 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
}
/**
- * @param string $fragment URL fragment (e.g. 'signature', 'ed25519')
+ * @param string $fragment URL fragment (e.g. 'signature' for cavage, 'ecdsa-p256-sha256' for the JWKS-published key)
* @return string
* @throws IdentityNotFoundException
*/
@@ -531,16 +531,27 @@ class OCMSignatoryManager implements IJwkResolvingSignatoryManager {
}
/**
+ * Build an EC P-256 JWK (RFC 7518 §6.2) from a PEM public key. The raw x/y
+ * coordinates from openssl are zero-padded to 32 bytes per RFC 7518 §6.2.1.2.
+ *
* @return array
*/
- private static function buildEd25519JwkArray(string $rawPublicKey, string $kid): array {
+ private static function buildEcdsaP256JwkArray(string $publicKeyPem, string $kid): array {
+ $details = openssl_pkey_get_details(openssl_pkey_get_public($publicKeyPem) ?: throw new \RuntimeException('invalid EC public key'));
+ if ($details === false || !isset($details['ec']['x'], $details['ec']['y'])) {
+ throw new \RuntimeException('invalid EC public key');
+ }
+ $x = str_pad($details['ec']['x'], 32, "\x00", STR_PAD_LEFT);
+ $y = str_pad($details['ec']['y'], 32, "\x00", STR_PAD_LEFT);
+
return [
- 'kty' => 'OKP',
- 'crv' => 'Ed25519',
+ 'kty' => 'EC',
+ 'crv' => 'P-256',
'kid' => $kid,
- 'alg' => 'EdDSA',
+ 'alg' => 'ES256',
'use' => 'sig',
- 'x' => JWT::urlsafeB64Encode($rawPublicKey),
+ 'x' => JWT::urlsafeB64Encode($x),
+ 'y' => JWT::urlsafeB64Encode($y),
];
}
}
diff --git a/lib/private/OCM/Rfc9421SignatoryManager.php b/lib/private/OCM/Rfc9421SignatoryManager.php
index f0756d9ca6e..f03c01eebeb 100644
--- a/lib/private/OCM/Rfc9421SignatoryManager.php
+++ b/lib/private/OCM/Rfc9421SignatoryManager.php
@@ -16,8 +16,8 @@ use OCP\Security\Signature\Model\Signatory;
/**
* Per-call wrapper around {@see OCMSignatoryManager} that swaps in the
- * Ed25519 signatory and sets `rfc9421.format`. Wrapping (vs mutating) keeps
- * the underlying DI-managed instance stateless across requests.
+ * JWKS-published signatory and sets `rfc9421.format`. Wrapping (vs mutating)
+ * keeps the underlying DI-managed instance stateless across requests.
*/
final class Rfc9421SignatoryManager implements IJwkResolvingSignatoryManager {
public function __construct(
@@ -37,9 +37,9 @@ final class Rfc9421SignatoryManager implements IJwkResolvingSignatoryManager {
#[\Override]
public function getLocalSignatory(): Signatory {
- $signatory = $this->delegate->getLocalEd25519Signatory();
+ $signatory = $this->delegate->getLocalJwksSignatory();
if ($signatory === null) {
- throw new IdentityNotFoundException('no Ed25519 signatory available');
+ throw new IdentityNotFoundException('no JWKS-published signatory available');
}
return $signatory;
}
diff --git a/lib/private/Security/IdentityProof/Manager.php b/lib/private/Security/IdentityProof/Manager.php
index d6ebe3813b2..298c1d459b3 100644
--- a/lib/private/Security/IdentityProof/Manager.php
+++ b/lib/private/Security/IdentityProof/Manager.php
@@ -179,14 +179,31 @@ class Manager {
}
/**
- * Generate an Ed25519 keypair via libsodium. Returns raw 32-byte public
- * + 64-byte secret (sodium seed||publickey), no PEM. Overwrites if
- * already present.
+ * Generate an ECDSA P-256 (prime256v1, SECG/JOSE ES256 curve) keypair via
+ * openssl. Returns PEM private + PEM public. Overwrites if already
+ * present. Private key is encrypted on disk.
+ *
+ * @throws \RuntimeException
*/
- public function generateEd25519AppKey(string $app, string $name): Key {
- $keyPair = sodium_crypto_sign_keypair();
- $publicKey = sodium_crypto_sign_publickey($keyPair);
- $privateKey = sodium_crypto_sign_secretkey($keyPair);
+ public function generateEcdsaP256AppKey(string $app, string $name): Key {
+ $res = openssl_pkey_new([
+ 'private_key_type' => OPENSSL_KEYTYPE_EC,
+ 'curve_name' => 'prime256v1',
+ ]);
+ if ($res === false) {
+ $this->logOpensslError();
+ throw new \RuntimeException('OpenSSL reported a problem');
+ }
+ if (openssl_pkey_export($res, $privateKey) === false) {
+ $this->logOpensslError();
+ throw new \RuntimeException('OpenSSL reported a problem');
+ }
+ $details = openssl_pkey_get_details($res);
+ if ($details === false || !isset($details['key'])) {
+ $this->logOpensslError();
+ throw new \RuntimeException('OpenSSL reported a problem');
+ }
+ $publicKey = $details['key'];
$id = $this->generateAppKeyId($app, $name);
try {
diff --git a/lib/private/Security/Signature/Model/Rfc9421OutgoingSignedRequest.php b/lib/private/Security/Signature/Model/Rfc9421OutgoingSignedRequest.php
index 3a44776ef4a..d2fa2a4ae86 100644
--- a/lib/private/Security/Signature/Model/Rfc9421OutgoingSignedRequest.php
+++ b/lib/private/Security/Signature/Model/Rfc9421OutgoingSignedRequest.php
@@ -23,8 +23,9 @@ use OCP\Security\Signature\ISignatoryManager;
/**
* RFC 9421 implementation of {@see IOutgoingSignedRequest}, sibling to the
- * draft-cavage {@see OutgoingSignedRequest}. Default Ed25519 with the `alg`
- * parameter omitted (RFC 9421 §3.3.7); verifier resolves it from the JWK.
+ * draft-cavage {@see OutgoingSignedRequest}. Default ECDSA P-256 (`ES256`)
+ * with the `alg` parameter omitted (RFC 9421 §3.3.7); verifier resolves it
+ * from the JWK.
*
* Options from {@see ISignatoryManager::getOptions()}: `rfc9421.signingAlgorithm`,
* `rfc9421.coveredComponents`, `rfc9421.contentDigestAlgorithm`,
@@ -60,7 +61,7 @@ class Rfc9421OutgoingSignedRequest extends SignedRequest implements
->setSignatory($signatoryManager->getLocalSignatory())
->setDigestAlgorithm($options['digestAlgorithm'] ?? DigestAlgorithm::SHA256);
- $this->signingAlgorithm = (string)($options['rfc9421.signingAlgorithm'] ?? 'ed25519');
+ $this->signingAlgorithm = (string)($options['rfc9421.signingAlgorithm'] ?? 'ecdsa-p256-sha256');
$contentDigestAlgorithm = (string)($options['rfc9421.contentDigestAlgorithm'] ?? ContentDigest::ALGO_SHA256);
/** @var list $components */
$components = $options['rfc9421.coveredComponents'] ?? self::DEFAULT_COMPONENTS;
@@ -138,7 +139,7 @@ class Rfc9421OutgoingSignedRequest extends SignedRequest implements
return $this->algorithm;
}
- /** RFC 9421 alg name (e.g. `ed25519`). Distinct from cavage's {@see getAlgorithm()}. */
+ /** RFC 9421 alg name (e.g. `ecdsa-p256-sha256`). Distinct from cavage's {@see getAlgorithm()}. */
public function getSigningAlgorithm(): string {
return $this->signingAlgorithm;
}
diff --git a/lib/private/Security/Signature/Rfc9421/Algorithm.php b/lib/private/Security/Signature/Rfc9421/Algorithm.php
index 40bec3cf153..4fd7569a1ff 100644
--- a/lib/private/Security/Signature/Rfc9421/Algorithm.php
+++ b/lib/private/Security/Signature/Rfc9421/Algorithm.php
@@ -18,11 +18,16 @@ use Throwable;
/**
* RFC 9421 §3.3 sign/verify primitives.
*
- * Asymmetric algorithms only: RSA-PKCS1-v1_5 (SHA-256/384/512), ECDSA P-256
- * SHA-256, ECDSA P-384 SHA-384, Ed25519. JOSE aliases (RFC 7518 / RFC 8037)
+ * Sign supports asymmetric algorithms reachable via ext-openssl: RSA-PKCS1-v1_5
+ * (SHA-256/384/512) and ECDSA P-256 / P-384. JOSE aliases (RFC 7518 / RFC 8037)
* accepted per RFC 9421 §3.3.7. RSA-PSS is rejected: OPENSSL_PKCS1_PSS_PADDING
* needs PHP 8.5 and we still support 8.2-8.4.
*
+ * Verify additionally accepts Ed25519 when ext-sodium is loaded; without sodium
+ * an Ed25519 signature throws {@see SignatureException}. Sodium is used directly
+ * because firebase/php-jwt's `validateEdDSAKey` base64url-decodes the key
+ * material, which mangles the raw sodium bytes.
+ *
* Sign delegates to {@see JWT::sign}. Verify takes a {@see Key} parsed by
* firebase/php-jwt (which has already validated the JWK's kty/crv/alg
* consistency) and only enforces the cross-source agreement between the JWK
@@ -39,12 +44,8 @@ final class Algorithm {
];
/**
- * For Ed25519 $privateKey is the raw 64-byte sodium secret key; otherwise
- * a PEM private key. Returns raw signature bytes (R||S for ECDSA).
- *
- * Ed25519 calls sodium directly: JWT::sign runs the key through
- * `validateEdDSAKey` which base64url-decodes it first, which mangles raw
- * sodium bytes.
+ * $privateKey is a PEM private key. Returns raw signature bytes (R||S for
+ * ECDSA). Ed25519 is verify-only and is rejected here.
*
* @throws SignatureException
*/
@@ -52,10 +53,7 @@ final class Algorithm {
$normalized = self::normalize($algorithm);
if ($normalized === 'ed25519') {
- if (strlen($privateKey) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) {
- throw new SignatureException('Ed25519 secret key must be ' . SODIUM_CRYPTO_SIGN_SECRETKEYBYTES . ' bytes');
- }
- return sodium_crypto_sign_detached($signatureBase, $privateKey);
+ throw new SignatureException('Ed25519 signing is not supported; use ECDSA P-256 or RSA');
}
try {
@@ -85,6 +83,9 @@ final class Algorithm {
$material = $key->getKeyMaterial();
if ($resolved === 'ed25519') {
+ if (!function_exists('sodium_crypto_sign_verify_detached')) {
+ throw new SignatureException('verifying Ed25519 signatures requires ext-sodium');
+ }
if (strlen($signature) !== SODIUM_CRYPTO_SIGN_BYTES) {
return false;
}
@@ -154,7 +155,6 @@ final class Algorithm {
private static function nativeToJose(string $native): string {
return match ($native) {
- 'ed25519' => 'EdDSA',
'ecdsa-p256-sha256' => 'ES256',
'ecdsa-p384-sha384' => 'ES384',
'rsa-v1_5-sha256' => 'RS256',
diff --git a/tests/lib/OCM/OCMJwksHandlerTest.php b/tests/lib/OCM/OCMJwksHandlerTest.php
index 7040b19f675..f7270298dee 100644
--- a/tests/lib/OCM/OCMJwksHandlerTest.php
+++ b/tests/lib/OCM/OCMJwksHandlerTest.php
@@ -54,23 +54,24 @@ class OCMJwksHandlerTest extends TestCase {
$this->appConfig->method('getValueBool')
->with('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, false, true)
->willReturn(true);
- $this->signatoryManager->expects($this->never())->method('getLocalEd25519Jwks');
+ $this->signatoryManager->expects($this->never())->method('getLocalJwks');
$body = $this->jsonBody($this->handler->handle('jwks.json', $this->context, null));
$this->assertSame(['keys' => []], $body);
}
- public function testPublishesEd25519JwksWhenAvailable(): void {
+ public function testPublishesJwksWhenAvailable(): void {
$this->appConfig->method('getValueBool')->willReturn(false);
$jwk = [
- 'kty' => 'OKP',
- 'crv' => 'Ed25519',
- 'kid' => 'https://example.org/ocm#ed25519',
- 'alg' => 'EdDSA',
+ 'kty' => 'EC',
+ 'crv' => 'P-256',
+ 'kid' => 'https://example.org/ocm#ecdsa-p256-sha256',
+ 'alg' => 'ES256',
'use' => 'sig',
'x' => 'AAAA',
+ 'y' => 'BBBB',
];
- $this->signatoryManager->method('getLocalEd25519Jwks')->willReturn([$jwk]);
+ $this->signatoryManager->method('getLocalJwks')->willReturn([$jwk]);
$body = $this->jsonBody($this->handler->handle('jwks.json', $this->context, null));
$this->assertSame(['keys' => [$jwk]], $body);
@@ -79,12 +80,12 @@ class OCMJwksHandlerTest extends TestCase {
public function testPublishesAllSlotsAdvertisedDuringRotation(): void {
$this->appConfig->method('getValueBool')->willReturn(false);
$active = [
- 'kty' => 'OKP', 'crv' => 'Ed25519', 'kid' => 'kid-1', 'alg' => 'EdDSA', 'use' => 'sig', 'x' => 'AAAA',
+ 'kty' => 'EC', 'crv' => 'P-256', 'kid' => 'kid-1', 'alg' => 'ES256', 'use' => 'sig', 'x' => 'AAAA', 'y' => 'BBBB',
];
$pending = [
- 'kty' => 'OKP', 'crv' => 'Ed25519', 'kid' => 'kid-2', 'alg' => 'EdDSA', 'use' => 'sig', 'x' => 'BBBB',
+ 'kty' => 'EC', 'crv' => 'P-256', 'kid' => 'kid-2', 'alg' => 'ES256', 'use' => 'sig', 'x' => 'CCCC', 'y' => 'DDDD',
];
- $this->signatoryManager->method('getLocalEd25519Jwks')->willReturn([$active, $pending]);
+ $this->signatoryManager->method('getLocalJwks')->willReturn([$active, $pending]);
$body = $this->jsonBody($this->handler->handle('jwks.json', $this->context, null));
$this->assertSame(['keys' => [$active, $pending]], $body);
@@ -92,7 +93,7 @@ class OCMJwksHandlerTest extends TestCase {
public function testEmptyKeySetWhenSignatoryUnavailable(): void {
$this->appConfig->method('getValueBool')->willReturn(false);
- $this->signatoryManager->method('getLocalEd25519Jwks')->willReturn([]);
+ $this->signatoryManager->method('getLocalJwks')->willReturn([]);
$body = $this->jsonBody($this->handler->handle('jwks.json', $this->context, null));
$this->assertSame(['keys' => []], $body);
@@ -100,7 +101,7 @@ class OCMJwksHandlerTest extends TestCase {
public function testFailingJwkBuildIsLoggedAndYieldsEmptyKeySet(): void {
$this->appConfig->method('getValueBool')->willReturn(false);
- $this->signatoryManager->method('getLocalEd25519Jwks')
+ $this->signatoryManager->method('getLocalJwks')
->willThrowException(new \RuntimeException('boom'));
$this->logger->expects($this->once())->method('warning');
diff --git a/tests/lib/OCM/OCMSignatoryManagerJwksTest.php b/tests/lib/OCM/OCMSignatoryManagerJwksTest.php
index 7fcc0818e31..b135efb8a8f 100644
--- a/tests/lib/OCM/OCMSignatoryManagerJwksTest.php
+++ b/tests/lib/OCM/OCMSignatoryManagerJwksTest.php
@@ -24,6 +24,10 @@ use Psr\Log\LoggerInterface;
use Test\TestCase;
class OCMSignatoryManagerJwksTest extends TestCase {
+ /** RFC 7517 §A.1 test vector for an EC P-256 public key. */
+ private const TEST_X = 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU';
+ private const TEST_Y = 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0';
+
private IAppConfig&MockObject $appConfig;
private ISignatureManager&MockObject $signatureManager;
private IURLGenerator&MockObject $urlGenerator;
@@ -68,21 +72,19 @@ class OCMSignatoryManagerJwksTest extends TestCase {
$kid = 'sender.example.org#key1';
$jwks = [
'keys' => [
- ['kty' => 'OKP', 'crv' => 'Ed25519', 'kid' => 'other', 'x' => 'AAAA'],
- ['kty' => 'OKP', 'crv' => 'Ed25519', 'kid' => $kid, 'x' => 'BBBB'],
+ $this->ecJwk('other'),
+ $this->ecJwk($kid),
],
];
$this->respondWith($jwks);
$key = $this->signatoryManager->getRemoteKey('sender.example.org', $kid);
$this->assertNotNull($key);
- $this->assertSame('EdDSA', $key->getAlgorithm());
- // Key stores OKP material as plain base64 of the raw bytes.
- $this->assertSame('BBBB', $key->getKeyMaterial());
+ $this->assertSame('ES256', $key->getAlgorithm());
}
public function testGetRemoteKeyReturnsNullWhenKidMissing(): void {
- $this->respondWith(['keys' => [['kty' => 'OKP', 'crv' => 'Ed25519', 'kid' => 'unrelated', 'x' => 'AAAA']]]);
+ $this->respondWith(['keys' => [$this->ecJwk('unrelated')]]);
$this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'other-kid'));
}
@@ -106,8 +108,8 @@ class OCMSignatoryManagerJwksTest extends TestCase {
}
public function testGetRemoteKeyReturnsNullOnUnparseableJwk(): void {
- // JWK with kty=OKP but no crv: parseKey rejects.
- $this->respondWith(['keys' => [['kty' => 'OKP', 'kid' => 'kid', 'x' => 'AAAA']]]);
+ // JWK with kty=EC but no crv: parseKey rejects.
+ $this->respondWith(['keys' => [['kty' => 'EC', 'kid' => 'kid', 'x' => self::TEST_X, 'y' => self::TEST_Y]]]);
$this->logger->expects($this->once())->method('warning');
$this->assertNull($this->signatoryManager->getRemoteKey('sender.example.org', 'kid'));
}
@@ -142,7 +144,7 @@ class OCMSignatoryManagerJwksTest extends TestCase {
public function testJwksCachedAcrossCallsToTheSameOrigin(): void {
$kid = 'sender.example.org#key1';
- $jwks = ['keys' => [['kty' => 'OKP', 'crv' => 'Ed25519', 'kid' => $kid, 'x' => 'AAAA']]];
+ $jwks = ['keys' => [$this->ecJwk($kid)]];
$this->client->expects($this->once())
->method('get')
->willReturn($this->jsonResponse($jwks));
@@ -152,8 +154,8 @@ class OCMSignatoryManagerJwksTest extends TestCase {
}
public function testCacheMissOnNewKidTriggersRefetchOnce(): void {
- $first = ['keys' => [['kty' => 'OKP', 'crv' => 'Ed25519', 'kid' => 'old', 'x' => 'AAAA']]];
- $second = ['keys' => [['kty' => 'OKP', 'crv' => 'Ed25519', 'kid' => 'new', 'x' => 'BBBB']]];
+ $first = ['keys' => [$this->ecJwk('old')]];
+ $second = ['keys' => [$this->ecJwk('new')]];
$this->client->expects($this->exactly(2))
->method('get')
->willReturnOnConsecutiveCalls(
@@ -174,4 +176,17 @@ class OCMSignatoryManagerJwksTest extends TestCase {
$response->method('getBody')->willReturn(json_encode($body, JSON_THROW_ON_ERROR));
return $response;
}
+
+ /** @return array */
+ private function ecJwk(string $kid): array {
+ return [
+ 'kty' => 'EC',
+ 'crv' => 'P-256',
+ 'kid' => $kid,
+ 'alg' => 'ES256',
+ 'use' => 'sig',
+ 'x' => self::TEST_X,
+ 'y' => self::TEST_Y,
+ ];
+ }
}
diff --git a/tests/lib/OCM/OCMSignatoryManagerRotationTest.php b/tests/lib/OCM/OCMSignatoryManagerRotationTest.php
index 9b52d88c61f..24d25f4476a 100644
--- a/tests/lib/OCM/OCMSignatoryManagerRotationTest.php
+++ b/tests/lib/OCM/OCMSignatoryManagerRotationTest.php
@@ -24,7 +24,7 @@ use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
-/** Ed25519 stage / activate / retire lifecycle, with stateful IAppConfig + IdentityProofManager fakes. */
+/** JWKS stage / activate / retire lifecycle, with stateful IAppConfig + IdentityProofManager fakes. */
class OCMSignatoryManagerRotationTest extends TestCase {
private IAppConfig&MockObject $appConfig;
private IdentityProofManager&MockObject $identityProofManager;
@@ -66,118 +66,118 @@ class OCMSignatoryManagerRotationTest extends TestCase {
public function testJwksBootstrapsActiveKeyOnFirstFetch(): void {
// Fresh instance: first JWKS hit must provision the active key.
- $jwks = $this->signatoryManager->getLocalEd25519Jwks();
+ $jwks = $this->signatoryManager->getLocalJwks();
$this->assertCount(1, $jwks);
- $this->assertSame('https://alice.example/ocm#ed25519-1', $jwks[0]['kid']);
+ $this->assertSame('https://alice.example/ocm#ecdsa-p256-sha256-1', $jwks[0]['kid']);
// And the bootstrapped key is the active one for outbound signing.
- $signatory = $this->signatoryManager->getLocalEd25519Signatory();
+ $signatory = $this->signatoryManager->getLocalJwksSignatory();
$this->assertSame($jwks[0]['kid'], $signatory->getKeyId());
}
public function testFirstCallProvisionsActiveKey(): void {
- $signatory = $this->signatoryManager->getLocalEd25519Signatory();
+ $signatory = $this->signatoryManager->getLocalJwksSignatory();
$this->assertNotNull($signatory);
- $this->assertSame('https://alice.example/ocm#ed25519-1', $signatory->getKeyId());
+ $this->assertSame('https://alice.example/ocm#ecdsa-p256-sha256-1', $signatory->getKeyId());
- $jwks = $this->signatoryManager->getLocalEd25519Jwks();
+ $jwks = $this->signatoryManager->getLocalJwks();
$this->assertCount(1, $jwks);
$this->assertSame($signatory->getKeyId(), $jwks[0]['kid']);
- $listed = $this->signatoryManager->listEd25519Keys();
+ $listed = $this->signatoryManager->listJwksKeys();
$this->assertSame([['poolId' => 1, 'kid' => $signatory->getKeyId(), 'slot' => 'active']], $listed);
}
public function testStageDoesNotChangeActiveSignerButPublishesNewJwk(): void {
- $initial = $this->signatoryManager->getLocalEd25519Signatory();
- $staged = $this->signatoryManager->stageEd25519Key();
+ $initial = $this->signatoryManager->getLocalJwksSignatory();
+ $staged = $this->signatoryManager->stageJwksKey();
$this->assertNotSame($initial->getKeyId(), $staged->getKeyId());
// Active signer is unchanged.
- $this->assertSame($initial->getKeyId(), $this->signatoryManager->getLocalEd25519Signatory()->getKeyId());
+ $this->assertSame($initial->getKeyId(), $this->signatoryManager->getLocalJwksSignatory()->getKeyId());
// JWKS now advertises both kids, active first then pending.
- $jwks = $this->signatoryManager->getLocalEd25519Jwks();
+ $jwks = $this->signatoryManager->getLocalJwks();
$this->assertSame([$initial->getKeyId(), $staged->getKeyId()], array_column($jwks, 'kid'));
}
public function testStageRefusesIfPendingAlreadyExists(): void {
- $this->signatoryManager->stageEd25519Key();
+ $this->signatoryManager->stageJwksKey();
$this->expectException(\RuntimeException::class);
- $this->expectExceptionMessageMatches('/pending Ed25519 key already exists/');
- $this->signatoryManager->stageEd25519Key();
+ $this->expectExceptionMessageMatches('/pending JWKS key already exists/');
+ $this->signatoryManager->stageJwksKey();
}
public function testActivatePromotesPendingAndDemotesActive(): void {
- $first = $this->signatoryManager->getLocalEd25519Signatory();
- $staged = $this->signatoryManager->stageEd25519Key();
- $this->signatoryManager->activateStagedEd25519Key();
+ $first = $this->signatoryManager->getLocalJwksSignatory();
+ $staged = $this->signatoryManager->stageJwksKey();
+ $this->signatoryManager->activateStagedJwksKey();
// New signer is the formerly-staged key.
- $this->assertSame($staged->getKeyId(), $this->signatoryManager->getLocalEd25519Signatory()->getKeyId());
+ $this->assertSame($staged->getKeyId(), $this->signatoryManager->getLocalJwksSignatory()->getKeyId());
// JWKS still advertises the former active key as retiring so peers
// verifying in-flight signatures with its kid don't fail.
- $kids = array_column($this->signatoryManager->getLocalEd25519Jwks(), 'kid');
+ $kids = array_column($this->signatoryManager->getLocalJwks(), 'kid');
$this->assertContains($first->getKeyId(), $kids);
$this->assertContains($staged->getKeyId(), $kids);
}
public function testActivateRefusesIfRetiringStillPopulated(): void {
- $this->signatoryManager->getLocalEd25519Signatory();
- $this->signatoryManager->stageEd25519Key();
- $this->signatoryManager->activateStagedEd25519Key();
+ $this->signatoryManager->getLocalJwksSignatory();
+ $this->signatoryManager->stageJwksKey();
+ $this->signatoryManager->activateStagedJwksKey();
// Retiring slot is now populated; staging again is allowed but
// activating must refuse until the admin explicitly retires the old
// key.
- $this->signatoryManager->stageEd25519Key();
+ $this->signatoryManager->stageJwksKey();
$this->expectException(\RuntimeException::class);
- $this->expectExceptionMessageMatches('/retiring Ed25519 key still exists/');
- $this->signatoryManager->activateStagedEd25519Key();
+ $this->expectExceptionMessageMatches('/retiring JWKS key still exists/');
+ $this->signatoryManager->activateStagedJwksKey();
}
public function testActivateRefusesWithoutPendingKey(): void {
- $this->signatoryManager->getLocalEd25519Signatory();
+ $this->signatoryManager->getLocalJwksSignatory();
$this->expectException(\RuntimeException::class);
- $this->expectExceptionMessageMatches('/no pending Ed25519 key/');
- $this->signatoryManager->activateStagedEd25519Key();
+ $this->expectExceptionMessageMatches('/no pending JWKS key/');
+ $this->signatoryManager->activateStagedJwksKey();
}
public function testRetireRemovesRetiringKeyFromJwks(): void {
- $first = $this->signatoryManager->getLocalEd25519Signatory();
- $staged = $this->signatoryManager->stageEd25519Key();
- $this->signatoryManager->activateStagedEd25519Key();
- $this->signatoryManager->retireEd25519Key();
+ $first = $this->signatoryManager->getLocalJwksSignatory();
+ $staged = $this->signatoryManager->stageJwksKey();
+ $this->signatoryManager->activateStagedJwksKey();
+ $this->signatoryManager->retireJwksKey();
- $kids = array_column($this->signatoryManager->getLocalEd25519Jwks(), 'kid');
+ $kids = array_column($this->signatoryManager->getLocalJwks(), 'kid');
$this->assertSame([$staged->getKeyId()], $kids);
- // listEd25519Keys also drops the retired pool.
- $listed = $this->signatoryManager->listEd25519Keys();
+ // listJwksKeys also drops the retired pool.
+ $listed = $this->signatoryManager->listJwksKeys();
$this->assertCount(1, $listed);
$this->assertSame($staged->getKeyId(), $listed[0]['kid']);
$this->assertNotContains($first->getKeyId(), array_column($listed, 'kid'));
}
public function testRetireRefusesWhenNothingToRetire(): void {
- $this->signatoryManager->getLocalEd25519Signatory();
+ $this->signatoryManager->getLocalJwksSignatory();
$this->expectException(\RuntimeException::class);
- $this->expectExceptionMessageMatches('/no retiring Ed25519 key/');
- $this->signatoryManager->retireEd25519Key();
+ $this->expectExceptionMessageMatches('/no retiring JWKS key/');
+ $this->signatoryManager->retireJwksKey();
}
public function testKidStaysStableThroughLifecycle(): void {
- $first = $this->signatoryManager->getLocalEd25519Signatory();
- $staged = $this->signatoryManager->stageEd25519Key();
+ $first = $this->signatoryManager->getLocalJwksSignatory();
+ $staged = $this->signatoryManager->stageJwksKey();
// kid for the staged key must stay the same once it is activated;
// peers that cached it during the stage window must still resolve it.
- $this->signatoryManager->activateStagedEd25519Key();
- $this->assertSame($staged->getKeyId(), $this->signatoryManager->getLocalEd25519Signatory()->getKeyId());
+ $this->signatoryManager->activateStagedJwksKey();
+ $this->assertSame($staged->getKeyId(), $this->signatoryManager->getLocalJwksSignatory()->getKeyId());
- $this->signatoryManager->retireEd25519Key();
- $this->signatoryManager->stageEd25519Key();
+ $this->signatoryManager->retireJwksKey();
+ $this->signatoryManager->stageJwksKey();
// And every newly minted kid must differ from prior ones, no pool
// counter rewinding.
- $kids = array_column($this->signatoryManager->listEd25519Keys(), 'kid');
+ $kids = array_column($this->signatoryManager->listJwksKeys(), 'kid');
$this->assertNotContains($first->getKeyId(), $kids);
$this->assertSame($kids, array_unique($kids));
}
@@ -208,7 +208,7 @@ class OCMSignatoryManagerRotationTest extends TestCase {
);
$this->expectException(\RuntimeException::class);
- $manager->getLocalEd25519Signatory();
+ $manager->getLocalJwksSignatory();
}
private function wireAppConfig(): void {
@@ -245,10 +245,15 @@ class OCMSignatoryManagerRotationTest extends TestCase {
$this->identityProofManager->method('hasAppKey')->willReturnCallback(
fn (string $app, string $name): bool => isset($this->appKeyStore[$app . '/' . $name])
);
- $this->identityProofManager->method('generateEd25519AppKey')->willReturnCallback(
+ $this->identityProofManager->method('generateEcdsaP256AppKey')->willReturnCallback(
function (string $app, string $name): Key {
- $keyPair = sodium_crypto_sign_keypair();
- $key = new Key(sodium_crypto_sign_publickey($keyPair), sodium_crypto_sign_secretkey($keyPair));
+ $res = openssl_pkey_new([
+ 'private_key_type' => OPENSSL_KEYTYPE_EC,
+ 'curve_name' => 'prime256v1',
+ ]);
+ openssl_pkey_export($res, $privatePem);
+ $publicPem = openssl_pkey_get_details($res)['key'];
+ $key = new Key($publicPem, $privatePem);
$this->appKeyStore[$app . '/' . $name] = $key;
return $key;
}
diff --git a/tests/lib/OCM/Rfc9421SignatoryManagerTest.php b/tests/lib/OCM/Rfc9421SignatoryManagerTest.php
index f186986cf81..4bdda737967 100644
--- a/tests/lib/OCM/Rfc9421SignatoryManagerTest.php
+++ b/tests/lib/OCM/Rfc9421SignatoryManagerTest.php
@@ -39,15 +39,15 @@ class Rfc9421SignatoryManagerTest extends TestCase {
$this->assertSame('rsa-sha512', $options['algorithm']);
}
- public function testGetLocalSignatoryReturnsEd25519Key(): void {
+ public function testGetLocalSignatoryReturnsJwksKey(): void {
$signatory = $this->createMock(Signatory::class);
- $this->delegate->method('getLocalEd25519Signatory')->willReturn($signatory);
+ $this->delegate->method('getLocalJwksSignatory')->willReturn($signatory);
$this->assertSame($signatory, $this->wrapper->getLocalSignatory());
}
- public function testGetLocalSignatoryThrowsWhenEd25519Unavailable(): void {
- $this->delegate->method('getLocalEd25519Signatory')->willReturn(null);
+ public function testGetLocalSignatoryThrowsWhenJwksKeyUnavailable(): void {
+ $this->delegate->method('getLocalJwksSignatory')->willReturn(null);
$this->expectException(IdentityNotFoundException::class);
$this->wrapper->getLocalSignatory();
diff --git a/tests/lib/Security/Signature/Model/Rfc9421RoundTripTest.php b/tests/lib/Security/Signature/Model/Rfc9421RoundTripTest.php
index 5f4285f14cc..e7d42460987 100644
--- a/tests/lib/Security/Signature/Model/Rfc9421RoundTripTest.php
+++ b/tests/lib/Security/Signature/Model/Rfc9421RoundTripTest.php
@@ -23,8 +23,8 @@ use OCP\Security\Signature\Model\Signatory;
use Test\TestCase;
class Rfc9421RoundTripTest extends TestCase {
- public function testEd25519RoundTripVerifies(): void {
- [$signatory, $jwk] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ public function testEcdsaP256RoundTripVerifies(): void {
+ [$signatory, $jwk] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManager($signatory);
$body = '{"hello":"world"}';
@@ -43,8 +43,30 @@ class Rfc9421RoundTripTest extends TestCase {
$this->addToAssertionCount(1);
}
+ public function testEd25519VerifyAcceptedWhenSodiumLoaded(): void {
+ $this->skipUnlessSodium();
+ [$signatory, $jwk] = $this->ed25519Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
+ $signatoryManager = $this->makeSignatoryManagerWithSigningAlgorithm($signatory, 'ed25519');
+
+ $body = '{"hello":"world"}';
+ $out = new Rfc9421OutgoingSignedRequest($body, $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares');
+ // Ed25519 sign() throws via Algorithm::sign; produce the signature directly.
+ $rawSig = sodium_crypto_sign_detached($out->getSignatureBaseString(), $signatory->getPrivateKey());
+ $out->setSignature(base64_encode($rawSig));
+ $headers = $out->getHeaders();
+ $paramsLine = '("@method" "@target-uri" "content-digest" "content-length" "date");created=' . time() . ';keyid="' . $signatory->getKeyId() . '"';
+ $headers['Signature-Input'] = 'ocm=' . $paramsLine;
+ $headers['Signature'] = 'ocm=:' . base64_encode($rawSig) . ':';
+
+ $req = $this->mockRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org');
+ $in = new Rfc9421IncomingSignedRequest($body, $req);
+ $in->setKey($jwk);
+ $in->verify();
+ $this->addToAssertionCount(1);
+ }
+
public function testTamperedBodyRejected(): void {
- [$signatory, $jwk] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManager($signatory);
$body = 'original';
@@ -57,7 +79,7 @@ class Rfc9421RoundTripTest extends TestCase {
}
public function testTamperedSignatureRejected(): void {
- [$signatory, $jwk] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ [$signatory, $jwk] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManager($signatory);
$body = 'msg';
@@ -77,7 +99,7 @@ class Rfc9421RoundTripTest extends TestCase {
}
public function testOutgoingUsesOcmLabel(): void {
- [$signatory] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManager($signatory);
$out = new Rfc9421OutgoingSignedRequest('msg', $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares');
@@ -89,7 +111,7 @@ class Rfc9421RoundTripTest extends TestCase {
}
public function testRequestWithoutOcmLabelRejected(): void {
- [$signatory, $jwk] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManager($signatory);
$out = new Rfc9421OutgoingSignedRequest('msg', $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares');
@@ -109,7 +131,7 @@ class Rfc9421RoundTripTest extends TestCase {
// RFC 8941 §4.2 last-wins on duplicate dictionary keys, but OCM
// mandates that duplicate `ocm` entries cause the request to be
// rejected outright. The model layer enforces that.
- [$signatory, $jwk] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManager($signatory);
$out = new Rfc9421OutgoingSignedRequest('msg', $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares');
@@ -125,7 +147,7 @@ class Rfc9421RoundTripTest extends TestCase {
}
public function testForeignSiblingLabelIgnored(): void {
- [$signatory, $jwk] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ [$signatory, $jwk] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManager($signatory);
$out = new Rfc9421OutgoingSignedRequest('msg', $signatoryManager, 'receiver.example.org', 'POST', 'https://receiver.example.org/ocm/shares');
@@ -147,7 +169,7 @@ class Rfc9421RoundTripTest extends TestCase {
}
public function testTooOldSignatureRejected(): void {
- [$signatory] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManager($signatory);
$body = 'msg';
@@ -165,7 +187,7 @@ class Rfc9421RoundTripTest extends TestCase {
}
public function testFutureCreatedRejected(): void {
- [$signatory] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManager($signatory);
$body = 'msg';
@@ -184,7 +206,7 @@ class Rfc9421RoundTripTest extends TestCase {
}
public function testMissingCreatedRejected(): void {
- [$signatory] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManager($signatory);
$body = 'msg';
@@ -205,7 +227,7 @@ class Rfc9421RoundTripTest extends TestCase {
// A peer that signs only `@method` and `@target-uri`: the body and
// freshness window aren't bound. Even with a valid signature we
// must refuse it.
- [$signatory] = $this->ed25519Material('https://sender.example.org/ocm#ed25519');
+ [$signatory] = $this->ecdsaP256Material('https://sender.example.org/ocm#ecdsa-p256-sha256');
$signatoryManager = $this->makeSignatoryManagerWithComponents(
$signatory,
['@method', '@target-uri'],
@@ -220,6 +242,12 @@ class Rfc9421RoundTripTest extends TestCase {
new Rfc9421IncomingSignedRequest($body, $req);
}
+ private function skipUnlessSodium(): void {
+ if (!extension_loaded('sodium')) {
+ $this->markTestSkipped('ext-sodium is not loaded');
+ }
+ }
+
private function makeSignatoryManagerWithComponents(Signatory $signatory, array $components): ISignatoryManager {
return new class($signatory, $components) implements ISignatoryManager {
public function __construct(
@@ -250,6 +278,67 @@ class Rfc9421RoundTripTest extends TestCase {
};
}
+ private function makeSignatoryManagerWithSigningAlgorithm(Signatory $signatory, string $signingAlgorithm): ISignatoryManager {
+ return new class($signatory, $signingAlgorithm) implements ISignatoryManager {
+ public function __construct(
+ private Signatory $sig,
+ private string $signingAlgorithm,
+ ) {
+ }
+
+ public function getProviderId(): string {
+ return 'test';
+ }
+
+ public function getOptions(): array {
+ return [
+ 'algorithm' => SignatureAlgorithm::RSA_SHA256,
+ 'digestAlgorithm' => DigestAlgorithm::SHA256,
+ 'rfc9421.signingAlgorithm' => $this->signingAlgorithm,
+ ];
+ }
+
+ public function getLocalSignatory(): Signatory {
+ return $this->sig;
+ }
+
+ public function getRemoteSignatory(string $remote): ?Signatory {
+ return null;
+ }
+ };
+ }
+
+ /**
+ * @return array{0: Signatory, 1: \Firebase\JWT\Key}
+ */
+ private function ecdsaP256Material(string $kid): array {
+ $pkey = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
+ $privatePem = '';
+ openssl_pkey_export($pkey, $privatePem);
+ $details = openssl_pkey_get_details($pkey);
+ $publicPem = $details['key'];
+
+ $signatory = new Signatory(true);
+ $signatory->setKeyId($kid);
+ $signatory->setPublicKey($publicPem);
+ $signatory->setPrivateKey($privatePem);
+
+ $x = str_pad($details['ec']['x'], 32, "\x00", STR_PAD_LEFT);
+ $y = str_pad($details['ec']['y'], 32, "\x00", STR_PAD_LEFT);
+ $key = JWK::parseKey([
+ 'kty' => 'EC',
+ 'crv' => 'P-256',
+ 'kid' => $kid,
+ 'alg' => 'ES256',
+ 'x' => self::b64url($x),
+ 'y' => self::b64url($y),
+ ], 'ES256');
+ return [$signatory, $key];
+ }
+
+ /**
+ * @return array{0: Signatory, 1: \Firebase\JWT\Key}
+ */
private function ed25519Material(string $kid): array {
$keypair = sodium_crypto_sign_keypair();
$publicKey = sodium_crypto_sign_publickey($keypair);
@@ -263,11 +352,15 @@ class Rfc9421RoundTripTest extends TestCase {
'crv' => 'Ed25519',
'kid' => $kid,
'alg' => 'EdDSA',
- 'x' => rtrim(strtr(base64_encode($publicKey), '+/', '-_'), '='),
+ 'x' => self::b64url($publicKey),
], 'EdDSA');
return [$signatory, $key];
}
+ private static function b64url(string $bin): string {
+ return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
+ }
+
private function makeSignatoryManager(Signatory $signatory): ISignatoryManager {
return new class($signatory) implements ISignatoryManager {
public function __construct(
diff --git a/tests/lib/Security/Signature/Rfc9421/AlgorithmTest.php b/tests/lib/Security/Signature/Rfc9421/AlgorithmTest.php
index ba117ca99ba..bb89430e5f0 100644
--- a/tests/lib/Security/Signature/Rfc9421/AlgorithmTest.php
+++ b/tests/lib/Security/Signature/Rfc9421/AlgorithmTest.php
@@ -19,7 +19,10 @@ class AlgorithmTest extends TestCase {
public function testNormalizeNativeIsPassThrough(): void {
$this->assertSame('ed25519', Algorithm::normalize('ed25519'));
$this->assertSame('rsa-v1_5-sha256', Algorithm::normalize('rsa-v1_5-sha256'));
+ $this->assertSame('rsa-v1_5-sha384', Algorithm::normalize('rsa-v1_5-sha384'));
+ $this->assertSame('rsa-v1_5-sha512', Algorithm::normalize('rsa-v1_5-sha512'));
$this->assertSame('ecdsa-p256-sha256', Algorithm::normalize('ecdsa-p256-sha256'));
+ $this->assertSame('ecdsa-p384-sha384', Algorithm::normalize('ecdsa-p384-sha384'));
}
public function testNormalizeJoseAliases(): void {
@@ -53,10 +56,17 @@ class AlgorithmTest extends TestCase {
$this->assertNull(Algorithm::deriveJoseAlgFromJwk([]));
}
- public function testEd25519RoundTrip(): void {
- [$priv, $key] = $this->ed25519KeyPair();
+ public function testEd25519SigningIsRejected(): void {
+ $this->expectException(SignatureException::class);
+ $this->expectExceptionMessageMatches('/Ed25519 signing is not supported/');
+ Algorithm::sign('payload', str_repeat("\x00", 64), 'ed25519');
+ }
+
+ public function testEd25519VerifyRoundTripWithSodium(): void {
+ $this->skipUnlessSodium();
+ [$secret, $key] = $this->ed25519KeyPair();
$base = 'arbitrary signature base';
- $sig = Algorithm::sign($base, $priv, 'ed25519');
+ $sig = sodium_crypto_sign_detached($base, $secret);
$this->assertSame(64, strlen($sig));
$this->assertTrue(Algorithm::verify($base, $sig, $key, 'ed25519'));
// JOSE alias accepted.
@@ -97,6 +107,7 @@ class AlgorithmTest extends TestCase {
}
public function testAlgHintConflictsWithJwkAlgRejected(): void {
+ $this->skipUnlessSodium();
// Ed25519 JWK, request claims ES256: RFC 9421 §3.2 step 6 disagreement.
[, $key] = $this->ed25519KeyPair();
$this->expectException(SignatureException::class);
@@ -104,6 +115,7 @@ class AlgorithmTest extends TestCase {
}
public function testParseKeyRejectsContradictoryAlg(): void {
+ $this->skipUnlessSodium();
// kty=OKP/crv=Ed25519 with alg=ES256 is contradictory; firebase's
// parseKey rejects it before we ever build a Key.
$keypair = sodium_crypto_sign_keypair();
@@ -117,14 +129,6 @@ class AlgorithmTest extends TestCase {
], null);
}
- public function testAlgHintAgreesViaJoseAlias(): void {
- [$priv, $key] = $this->ed25519KeyPair();
- $base = 'agreement check';
- $sig = Algorithm::sign($base, $priv, 'ed25519');
- $this->assertTrue(Algorithm::verify($base, $sig, $key, 'ed25519'));
- $this->assertTrue(Algorithm::verify($base, $sig, $key, 'EdDSA'));
- }
-
public function testEcdsaRawToDerProducesValidSignature(): void {
[$priv, $key] = $this->ecKeyPair('prime256v1', 'P-256', 'ES256');
$rawSig = Algorithm::sign('msg', $priv, 'ecdsa-p256-sha256');
@@ -137,6 +141,12 @@ class AlgorithmTest extends TestCase {
$this->assertNull(Algorithm::ecdsaRawToDer('short', 32));
}
+ private function skipUnlessSodium(): void {
+ if (!extension_loaded('sodium')) {
+ $this->markTestSkipped('ext-sodium is not loaded');
+ }
+ }
+
/**
* @return array{0: string, 1: Key}
*/
diff --git a/tests/lib/Security/Signature/SignatureManagerDispatchTest.php b/tests/lib/Security/Signature/SignatureManagerDispatchTest.php
index ae5945fd9b5..698ea59d618 100644
--- a/tests/lib/Security/Signature/SignatureManagerDispatchTest.php
+++ b/tests/lib/Security/Signature/SignatureManagerDispatchTest.php
@@ -52,9 +52,6 @@ class SignatureManagerDispatchTest extends TestCase {
}
public function testOutgoingDispatchesToCavageByDefault(): void {
- // Cavage signs with an RSA PEM, so we need a real RSA keypair here;
- // the Ed25519 helper would produce libsodium bytes that openssl_sign
- // can't consume.
$signatoryManager = $this->rsaSignatoryManager();
$signed = $this->signatureManager->getOutgoingSignedRequest(
@@ -68,7 +65,7 @@ class SignatureManagerDispatchTest extends TestCase {
}
public function testOutgoingDispatchesToRfc9421WhenOptionSet(): void {
- [$signatoryManager,] = $this->ed25519SignatoryManager(rfc9421Format: true);
+ [$signatoryManager,] = $this->ecdsaP256SignatoryManager(rfc9421Format: true);
$signed = $this->signatureManager->getOutgoingSignedRequest(
$signatoryManager,
@@ -84,7 +81,7 @@ class SignatureManagerDispatchTest extends TestCase {
}
public function testInboundDispatchesToRfc9421WhenSignatureInputPresent(): void {
- [$signatoryManager, $jwk, $secret] = $this->ed25519SignatoryManager(rfc9421Format: true);
+ [$signatoryManager, $jwk] = $this->ecdsaP256SignatoryManager(rfc9421Format: true);
// Build a real signed request and replay its headers as the inbound
// request to exercise the full inbound path including verification.
@@ -101,14 +98,14 @@ class SignatureManagerDispatchTest extends TestCase {
$this->primeRequest($headers, 'POST', '/ocm/shares', 'receiver.example.org');
- $resolver = $this->makeKeyResolver($signatoryManager, $jwk, 'https://sender.example.org/ocm#ed25519');
+ $resolver = $this->makeKeyResolver($signatoryManager, $jwk, 'https://sender.example.org/ocm#ecdsa-p256-sha256');
$signed = $this->signatureManager->getIncomingSignedRequest($resolver, $body);
$this->assertInstanceOf(Rfc9421IncomingSignedRequest::class, $signed);
}
public function testInboundRejectsRfc9421WhenSignatoryManagerCannotResolve(): void {
- [$signatoryManager,] = $this->ed25519SignatoryManager(rfc9421Format: true);
+ [$signatoryManager,] = $this->ecdsaP256SignatoryManager(rfc9421Format: true);
$body = '{"hello":"world"}';
$out = new Rfc9421OutgoingSignedRequest(
@@ -165,26 +162,31 @@ class SignatureManagerDispatchTest extends TestCase {
}
/**
- * @return array{ISignatoryManager, Key, string} [manager, parsed verification key, raw secret key]
+ * @return array{ISignatoryManager, Key} [manager, parsed verification key]
*/
- private function ed25519SignatoryManager(bool $rfc9421Format): array {
- $keypair = sodium_crypto_sign_keypair();
- $publicKey = sodium_crypto_sign_publickey($keypair);
- $secretKey = sodium_crypto_sign_secretkey($keypair);
- $kid = 'https://sender.example.org/ocm#ed25519';
+ private function ecdsaP256SignatoryManager(bool $rfc9421Format): array {
+ $pkey = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
+ $privatePem = '';
+ openssl_pkey_export($pkey, $privatePem);
+ $details = openssl_pkey_get_details($pkey);
+ $publicPem = $details['key'];
+ $kid = 'https://sender.example.org/ocm#ecdsa-p256-sha256';
$signatory = new Signatory(true);
$signatory->setKeyId($kid);
- $signatory->setPublicKey($publicKey);
- $signatory->setPrivateKey($secretKey);
+ $signatory->setPublicKey($publicPem);
+ $signatory->setPrivateKey($privatePem);
+ $x = str_pad($details['ec']['x'], 32, "\x00", STR_PAD_LEFT);
+ $y = str_pad($details['ec']['y'], 32, "\x00", STR_PAD_LEFT);
$key = JWK::parseKey([
- 'kty' => 'OKP',
- 'crv' => 'Ed25519',
+ 'kty' => 'EC',
+ 'crv' => 'P-256',
'kid' => $kid,
- 'alg' => 'EdDSA',
- 'x' => rtrim(strtr(base64_encode($publicKey), '+/', '-_'), '='),
- ], 'EdDSA');
+ 'alg' => 'ES256',
+ 'x' => rtrim(strtr(base64_encode($x), '+/', '-_'), '='),
+ 'y' => rtrim(strtr(base64_encode($y), '+/', '-_'), '='),
+ ], 'ES256');
$manager = new class($signatory, $rfc9421Format) implements ISignatoryManager {
public function __construct(
@@ -213,7 +215,7 @@ class SignatureManagerDispatchTest extends TestCase {
return null;
}
};
- return [$manager, $key, $secretKey];
+ return [$manager, $key];
}
private function makeKeyResolver(ISignatoryManager $delegate, Key $key, string $kid): IJwkResolvingSignatoryManager {