OutgoingSignedRequest.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Security\Signature\Model;
  8. use JsonSerializable;
  9. use NCU\Security\Signature\Enum\DigestAlgorithm;
  10. use NCU\Security\Signature\Enum\SignatureAlgorithm;
  11. use NCU\Security\Signature\Exceptions\SignatoryException;
  12. use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
  13. use NCU\Security\Signature\IOutgoingSignedRequest;
  14. use NCU\Security\Signature\ISignatoryManager;
  15. use NCU\Security\Signature\ISignatureManager;
  16. use OC\Security\Signature\SignatureManager;
  17. /**
  18. * extends ISignedRequest to add info requested at the generation of the signature
  19. *
  20. * @see ISignatureManager for details on signature
  21. * @since 31.0.0
  22. */
  23. class OutgoingSignedRequest extends SignedRequest implements
  24. IOutgoingSignedRequest,
  25. JsonSerializable {
  26. private string $host = '';
  27. private array $headers = [];
  28. /** @var list<string> $headerList */
  29. private array $headerList = [];
  30. private SignatureAlgorithm $algorithm;
  31. public function __construct(
  32. string $body,
  33. ISignatoryManager $signatoryManager,
  34. private readonly string $identity,
  35. private readonly string $method,
  36. private readonly string $path,
  37. ) {
  38. parent::__construct($body);
  39. $options = $signatoryManager->getOptions();
  40. $this->setHost($identity)
  41. ->setAlgorithm($options['algorithm'] ?? SignatureAlgorithm::RSA_SHA256)
  42. ->setSignatory($signatoryManager->getLocalSignatory())
  43. ->setDigestAlgorithm($options['digestAlgorithm'] ?? DigestAlgorithm::SHA256);
  44. $headers = array_merge([
  45. '(request-target)' => strtolower($method) . ' ' . $path,
  46. 'content-length' => strlen($this->getBody()),
  47. 'date' => gmdate($options['dateHeader'] ?? SignatureManager::DATE_HEADER),
  48. 'digest' => $this->getDigest(),
  49. 'host' => $this->getHost()
  50. ], $options['extraSignatureHeaders'] ?? []);
  51. $signing = $headerList = [];
  52. foreach ($headers as $element => $value) {
  53. $signing[] = $element . ': ' . $value;
  54. $headerList[] = $element;
  55. if ($element !== '(request-target)') {
  56. $this->addHeader($element, $value);
  57. }
  58. }
  59. $this->setHeaderList($headerList)
  60. ->setSignatureData($signing);
  61. }
  62. /**
  63. * @inheritDoc
  64. *
  65. * @param string $host
  66. * @return $this
  67. * @since 31.0.0
  68. */
  69. public function setHost(string $host): self {
  70. $this->host = $host;
  71. return $this;
  72. }
  73. /**
  74. * @inheritDoc
  75. *
  76. * @return string
  77. * @since 31.0.0
  78. */
  79. public function getHost(): string {
  80. return $this->host;
  81. }
  82. /**
  83. * @inheritDoc
  84. *
  85. * @param string $key
  86. * @param string|int|float $value
  87. *
  88. * @return self
  89. * @since 31.0.0
  90. */
  91. public function addHeader(string $key, string|int|float $value): self {
  92. $this->headers[$key] = $value;
  93. return $this;
  94. }
  95. /**
  96. * @inheritDoc
  97. *
  98. * @return array
  99. * @since 31.0.0
  100. */
  101. public function getHeaders(): array {
  102. return $this->headers;
  103. }
  104. /**
  105. * set the ordered list of used headers in the Signature
  106. *
  107. * @param list<string> $list
  108. *
  109. * @return self
  110. * @since 31.0.0
  111. */
  112. public function setHeaderList(array $list): self {
  113. $this->headerList = $list;
  114. return $this;
  115. }
  116. /**
  117. * returns ordered list of used headers in the Signature
  118. *
  119. * @return list<string>
  120. * @since 31.0.0
  121. */
  122. public function getHeaderList(): array {
  123. return $this->headerList;
  124. }
  125. /**
  126. * @inheritDoc
  127. *
  128. * @param SignatureAlgorithm $algorithm
  129. *
  130. * @return self
  131. * @since 31.0.0
  132. */
  133. public function setAlgorithm(SignatureAlgorithm $algorithm): self {
  134. $this->algorithm = $algorithm;
  135. return $this;
  136. }
  137. /**
  138. * @inheritDoc
  139. *
  140. * @return SignatureAlgorithm
  141. * @since 31.0.0
  142. */
  143. public function getAlgorithm(): SignatureAlgorithm {
  144. return $this->algorithm;
  145. }
  146. /**
  147. * @inheritDoc
  148. *
  149. * @return self
  150. * @throws SignatoryException
  151. * @throws SignatoryNotFoundException
  152. * @since 31.0.0
  153. */
  154. public function sign(): self {
  155. $privateKey = $this->getSignatory()->getPrivateKey();
  156. if ($privateKey === '') {
  157. throw new SignatoryException('empty private key');
  158. }
  159. openssl_sign(
  160. implode("\n", $this->getSignatureData()),
  161. $signed,
  162. $privateKey,
  163. $this->getAlgorithm()->value
  164. );
  165. $this->setSignature(base64_encode($signed));
  166. $this->setSigningElements(
  167. [
  168. 'keyId="' . $this->getSignatory()->getKeyId() . '"',
  169. 'algorithm="' . $this->getAlgorithm()->value . '"',
  170. 'headers="' . implode(' ', $this->getHeaderList()) . '"',
  171. 'signature="' . $this->getSignature() . '"'
  172. ]
  173. );
  174. $this->addHeader('Signature', implode(',', $this->getSigningElements()));
  175. return $this;
  176. }
  177. /**
  178. * @param string $clear
  179. * @param string $privateKey
  180. * @param SignatureAlgorithm $algorithm
  181. *
  182. * @return string
  183. * @throws SignatoryException
  184. */
  185. private function signString(string $clear, string $privateKey, SignatureAlgorithm $algorithm): string {
  186. if ($privateKey === '') {
  187. throw new SignatoryException('empty private key');
  188. }
  189. openssl_sign($clear, $signed, $privateKey, $algorithm->value);
  190. return base64_encode($signed);
  191. }
  192. public function jsonSerialize(): array {
  193. return array_merge(
  194. parent::jsonSerialize(),
  195. [
  196. 'host' => $this->host,
  197. 'headers' => $this->headers,
  198. 'algorithm' => $this->algorithm->value,
  199. 'method' => $this->method,
  200. 'identity' => $this->identity,
  201. 'path' => $this->path,
  202. ]
  203. );
  204. }
  205. }