nextcloud/tests/lib/Security/Signature/Rfc9421/ContentDigestTest.php
Micke Nordin 0eb927e617 feat(http-sig): RFC 9421 protocol primitives
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>
2026-05-27 11:03:55 +02:00

76 lines
2.7 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\Security\Signature\Rfc9421;
use OC\Security\Signature\Rfc9421\ContentDigest;
use Test\TestCase;
class ContentDigestTest extends TestCase {
public function testComputeRoundTrip(): void {
$body = '{"hello":"world"}';
$header = ContentDigest::compute($body, ContentDigest::ALGO_SHA256);
$this->assertStringStartsWith('sha-256=:', $header);
$this->assertStringEndsWith(':', $header);
$this->assertTrue(ContentDigest::verify($header, $body));
}
public function testDifferentBodyFails(): void {
$header = ContentDigest::compute('hello', ContentDigest::ALGO_SHA256);
$this->assertFalse(ContentDigest::verify($header, 'goodbye'));
}
public function testSha512(): void {
$header = ContentDigest::compute('payload', ContentDigest::ALGO_SHA512);
$this->assertStringStartsWith('sha-512=:', $header);
$this->assertTrue(ContentDigest::verify($header, 'payload'));
}
public function testParseMultipleAlgorithmsAcceptsAnyMatch(): void {
$body = 'data';
$sha256 = ContentDigest::compute($body, ContentDigest::ALGO_SHA256);
$sha512 = ContentDigest::compute($body, ContentDigest::ALGO_SHA512);
$header = $sha256 . ', ' . $sha512;
$this->assertTrue(ContentDigest::verify($header, $body));
}
public function testFailsIfAnyRecognisedAlgorithmMismatches(): void {
// All recognised digests must agree. A correct sha-256 alongside a
// wrong sha-512 is treated as an attack on the weaker algorithm,
// not as a successful match on the stronger one.
$body = 'data';
$sha256 = ContentDigest::compute($body, ContentDigest::ALGO_SHA256);
$wrongSha512 = 'sha-512=:' . base64_encode(hash('sha512', 'tampered', true)) . ':';
$this->assertFalse(ContentDigest::verify($sha256 . ', ' . $wrongSha512, $body));
// And the inverse ordering.
$this->assertFalse(ContentDigest::verify($wrongSha512 . ', ' . $sha256, $body));
}
public function testUnknownAlgorithmIsIgnored(): void {
$body = 'data';
$sha256 = ContentDigest::compute($body, ContentDigest::ALGO_SHA256);
$header = 'md5=:abcd:, ' . $sha256;
$this->assertTrue(ContentDigest::verify($header, $body));
}
public function testEmptyHeaderFails(): void {
$this->assertFalse(ContentDigest::verify('', 'body'));
}
public function testGarbageHeaderFails(): void {
$this->assertFalse(ContentDigest::verify('not a digest', 'body'));
}
public function testParseExtractsRawBytes(): void {
$header = ContentDigest::compute('abc', ContentDigest::ALGO_SHA256);
$parsed = ContentDigest::parse($header);
$this->assertArrayHasKey('sha-256', $parsed);
$this->assertSame(hash('sha256', 'abc', true), $parsed['sha-256']);
}
}