aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/macgirvin/http-message-signer
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/macgirvin/http-message-signer')
-rw-r--r--vendor/macgirvin/http-message-signer/LICENSE30
-rw-r--r--vendor/macgirvin/http-message-signer/README.md121
-rw-r--r--vendor/macgirvin/http-message-signer/TESTING5
-rw-r--r--vendor/macgirvin/http-message-signer/composer.json27
-rw-r--r--vendor/macgirvin/http-message-signer/phpunit.xml20
-rw-r--r--vendor/macgirvin/http-message-signer/src/HttpMessageSigner.php821
-rw-r--r--vendor/macgirvin/http-message-signer/src/StructuredFieldTypes.php91
7 files changed, 1115 insertions, 0 deletions
diff --git a/vendor/macgirvin/http-message-signer/LICENSE b/vendor/macgirvin/http-message-signer/LICENSE
new file mode 100644
index 000000000..36f87d3af
--- /dev/null
+++ b/vendor/macgirvin/http-message-signer/LICENSE
@@ -0,0 +1,30 @@
+BSD 3-Clause License
+
+Copyright (c) 2025, Quantificant LLC <waitman@quantificant.com>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names
+ of its contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/vendor/macgirvin/http-message-signer/README.md b/vendor/macgirvin/http-message-signer/README.md
new file mode 100644
index 000000000..3e111f860
--- /dev/null
+++ b/vendor/macgirvin/http-message-signer/README.md
@@ -0,0 +1,121 @@
+# HTTP Message Signer (RFC 9421)
+
+
+A PHP 8.1+ library for signing and verifying HTTP messages (requests or responses) per [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421).
+
+This is a fork of quantificant/http-message-signer
+
+Supports:
+- RSA-SHA256
+- Ed25519
+- HMAC-SHA256
+- PSR-7 requests (e.g., Guzzle)
+- Optionally (recommended) calculate and verify body digest (content-digest header)
+
+Requirements:
+- bakame/http-structured-fields
+- psr/http-message
+
+## Note
+
+This is Alpha version please report issues. Thanks. Tested on PHP 8.4, should run fine on 8.1+
+
+2025-05-28: Partially reversed the constructor change.
+
+
+## Installation
+
+```bash
+composer require macgirvin/http-message-signer
+```
+
+
+## Notes
+
+An instance of a PSR-7 MessageInterface is passed to the sign and verify functions. This can be a RequestInterface or a ResponseInterface. Typically, this will be a RequestInterface. If your web framework does not supply a pre-populated PSR7-compatible request interface, you can quickly generate one using
+
+```
+use GuzzleHttp\Psr7\ServerRequest;
+
+$request = ServerRequest::fromGlobals();
+```
+
+This would typically be used to verify a message.
+
+To sign a message, install the composer package guzzlehttp/psr7 and create an instance of `Request`.
+
+## Usage
+
+```php
+use HttpSignature\HttpMessageSigner;
+use GuzzleHttp\Psr7\Request;
+
+$request = new Request(
+ 'GET',
+ 'https://api.example.com/resource?bat&baz=3',
+ [
+ 'Host' => 'api.example.com',
+ 'Date' => gmdate('D, d M Y H:i:s T'),
+ ...additional headers
+ ]
+);
+
+$signer = (new HttpMessageSigner())
+ ->setPrivateKey($privateKey) // only needed for signing
+ ->setPublicKey($publicKey) // only needed for verifying
+ ->setKeyId('https://example.com/dave#rsaKey') // required
+ ->setAlgorithm('rsa-sha256') // required
+ ->setCreated(time()) // recommended
+ ->setExpires(time() + 300) // optional, contentious
+ ->setNonce('xJJ9;ro.3*kidney`') // optional one-time token
+ ->setTag('fediverse') // optional app profile name
+ ->setSignatureId('sig1') // optional, default is sig1
+
+
+$request = $signer->signRequest('("@method" "@path" "host" "date")', $request);
+$isValid = $signer->verifyRequest($request);
+```
+
+See full examples in `/tests`.
+
+## Structured Fields
+
+RFC9421 makes heavy use of HTTP Structured Fields (RFC8941/RFC9651). The syntax is very precise and unforgiving.
+
+The signRequest() method takes a structured InnerList of components to sign. These may be headers or derived fields.
+The string will look something like the following (where `...` represents additional components):
+
+```
+'("header1" "header2" "@method" ...)'
+```
+
+and may include modifier parameters. These are represented as
+
+```
+'("@query-param";name="foo" "header2";sf "header3" ...)'
+```
+
+Parameters beginning with '@' are components derived from the HTTP request but may not be represented in the headers. Please review RFC9421 for precise definitions.
+
+
+Using the 'sf' parameter on a component will treat a signature component as a Structured Field when normalising the string.
+
+However, parsing Structured Fields by adding the 'sf' parameter is likely to fail unless you know what `type` it is. A built-in table contains the type definition for a number of known stuctured header types. This list is probably incomplete. A method `addStructuredFieldTypes()` is available to add the type information so it can be successfullly parsed. This takes an array with key of the lowercase header name and a value; which is one of 'list', 'innerlist', 'parameters, 'dictionary', 'item'. If the header name is in the list and the 'sf' modifier is used, the header will be parsed as the Structured Field type indicated.
+
+If a Structured Field is declared as type 'dictionary'; it is suitable for use with the RFC9421 `key` parameter. Using this parameter will fail if the Structured Field type is unknown or has not been registered.
+
+The signRequest() and verifyRequest() methods both use an instance of MessageInterface. In nearly all cases, this will be the RequestInterface. However, when signing responses, the default will be the ResponseInterface, and if components are required from the RequestInterface, the :req parameter must be added to the field definition.
+
+To sign or verify an HTTP Response, use a ResponseInterface as the provided `$interface`, and provide the RequestInterface in `$originalRequest`. This is optional but will allow the `req` modifier to work correctly when signing Responses.
+
+## Known issues
+Currently not implemented is the special handling of the `cookie` and `set-cookie` headers when using the `sf` modifier. For further information please see https://httpwg.org/http-extensions/draft-ietf-httpbis-retrofit.html and https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-20 (or later). It is planned to implement this once RFC6265bis is finalised as a new RFC.
+
+Also not currently implemented are some of the many signature algorithms; as we're currently focused primarily on rsa-sha256 and ed25519.
+
+Pull requests welcome.
+
+
+## License
+
+BSD 3-Clause
diff --git a/vendor/macgirvin/http-message-signer/TESTING b/vendor/macgirvin/http-message-signer/TESTING
new file mode 100644
index 000000000..a04c516a2
--- /dev/null
+++ b/vendor/macgirvin/http-message-signer/TESTING
@@ -0,0 +1,5 @@
+Run Tests:
+
+$ composer require --dev phpunit/phpunit
+$ vendor/bin/phpunit
+
diff --git a/vendor/macgirvin/http-message-signer/composer.json b/vendor/macgirvin/http-message-signer/composer.json
new file mode 100644
index 000000000..8c02de2c6
--- /dev/null
+++ b/vendor/macgirvin/http-message-signer/composer.json
@@ -0,0 +1,27 @@
+{
+ "name": "macgirvin/http-message-signer",
+ "description": "RFC 9421 HTTP Message Signer and Verifier for PSR-7 requests",
+ "type": "library",
+ "require": {
+ "php": "^8.1",
+ "psr/http-message": "^2.0",
+ "guzzlehttp/psr7": "^2.0",
+ "bakame/http-structured-fields": "^2.0",
+ "ext-openssl": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "HttpSignature\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "HttpSignature\\Tests\\": "tests/"
+ }
+ },
+ "license": "BSD-3-Clause",
+ "minimum-stability": "stable"
+}
diff --git a/vendor/macgirvin/http-message-signer/phpunit.xml b/vendor/macgirvin/http-message-signer/phpunit.xml
new file mode 100644
index 000000000..4149ce1a9
--- /dev/null
+++ b/vendor/macgirvin/http-message-signer/phpunit.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.1/phpunit.xsd"
+ bootstrap="vendor/autoload.php"
+ displayDetailsOnTestsThatTriggerWarnings="true"
+ colors="true">
+
+ <testsuites>
+ <testsuite name="HttpSignature Test Suite">
+ <directory>tests</directory>
+ </testsuite>
+ </testsuites>
+
+ <source>
+ <include>
+ <directory>src</directory>
+ </include>
+ </source>
+
+</phpunit>
diff --git a/vendor/macgirvin/http-message-signer/src/HttpMessageSigner.php b/vendor/macgirvin/http-message-signer/src/HttpMessageSigner.php
new file mode 100644
index 000000000..977bed4cd
--- /dev/null
+++ b/vendor/macgirvin/http-message-signer/src/HttpMessageSigner.php
@@ -0,0 +1,821 @@
+<?php
+
+namespace HttpSignature;
+
+use Bakame\Http\StructuredFields\InnerList;
+use Bakame\Http\StructuredFields\Item;
+use Bakame\Http\StructuredFields\OuterList;
+use Bakame\Http\StructuredFields\Parameters;
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Bakame\Http\StructuredFields\Dictionary;
+
+class HttpMessageSigner
+{
+ private string $keyId;
+ private string $privateKey;
+ private string $publicKey;
+ private string $algorithm;
+ private string $signatureId = 'sig1';
+ private string $created = '';
+ private string $expires = '';
+ private string $nonce = '';
+ private string $tag = '';
+
+ private array $structuredFieldTypes = [];
+
+ private $originalRequest;
+
+
+ public function __construct()
+ {
+ $this->setStructuredFieldTypes((new StructuredFieldTypes())->getFields());
+ return $this;
+ }
+
+ public function getHeaders($interface): array
+ {
+ $headers = [];
+ foreach ($interface->getHeaders() as $name => $values) {
+ $headers[strtolower($name)] = implode(', ', $values);
+ }
+ return $headers;
+ }
+
+
+ /* PSR-7 interface to signing function */
+
+ public function signRequest(string $coveredFields, MessageInterface $interface, RequestInterface $originalRequest = null): MessageInterface
+ {
+ $headers = $this->getHeaders($interface);
+ if ($originalRequest) {
+ $this->setOriginalRequest($originalRequest);
+ }
+
+ $signedHeaders = $this->sign(
+ $headers,
+ $coveredFields,
+ $interface
+ );
+
+ foreach (['signature-input', 'signature'] as $header) {
+ $interface = $interface->withHeader($header, $signedHeaders[$header]);
+ }
+ return $interface;
+ }
+
+ /* PSR-7 verify interface and also check body digest if included */
+
+ public function verifyRequest(MessageInterface $interface, RequestInterface $originalRequest = null): bool
+ {
+ $headers = [];
+ if ($originalRequest) {
+ $this->setOriginalRequest($originalRequest);
+ }
+ foreach ($interface->getHeaders() as $name => $values) {
+ $headers[strtolower($name)] = implode(', ', $values);
+ }
+
+ /* check the body digest if it's present */
+
+ if (isset($headers['content-digest'])) {
+ $body = (string)$interface->getBody();
+ if (!$this->isBodyDigestValid($body, $headers['content-digest'])) {
+ return false;
+ }
+ }
+
+ return $this->verify($headers, $interface);
+ }
+
+ /**
+ * check body digest
+ *
+ * From https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Digest
+ * The algorithm used to create a digest of the message content. Only two registered digest algorithms are
+ * considered secure: sha-512 and sha-256. The insecure (legacy) registered digest algorithms
+ * are: md5, sha (SHA-1), unixsum, unixcksum, adler (ADLER32) and crc32c.
+ */
+
+ private function isBodyDigestValid(string $body, string $headerValue): bool
+ {
+ if (!preg_match('/sha-(.*?)=:(.*?):/', $headerValue, $matches)) {
+ return false;
+ }
+ if (!in_array($matches[1], ['256', '512'], true)) {
+ return false;
+ }
+
+ $algorithm = 'sha' . $matches[1];
+
+ $expectedDigest = base64_decode($matches[2]);
+ $actualDigest = hash($algorithm, $body, true);
+
+ return hash_equals($expectedDigest, $actualDigest);
+ }
+
+ /**
+ * @param array $headers
+ * @param string $coveredFields
+ *
+ * Can be used as a development function to peek into the internals of the exact things
+ * that were signed and/or test the internal results of data normalisation.
+ * This matches the sign() function except that it just returns the serialised string
+ * and does not sign it.
+ */
+ public function calculateSignatureBase(array $headers, string $coveredFields, $interface)
+ {
+ $signatureComponents = [];
+ $processedComponents = [];
+ $dict = $this->parseStructuredDict($coveredFields);
+
+ if ($dict->isNotEmpty()) {
+ $coveredStructuredFields = $dict->__toString();
+ $indices = $dict->indices();
+ foreach ($indices as $index) {
+ $member = $dict->getByIndex($index);
+ if (!$member) {
+ throw new \Exception('Index ' . $index . ' not found');
+ }
+ if (in_array($member, $processedComponents, true)) {
+ throw new \Exception('Duplicate member found');
+ }
+ $processedComponents[] = $member;
+ $signatureComponents[] = $this->canonicalizeComponent($member, $headers, $interface);
+ }
+ }
+
+ $signatureInput = $coveredStructuredFields . ';keyid="'
+ . $this->keyId . '";alg="' . $this->algorithm . '"';
+
+ if ($this->created) {
+ $signatureInput .= ';created=' . $this->created;
+ }
+ if ($this->expires) {
+ $signatureInput .= ';expires=' . $this->expires;
+ }
+ if ($this->nonce) {
+ $signatureInput .= ';nonce="' . $this->nonce . '"';
+ }
+ if ($this->tag) {
+ $signatureInput .= ';tag="' . $this->tag . '"';
+ }
+
+
+ /**
+ * Always include @signature-params in the result.
+ */
+ $signatureComponents[] = '"@signature-params": ' . $signatureInput;
+
+ $signatureBase = implode("\n", $signatureComponents);
+ return $signatureBase;
+
+ }
+
+ public function sign(array $headers, string $coveredFields, MessageInterface $interface): array
+ {
+ $signatureComponents = [];
+ $processedComponents = [];
+
+ $dict = $this->parseStructuredDict($coveredFields);
+
+ if ($dict->isNotEmpty()) {
+ $coveredStructuredFields = $dict->__toString();
+ $indices = $dict->indices();
+ foreach ($indices as $index) {
+ $member = $dict->getByIndex($index);
+ if (!$member) {
+ throw new \Exception('Index ' . $index . ' not found');
+ }
+ if (in_array($member, $processedComponents, true)) {
+ throw new \Exception('Duplicate member found');
+ }
+ $processedComponents[] = $member;
+ $signatureComponents[] = $this->canonicalizeComponent($member, $headers, $interface);
+ }
+ }
+
+ $signatureInput = $coveredStructuredFields . ';keyid="'
+ . $this->keyId . '";alg="' . $this->algorithm . '"'
+ . (($this->created) ? ';created=' . $this->created : '')
+ . (($this->expires) ? ';expires=' . $this->expires : '')
+ . (($this->nonce) ? ';nonce="' . $this->nonce . '"' : '')
+ . (($this->tag) ? ';tag="' . $this->tag . '"' : '');
+
+ /**
+ * Always include @signature-params in the result.
+ */
+ $signatureComponents[] = '"@signature-params": ' . $signatureInput;
+
+ $signatureBase = implode("\n", $signatureComponents);
+ $signature = $this->createSignature($signatureBase);
+
+ $headers['signature-input'] = "$this->signatureId=$signatureInput";
+ $headers['signature'] = "$this->signatureId=:$signature:";
+
+ return $headers;
+ }
+
+ public function verify(array $headers, $interface): bool
+ {
+ if (!isset($headers['signature-input'], $headers['signature'])) {
+ return false;
+ }
+ $headers[] = 'signature-params';
+
+ $sigInputDict = $this->parseStructuredDict($headers['signature-input']);
+
+ $signatureComponents = [];
+
+ if ($sigInputDict->isNotEmpty()) {
+ $indices = $sigInputDict->indices();
+ foreach ($indices as $index) {
+ [$dictName, $members] = $sigInputDict->getByIndex($index);
+ if ($members instanceof InnerList) {
+ $innerIndices = $members->indices();
+ foreach ($innerIndices as $innerIndex) {
+ $member = $members->getByIndex($innerIndex);
+ $signatureComponents[$dictName][] = $this->canonicalizeComponent($member, $headers, $interface);
+ }
+ $parameters = $this->extractParameters($members);
+
+ if (isset($parameters['expires'])) {
+ $expires = (int) $parameters['expires'];
+ if ($expires < time()) {
+ return false;
+ }
+ if (isset($parameters['created'])) {
+ $created = (int) $parameters['created'];
+ if ($created >= $expires) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ $sigDict = $this->parseStructuredDict($headers['signature']);
+ if ($sigDict->isNotEmpty()) {
+ $indices = $sigDict->indices();
+ foreach ($indices as $index) {
+ [$dictName, $members] = $sigDict->getByIndex($index);
+ if ($members instanceof Item) {
+ $signatures[$dictName] = $members->value();
+ }
+ if ($members instanceof InnerList) {
+ $innerIndices = $members->indices();
+ foreach ($innerIndices as $innerIndex) {
+ $signatures[$dictName][] = $members->getByIndex($innerIndex);
+ }
+ }
+ }
+ }
+
+ foreach ($signatureComponents as $dictName => $dictComponents) {
+ $namedSignatureComponents = $signatureComponents[$dictName];
+ $signatureParamsStr = $sigInputDict[$dictName]->toHttpValue();
+ $namedSignatureComponents[] = '"@signature-params": ' . $signatureParamsStr;
+ $signatureBase = implode("\n", $namedSignatureComponents);
+ if (!isset($sigDict[$dictName])) {
+ return false;
+ }
+
+ $decodedSig = base64_decode(trim($sigDict[$dictName]->__toString(), ':'));
+ return $this->verifySignature($signatureBase, $decodedSig, $params['alg'] ?? $this->algorithm);
+ }
+ return false;
+ }
+
+ protected function extractParameters($field): array
+ {
+ $parameters = [];
+ $fieldParams = $field->parameters();
+
+ if ($fieldParams->isNotEmpty()) {
+ $indices = $fieldParams->indices();
+ foreach ($indices as $index) {
+ [$name, $item] = $fieldParams->getByIndex($index);
+ $parameters[$name] = $item->value();
+ }
+ }
+ return $parameters;
+ }
+
+ private function canonicalizeComponent($field, array $headers, MessageInterface $interface): string
+ {
+ $fieldName = $field->value();
+ $parameters = $this->extractParameters($field);
+ if (isset($parameters['bs']) && isset($parameters['sf'])) {
+ throw new \Exception('Cannot use both bs and sf');
+ }
+
+ $whichRequest = $interface;
+ if (isset($parameters['req']) && $interface instanceof ResponseInterface) {
+ $whichRequest = $this->getOriginalRequest();
+ }
+ $whichHeaders = $headers;
+
+ if (isset($parameters['tr'])) {
+ $whichHeaders = $whichRequest->getTrailers();
+ }
+
+ [$name, $value] = $this->getFieldValue($fieldName, $whichRequest, $whichHeaders, $parameters);
+
+ if (isset($parameters['bs'])) {
+ $result = $name . ';bs: ';
+ $values = $whichRequest->getHeader($fieldName);
+ if (!$values) {
+ return '';
+ }
+ if (!is_array($values)) {
+ $values = [$values];
+ }
+ foreach ($values as $value) {
+ $value = trim($value);
+ $result .= ':' . base64_encode($value) . ':' . ', ';
+ }
+ return $values ? rtrim($result, ', ') : $result;
+ }
+
+ if (isset($parameters['sf'])) {
+ $value = $this->applyStructuredField($name, $value);
+ return $name . ';sf: ' . $value;
+ }
+ if (isset($parameters['key'])) {
+ $childName = $parameters['key'];
+ $value = $this->applySingleKeyValue($name, $childName, $value);
+ return $name . ';key="' . $childName . '": ' . $value;
+ }
+ return $name . ': ' . $value;
+ }
+
+ private function getFieldValue($fieldName, MessageInterface $interface, $headers, $parameters ): array
+ {
+ $value = match ($fieldName) {
+ '@signature-params' => ['', ''],
+ '@method' => ['"@method"', strtoupper($interface->getMethod())],
+ '@authority' => ['"@authority"', $interface->getUri()->getAuthority()],
+ '@scheme' => ['"@scheme"', strtolower($interface->getUri()->getScheme())],
+ '@target-uri' => ['"target-uri"', $interface->getUri()->__toString()],
+ '@request-target' => ['"@request-target"', $interface->getRequestTarget()],
+ '@path' => ['"@path"', $interface->getUri()->getPath()],
+ '@query' => ['"@query"', $interface->getUri()->getQuery()],
+ '@query-param' => $this->getQueryParam($interface, $parameters) ?? ['', ''],
+ '@status' => ['"@status"', '"@status": ' . $interface->getStatusCode()],
+ default => ['"' . $fieldName . '"', trim($headers[$fieldName] ?? '')],
+ };
+ return $value;
+ }
+
+ /**
+ * @param string $query
+ * @return array|null
+ *
+ * Should not use PHP's parse_str function here, as it has some issues with
+ * spaces and dots in parameter names. These are unlikely to occur, but the
+ * following function treats them as opaque strings rather than as variable
+ * names.
+ */
+ private function parseQueryString(string $query): array|null
+ {
+ $result = [];
+
+ if (!isset($query)) {
+ return null;
+ }
+
+ $queryParams = explode('&', $query);
+ foreach ($queryParams as $param) {
+ // The '=' character is not required and indicates a boolean true value if unset.
+ $element = explode('=', $param, 2);
+ $result[urldecode($element[0])] = isset($element[1]) ? urldecode($element[1]) : '';
+ }
+ return $result;
+ }
+
+ /**
+ * @param $parameters
+ * @param $name
+ * @return string|null
+ *
+ * Find one query parameter by name (which must supplied as parameters in the (structured) covered field list).
+ */
+ private function getQueryParam($whichRequest, array $parameters): array
+ {
+ $queryString = $whichRequest->getUri()->getQuery();
+ if ($queryString) {
+ $queryParams = $this->parseQueryString($queryString);
+ $fieldName = $parameters['name'];
+ if ($fieldName) {
+ return ['"_' . $fieldName . '_"', $queryParams[$fieldName] ? '"' . $queryParams[$fieldName] . '"' : ''];
+ }
+ }
+ throw new \Exception('Query string named parameter not set');
+ }
+
+ private function applyStructuredField(string $name, string $fieldValue): string
+ {
+ $type = $this->structuredFieldTypes[trim($name, '"')];
+ switch ($type) {
+ case 'list':
+ $field = OuterList::fromHttpValue($fieldValue);
+ break;
+ case 'innerlist':
+ $field = InnerList::fromHttpValue($fieldValue);
+ break;
+ case 'parameters':
+ $field = Parameters::fromHttpValue($fieldValue);
+ break;
+ case 'dictionary':
+ $field = Dictionary::fromHttpValue($fieldValue);
+ break;
+ case 'item':
+ $field = Item::fromHttpValue($fieldValue);
+ break;
+ case 'url':
+ return '"' . $fieldValue . '"';
+ case 'date':
+ return '@' . strtotime($fieldValue);
+ case 'etag':
+ $result = '';
+ $list = explode(',', $fieldValue);
+ foreach ($list as $item) {
+ if (str_starts_with(trim($item), 'W/')) {
+ $result .= substr(trim($item), 2) . '; w' . ', ';
+ } else {
+ $result .= trim($item) . ', ';
+ }
+ }
+ return rtrim($result, ', ');
+ case 'cookie':
+ // @TODO
+ default:
+ break;
+ }
+ if (!$field) {
+ return '';
+ }
+ return $field->toHttpValue();
+ }
+
+ private function applySingleKeyValue(string $name, string $key, string $fieldValue): string
+ {
+ $type = $this->structuredFieldTypes[trim($name, '"')];
+ if (empty($type) || $type === 'dictionary') {
+ $dictionary = Dictionary::fromHttpValue($fieldValue);
+ if ($dictionary->isNotEmpty() && isset($dictionary[$key])) {
+ return $dictionary[$key]->toHttpValue();
+ }
+ }
+ return '';
+ }
+
+ private function applyByteSequence(string $fieldValue): string
+ {
+ return $fieldValue;
+ }
+
+ private function applyTrailer(string $fieldValue): string
+ {
+ return $fieldValue;
+ }
+
+ private function createSignature(string $data): string
+ {
+ return match ($this->algorithm) {
+ 'rsa-sha256' => $this->rsaSign($data),
+ 'ed25519' => $this->ed25519Sign($data),
+ 'hmac-sha256' => base64_encode(hash_hmac('sha256', $data, $this->privateKey, true)),
+ default => throw new \RuntimeException("Unsupported algorithm: $this->algorithm")
+ };
+ }
+
+ private function verifySignature(string $data, string $signature, string $alg): bool
+ {
+ return match ($alg) {
+ 'rsa-sha256' => openssl_verify($data, $signature, $this->publicKey,
+ OPENSSL_ALGO_SHA256) === 1,
+ 'ed25519' => openssl_verify($data, $signature, $this->publicKey, "Ed25519") === 1,
+ 'hmac-sha256' => hash_equals(
+ base64_encode(hash_hmac('sha256', $data, $this->privateKey, true)),
+ base64_encode($signature)
+ ),
+ default => false
+ };
+ }
+
+ /* sign with rsa or ed25519 */
+
+ private function rsaSign(string $data): string
+ {
+ if (!openssl_sign($data, $signature, $this->privateKey, OPENSSL_ALGO_SHA256)) {
+ throw new \RuntimeException("RSA signing failed");
+ }
+ return base64_encode($signature);
+ }
+
+ private function ed25519Sign(string $data): string
+ {
+ if (!openssl_sign($data, $signature, $this->privateKey, "Ed25519")) {
+ throw new \RuntimeException("Ed25519 signing failed");
+ }
+ return base64_encode($signature);
+ }
+
+ /* parse a structed dict */
+
+ private function parseStructuredDict(string $headerValue)
+ {
+ if (str_starts_with(trim($headerValue), '(')) {
+ return InnerList::fromHttpValue($headerValue);
+ }
+ else {
+ return Dictionary::fromHttpValue($headerValue);
+ }
+ }
+
+ /* Recommended to calculate the digest of the body and add it to
+ covered headers and sign, but not required. Convenience function
+ to calculate the digest.
+
+ ex:
+
+ $digest = $signer->createContentDigestHeader($body);
+ $request = $request->withHeader('Content-Digest', $digest);
+ */
+
+ /**
+ * @param string $body
+ * @param $algorithm ('sha256' || 'sha512')
+ * @return string
+ *
+ * From https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Digest
+ * The algorithm used to create a digest of the message content. Only two registered digest algorithms are
+ * considered secure: sha-512 and sha-256. The insecure (legacy) registered digest algorithms
+ * are: md5, sha (SHA-1), unixsum, unixcksum, adler (ADLER32) and crc32c.
+ */
+
+ public function createContentDigestHeader(string $body, $algorithm = 'sha256'): string
+ {
+ $supportedAlgorithms = ['sha256' => 'sha-256', 'sha512' => 'sha-512'];
+ foreach ($supportedAlgorithms as $alg => $value) {
+ if ($alg === $algorithm) {
+ $algorithmHeaderString = $value;
+ break;
+ }
+ }
+ if (!isset($algorithmHeaderString)) {
+ throw new \RuntimeException("Unsupported algorithm: $algorithm");
+ }
+ $digest = hash($algorithm, $body, true);
+ /**
+ * Output as structured field.
+ */
+ return $algorithmHeaderString . '=:' . base64_encode($digest) . ':';
+ }
+
+ /* Convenience function, probably want to use a robust PSR7 solution instead */
+
+ public static function parseHttpMessage(string $raw): array
+ {
+ [$headerPart, $body] = explode("\r\n\r\n", $raw, 2);
+ $lines = explode("\r\n", $headerPart);
+ $requestLine = array_shift($lines);
+ [$method, $path] = explode(' ', $requestLine);
+
+ $headers = [];
+ foreach ($lines as $line) {
+ [$name, $value] = explode(':', $line, 2);
+ $headers[strtolower(trim($name))] = trim($value);
+ }
+
+ return [
+ 'method' => $method,
+ 'path' => $path,
+ 'headers' => $headers,
+ 'body' => $body
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public function getStructuredFieldTypes(): array
+ {
+ return $this->structuredFieldTypes;
+ }
+
+ /**
+ * @param array $structuredFieldTypes
+ * @return HttpMessageSigner
+ */
+ public function setStructuredFieldTypes(array $structuredFieldTypes): HttpMessageSigner
+ {
+ $this->structuredFieldTypes = $structuredFieldTypes;
+ return $this;
+ }
+
+ /**
+ * $structuredFieldType consists of a key named after a specific header field, and a value
+ * which is one of 'list', 'innerlist', 'parameters, 'dictionary', 'item'.
+ *
+ * Example:
+ * ['example-dict' => 'dictionary']
+ *
+ * The 'sf' flag will not be honoured unless the structured type of the header is registered/known.
+ *
+ * @param array $structuredFieldType
+ * @return $this
+ */
+
+ public function addStructuredFieldType(array $structuredFieldType): HttpMessageSigner
+ {
+ foreach ($structuredFieldType as $key => $value) {
+ $this->structuredFieldTypes[$key] = $value;
+ }
+ return $this;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getOriginalRequest()
+ {
+ return $this->originalRequest;
+ }
+
+ /**
+ * @param mixed $originalRequest
+ * @return HttpMessageSigner
+ */
+ public function setOriginalRequest($originalRequest)
+ {
+ $this->originalRequest = $originalRequest;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getKeyId(): string
+ {
+ return $this->keyId;
+ }
+
+ /**
+ * @param string $keyId
+ * @return HttpMessageSigner
+ */
+ public function setKeyId(string $keyId): HttpMessageSigner
+ {
+ $this->keyId = $keyId;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPrivateKey(): string
+ {
+ return $this->privateKey;
+ }
+
+ /**
+ * @param string $privateKey
+ * @return HttpMessageSigner
+ */
+ public function setPrivateKey(string $privateKey): HttpMessageSigner
+ {
+ $this->privateKey = $privateKey;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPublicKey(): string
+ {
+ return $this->publicKey;
+ }
+
+ /**
+ * @param string $publicKey
+ * @return HttpMessageSigner
+ */
+ public function setPublicKey(string $publicKey): HttpMessageSigner
+ {
+ $this->publicKey = $publicKey;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAlgorithm(): string
+ {
+ return $this->algorithm;
+ }
+
+ /**
+ * @param string $algorithm
+ * @return HttpMessageSigner
+ */
+ public function setAlgorithm(string $algorithm): HttpMessageSigner
+ {
+ $this->algorithm = $algorithm;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getSignatureId(): string
+ {
+ return $this->signatureId;
+ }
+
+ /**
+ * @param string $signatureId
+ * @return HttpMessageSigner
+ */
+ public function setSignatureId(string $signatureId): HttpMessageSigner
+ {
+ $this->signatureId = $signatureId;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCreated(): string
+ {
+ return $this->created;
+ }
+
+ /**
+ * @param string $created
+ * @return HttpMessageSigner
+ */
+ public function setCreated(string $created): HttpMessageSigner
+ {
+ $this->created = $created;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getExpires(): string
+ {
+ return $this->expires;
+ }
+
+ /**
+ * @param string $expires
+ * @return HttpMessageSigner
+ */
+ public function setExpires(string $expires): HttpMessageSigner
+ {
+ $this->expires = $expires;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getNonce(): string
+ {
+ return $this->nonce;
+ }
+
+ /**
+ * @param string $nonce
+ * @return HttpMessageSigner
+ */
+ public function setNonce(string $nonce): HttpMessageSigner
+ {
+ $this->nonce = $nonce;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTag(): string
+ {
+ return $this->tag;
+ }
+
+ /**
+ * @param string $tag
+ * @return HttpMessageSigner
+ */
+ public function setTag(string $tag): HttpMessageSigner
+ {
+ $this->tag = $tag;
+ return $this;
+ }
+}
+
diff --git a/vendor/macgirvin/http-message-signer/src/StructuredFieldTypes.php b/vendor/macgirvin/http-message-signer/src/StructuredFieldTypes.php
new file mode 100644
index 000000000..df70717f8
--- /dev/null
+++ b/vendor/macgirvin/http-message-signer/src/StructuredFieldTypes.php
@@ -0,0 +1,91 @@
+<?php
+namespace HttpSignature;
+
+
+class StructuredFieldTypes
+{
+ public function __construct()
+ {
+ return $this;
+ }
+
+ public function getFields(): array
+ {
+ $returnValue = [];
+ $fields = explode("\n", $this->fieldlist);
+ foreach ($fields as $entry) {
+ $exploded = explode(" ", $entry);
+ $returnValue[$exploded[0]] = trim($exploded[1]);
+ }
+ return $returnValue;
+ }
+
+ public $fieldlist =
+'accept list
+accept-encoding list
+accept-language list
+accept-patch list
+accept-post list
+accept-ranges list
+access-control-allow-credentials item
+access-control-allow-headers list
+access-control-allow-methods list
+access-control-allow-origin item
+access-control-expose-headers list
+access-control-max-age item
+access-control-request-headers list
+access-control-request-method item
+age item
+allow list
+alpn list
+alt-svc dictionary
+alt-used item
+cache-control dictionary
+cdn-loop list
+clear-site-data list
+connection list
+content-encoding list
+content-language list
+content-length list
+content-location url
+content-type item
+cookie cookie
+cross-origin-resource-policy item
+date date
+dnt item
+etag etag
+expect dictionary
+expect-ct dictionary
+expires date
+host item
+if-match etag
+if-modified-since date
+if-none-match etag
+if-unmodified-since date
+keep-alive dictionary
+last-modified date
+location url
+max-forwards item
+origin item
+pragma dictionary
+prefer dictionary
+preference-applied dictionary
+referer url
+retry-after item
+sec-websocket-extensions list
+sec-websocket-protocol list
+sec-websocket-version item
+server-timing list
+set-cookie cookie
+surrogate-control dictionary
+te list
+timing-allow-origin list
+trailer list
+transfer-encoding list
+upgrade-insecure-requests item
+vary list
+x-content-type-options item
+x-frame-options item
+x-xss-protection list';
+
+} \ No newline at end of file