You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Manager.php 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
  5. *
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author Roeland Jago Douma <roeland@famdouma.nl>
  9. *
  10. * @license GNU AGPL version 3 or any later version
  11. *
  12. * This program is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU Affero General Public License as
  14. * published by the Free Software Foundation, either version 3 of the
  15. * License, or (at your option) any later version.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU Affero General Public License for more details.
  21. *
  22. * You should have received a copy of the GNU Affero General Public License
  23. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. *
  25. */
  26. namespace OC\Authentication\WebAuthn;
  27. use Cose\Algorithm\Signature\ECDSA\ES256;
  28. use Cose\Algorithm\Signature\RSA\RS256;
  29. use Cose\Algorithms;
  30. use GuzzleHttp\Psr7\ServerRequest;
  31. use OC\Authentication\WebAuthn\Db\PublicKeyCredentialEntity;
  32. use OC\Authentication\WebAuthn\Db\PublicKeyCredentialMapper;
  33. use OCP\AppFramework\Db\DoesNotExistException;
  34. use OCP\IConfig;
  35. use OCP\IUser;
  36. use Psr\Log\LoggerInterface;
  37. use Webauthn\AttestationStatement\AttestationObjectLoader;
  38. use Webauthn\AttestationStatement\AttestationStatementSupportManager;
  39. use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
  40. use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
  41. use Webauthn\AuthenticatorAssertionResponse;
  42. use Webauthn\AuthenticatorAssertionResponseValidator;
  43. use Webauthn\AuthenticatorAttestationResponse;
  44. use Webauthn\AuthenticatorAttestationResponseValidator;
  45. use Webauthn\AuthenticatorSelectionCriteria;
  46. use Webauthn\PublicKeyCredentialCreationOptions;
  47. use Webauthn\PublicKeyCredentialDescriptor;
  48. use Webauthn\PublicKeyCredentialLoader;
  49. use Webauthn\PublicKeyCredentialParameters;
  50. use Webauthn\PublicKeyCredentialRequestOptions;
  51. use Webauthn\PublicKeyCredentialRpEntity;
  52. use Webauthn\PublicKeyCredentialUserEntity;
  53. use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
  54. class Manager {
  55. /** @var CredentialRepository */
  56. private $repository;
  57. /** @var PublicKeyCredentialMapper */
  58. private $credentialMapper;
  59. /** @var LoggerInterface */
  60. private $logger;
  61. /** @var IConfig */
  62. private $config;
  63. public function __construct(
  64. CredentialRepository $repository,
  65. PublicKeyCredentialMapper $credentialMapper,
  66. LoggerInterface $logger,
  67. IConfig $config
  68. ) {
  69. $this->repository = $repository;
  70. $this->credentialMapper = $credentialMapper;
  71. $this->logger = $logger;
  72. $this->config = $config;
  73. }
  74. public function startRegistration(IUser $user, string $serverHost): PublicKeyCredentialCreationOptions {
  75. $rpEntity = new PublicKeyCredentialRpEntity(
  76. 'Nextcloud', //Name
  77. $this->stripPort($serverHost), //ID
  78. null //Icon
  79. );
  80. $userEntity = new PublicKeyCredentialUserEntity(
  81. $user->getUID(), // Name
  82. $user->getUID(), // ID
  83. $user->getDisplayName() // Display name
  84. // 'https://foo.example.co/avatar/123e4567-e89b-12d3-a456-426655440000' //Icon
  85. );
  86. $challenge = random_bytes(32);
  87. $publicKeyCredentialParametersList = [
  88. new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256),
  89. new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_RS256),
  90. ];
  91. $timeout = 60000;
  92. $excludedPublicKeyDescriptors = [
  93. ];
  94. $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
  95. null,
  96. AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
  97. null,
  98. false,
  99. );
  100. return new PublicKeyCredentialCreationOptions(
  101. $rpEntity,
  102. $userEntity,
  103. $challenge,
  104. $publicKeyCredentialParametersList,
  105. $authenticatorSelectionCriteria,
  106. PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
  107. $excludedPublicKeyDescriptors,
  108. $timeout,
  109. );
  110. }
  111. public function finishRegister(PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, string $name, string $data): PublicKeyCredentialEntity {
  112. $tokenBindingHandler = new TokenBindingNotSupportedHandler();
  113. $attestationStatementSupportManager = new AttestationStatementSupportManager();
  114. $attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
  115. $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager);
  116. $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader);
  117. // Extension Output Checker Handler
  118. $extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
  119. // Authenticator Attestation Response Validator
  120. $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator(
  121. $attestationStatementSupportManager,
  122. $this->repository,
  123. $tokenBindingHandler,
  124. $extensionOutputCheckerHandler
  125. );
  126. $authenticatorAttestationResponseValidator->setLogger($this->logger);
  127. try {
  128. // Load the data
  129. $publicKeyCredential = $publicKeyCredentialLoader->load($data);
  130. $response = $publicKeyCredential->response;
  131. // Check if the response is an Authenticator Attestation Response
  132. if (!$response instanceof AuthenticatorAttestationResponse) {
  133. throw new \RuntimeException('Not an authenticator attestation response');
  134. }
  135. // Check the response against the request
  136. $request = ServerRequest::fromGlobals();
  137. $publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
  138. $response,
  139. $publicKeyCredentialCreationOptions,
  140. $request,
  141. ['localhost'],
  142. );
  143. } catch (\Throwable $exception) {
  144. throw $exception;
  145. }
  146. // Persist the data
  147. return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name);
  148. }
  149. private function stripPort(string $serverHost): string {
  150. return preg_replace('/(:\d+$)/', '', $serverHost);
  151. }
  152. public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions {
  153. // List of registered PublicKeyCredentialDescriptor classes associated to the user
  154. $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) {
  155. $credential = $entity->toPublicKeyCredentialSource();
  156. return new PublicKeyCredentialDescriptor(
  157. $credential->type,
  158. $credential->publicKeyCredentialId,
  159. );
  160. }, $this->credentialMapper->findAllForUid($uid));
  161. // Public Key Credential Request Options
  162. return new PublicKeyCredentialRequestOptions(
  163. random_bytes(32), // Challenge
  164. $this->stripPort($serverHost), // Relying Party ID
  165. $registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes
  166. AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
  167. 60000, // Timeout
  168. );
  169. }
  170. public function finishAuthentication(PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, string $data, string $uid) {
  171. $attestationStatementSupportManager = new AttestationStatementSupportManager();
  172. $attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
  173. $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager);
  174. $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader);
  175. $tokenBindingHandler = new TokenBindingNotSupportedHandler();
  176. $extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
  177. $algorithmManager = new \Cose\Algorithm\Manager();
  178. $algorithmManager->add(new ES256());
  179. $algorithmManager->add(new RS256());
  180. $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
  181. $this->repository,
  182. $tokenBindingHandler,
  183. $extensionOutputCheckerHandler,
  184. $algorithmManager,
  185. );
  186. $authenticatorAssertionResponseValidator->setLogger($this->logger);
  187. try {
  188. $this->logger->debug('Loading publickey credentials from: ' . $data);
  189. // Load the data
  190. $publicKeyCredential = $publicKeyCredentialLoader->load($data);
  191. $response = $publicKeyCredential->response;
  192. // Check if the response is an Authenticator Attestation Response
  193. if (!$response instanceof AuthenticatorAssertionResponse) {
  194. throw new \RuntimeException('Not an authenticator attestation response');
  195. }
  196. // Check the response against the request
  197. $request = ServerRequest::fromGlobals();
  198. $publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check(
  199. $publicKeyCredential->rawId,
  200. $response,
  201. $publicKeyCredentialRequestOptions,
  202. $request,
  203. $uid,
  204. ['localhost'],
  205. );
  206. } catch (\Throwable $e) {
  207. throw $e;
  208. }
  209. return true;
  210. }
  211. public function deleteRegistration(IUser $user, int $id): void {
  212. try {
  213. $entry = $this->credentialMapper->findById($user->getUID(), $id);
  214. } catch (DoesNotExistException $e) {
  215. $this->logger->warning("WebAuthn device $id does not exist, can't delete it");
  216. return;
  217. }
  218. $this->credentialMapper->delete($entry);
  219. }
  220. public function isWebAuthnAvailable(): bool {
  221. if (!extension_loaded('bcmath')) {
  222. return false;
  223. }
  224. if (!extension_loaded('gmp')) {
  225. return false;
  226. }
  227. if (!$this->config->getSystemValueBool('auth.webauthn.enabled', true)) {
  228. return false;
  229. }
  230. return true;
  231. }
  232. }