mirror of
https://github.com/nextcloud/server.git
synced 2026-05-28 04:32:30 -04:00
Add the RFC 9421 (HTTP Message Signatures) sign/verify path alongside the existing draft-cavage implementation: - Algorithm: sodium for Ed25519, JWT::sign for RSA / ECDSA, ecdsaRawToDer for the ECDSA wire format. JWK parsing via JWK::parseKey. - SignatureBase: RFC 9421 §2.5 base construction for the derived components OCM uses plus plain HTTP fields. - ContentDigest: RFC 9530 helpers used as a covered component. - Rfc9421IncomingSignedRequest / Rfc9421OutgoingSignedRequest: request models. Parsing of Signature-Input / Signature delegates to gapple\\StructuredFields\\Parser. - IJwkResolvingSignatoryManager: capability bit signatory managers advertise to participate in RFC 9421 verification. - OcmProfile: OCM-mandated dictionary label. - SignatureManager: dispatch to RFC 9421 inbound when Signature-Input is present, outbound when rfc9421.format is set. Plus tests for each primitive and a full round-trip across the model. Signed-off-by: Micke Nordin <kano@sunet.se>
72 lines
2 KiB
PHP
72 lines
2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OC\Security\Signature\Rfc9421;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
/** RFC 9530 `Content-Digest` helpers; covered by RFC 9421 §7.2.5 in OCM signatures. */
|
|
final class ContentDigest {
|
|
public const ALGO_SHA256 = 'sha-256';
|
|
public const ALGO_SHA512 = 'sha-512';
|
|
|
|
public static function compute(string $body, string $algorithm = self::ALGO_SHA256): string {
|
|
$hashAlgorithm = self::hashAlgorithmFor($algorithm);
|
|
return $algorithm . '=:' . base64_encode(hash($hashAlgorithm, $body, true)) . ':';
|
|
}
|
|
|
|
/**
|
|
* True iff at least one recognised algorithm matches and none mismatch.
|
|
* Stricter than RFC 9530 §2's "any-match"; OCM treats mismatches as an
|
|
* attack on the weaker algorithm.
|
|
*/
|
|
public static function verify(string $header, string $body): bool {
|
|
$matched = false;
|
|
foreach (self::parse($header) as $algorithm => $digest) {
|
|
try {
|
|
$hashAlgorithm = self::hashAlgorithmFor($algorithm);
|
|
} catch (InvalidArgumentException) {
|
|
continue;
|
|
}
|
|
if (!hash_equals(hash($hashAlgorithm, $body, true), $digest)) {
|
|
return false;
|
|
}
|
|
$matched = true;
|
|
}
|
|
return $matched;
|
|
}
|
|
|
|
/** @return array<string, string> [algorithm => raw bytes] */
|
|
public static function parse(string $header): array {
|
|
$out = [];
|
|
foreach (explode(',', $header) as $entry) {
|
|
$entry = trim($entry);
|
|
if ($entry === '') {
|
|
continue;
|
|
}
|
|
if (!preg_match('#^([a-z0-9-]+)=:([A-Za-z0-9+/=]*):$#', $entry, $m)) {
|
|
continue;
|
|
}
|
|
$decoded = base64_decode($m[2], true);
|
|
if ($decoded === false) {
|
|
continue;
|
|
}
|
|
$out[strtolower($m[1])] = $decoded;
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
private static function hashAlgorithmFor(string $algorithm): string {
|
|
return match (strtolower($algorithm)) {
|
|
self::ALGO_SHA256 => 'sha256',
|
|
self::ALGO_SHA512 => 'sha512',
|
|
default => throw new InvalidArgumentException('unsupported content-digest algorithm: ' . $algorithm),
|
|
};
|
|
}
|
|
}
|