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.

275 lines
9.3 KiB

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