nextcloud/tests/lib/Security/Signature/Rfc9421/SignatureBaseTest.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

85 lines
2.9 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\SignatureBase;
use OCP\Security\Signature\Exceptions\SignatureException;
use Test\TestCase;
class SignatureBaseTest extends TestCase {
public function testBuildBasicComponents(): void {
$base = SignatureBase::build(
method: 'POST',
uri: 'https://example.org/foo?bar=baz',
headers: [
'content-digest' => 'sha-256=:abcd:',
'date' => 'Mon, 04 May 2026 12:00:00 GMT',
],
components: ['@method', '@target-uri', 'content-digest', 'date'],
signatureParamsLine: '("@method" "@target-uri" "content-digest" "date");created=1;keyid="k"',
);
$expected = '"@method": POST' . "\n"
. '"@target-uri": https://example.org/foo?bar=baz' . "\n"
. '"content-digest": sha-256=:abcd:' . "\n"
. '"date": Mon, 04 May 2026 12:00:00 GMT' . "\n"
. '"@signature-params": ("@method" "@target-uri" "content-digest" "date");created=1;keyid="k"';
$this->assertSame($expected, $base);
}
public function testAuthorityStripsDefaultPort(): void {
$base = SignatureBase::build('GET', 'https://EXAMPLE.org:443/x', [], ['@authority'], '()');
$this->assertStringContainsString('"@authority": example.org' . "\n", $base);
}
public function testAuthorityKeepsCustomPort(): void {
$base = SignatureBase::build('GET', 'https://example.org:8443/x', [], ['@authority'], '()');
$this->assertStringContainsString('"@authority": example.org:8443' . "\n", $base);
}
public function testQueryComponent(): void {
$base = SignatureBase::build('GET', 'https://example.org/x?a=1', [], ['@query'], '()');
$this->assertStringContainsString('"@query": ?a=1' . "\n", $base);
}
public function testMissingFieldThrows(): void {
$this->expectException(SignatureException::class);
SignatureBase::build('GET', 'https://example.org/', [], ['x-missing'], '()');
}
public function testFieldValueIsTrimmed(): void {
$base = SignatureBase::build(
'GET',
'https://example.org/',
['date' => ' Mon, 04 May 2026 12:00:00 GMT '],
['date'],
'()'
);
$this->assertStringContainsString('"date": Mon, 04 May 2026 12:00:00 GMT' . "\n", $base);
}
public function testSerializeSignatureParams(): void {
$line = SignatureBase::serializeSignatureParams(
['@method', '@target-uri'],
['created' => 100, 'keyid' => 'kid', 'expires' => 200],
);
$this->assertSame('("@method" "@target-uri");created=100;keyid="kid";expires=200', $line);
}
public function testSerializeBareItemEscapesQuotes(): void {
$this->assertSame('"\\"hi\\""', SignatureBase::serializeBareItem('"hi"'));
$this->assertSame('"\\\\"', SignatureBase::serializeBareItem('\\'));
}
public function testSerializeBareItemBoolean(): void {
$this->assertSame('?1', SignatureBase::serializeBareItem(true));
$this->assertSame('?0', SignatureBase::serializeBareItem(false));
}
}