[Web] FIDO2: Add Face ID via Apple
parent
ff071e5120
commit
46643af00c
|
@ -38,6 +38,7 @@ class AttestationObject {
|
||||||
switch ($enc['fmt']) {
|
switch ($enc['fmt']) {
|
||||||
case 'android-key': $this->_attestationFormat = new Format\AndroidKey($enc, $this->_authenticatorData); break;
|
case 'android-key': $this->_attestationFormat = new Format\AndroidKey($enc, $this->_authenticatorData); break;
|
||||||
case 'android-safetynet': $this->_attestationFormat = new Format\AndroidSafetyNet($enc, $this->_authenticatorData); break;
|
case 'android-safetynet': $this->_attestationFormat = new Format\AndroidSafetyNet($enc, $this->_authenticatorData); break;
|
||||||
|
case 'apple': $this->_attestationFormat = new Format\Apple($enc, $this->_authenticatorData); break;
|
||||||
case 'fido-u2f': $this->_attestationFormat = new Format\U2f($enc, $this->_authenticatorData); break;
|
case 'fido-u2f': $this->_attestationFormat = new Format\U2f($enc, $this->_authenticatorData); break;
|
||||||
case 'none': $this->_attestationFormat = new Format\None($enc, $this->_authenticatorData); break;
|
case 'none': $this->_attestationFormat = new Format\None($enc, $this->_authenticatorData); break;
|
||||||
case 'packed': $this->_attestationFormat = new Format\Packed($enc, $this->_authenticatorData); break;
|
case 'packed': $this->_attestationFormat = new Format\Packed($enc, $this->_authenticatorData); break;
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class Apple extends FormatBase {
|
||||||
|
private $_x5c;
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check packed data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
|
||||||
|
// certificate for validation
|
||||||
|
if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
|
||||||
|
|
||||||
|
// The attestation certificate attestnCert MUST be the first element in the array
|
||||||
|
$attestnCert = array_shift($attStmt['x5c']);
|
||||||
|
|
||||||
|
if (!($attestnCert instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_x5c = $attestnCert->getBinaryString();
|
||||||
|
|
||||||
|
// certificate chain
|
||||||
|
foreach ($attStmt['x5c'] as $chain) {
|
||||||
|
if ($chain instanceof ByteBuffer) {
|
||||||
|
$this->_x5c_chain[] = $chain->getBinaryString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new WebAuthnException('invalid Apple attestation statement: missing x5c', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
return $this->_createCertificatePem($this->_x5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
return $this->_validateOverX5c($clientDataHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validate if x5c is present
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
protected function _validateOverX5c($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concatenate authenticatorData and clientDataHash to form nonceToHash.
|
||||||
|
$nonceToHash = $this->_authenticatorData->getBinary();
|
||||||
|
$nonceToHash .= $clientDataHash;
|
||||||
|
|
||||||
|
// Perform SHA-256 hash of nonceToHash to produce nonce
|
||||||
|
$nonce = hash('SHA256', $nonceToHash, true);
|
||||||
|
|
||||||
|
$credCert = openssl_x509_read($this->getCertificatePem());
|
||||||
|
if ($credCert === false) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate: ' . \openssl_error_string(), WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$keyData = openssl_pkey_get_details(openssl_pkey_get_public($credCert));
|
||||||
|
$key = is_array($keyData) && array_key_exists('key', $keyData) ? $keyData['key'] : null;
|
||||||
|
|
||||||
|
|
||||||
|
// Verify that nonce equals the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert.
|
||||||
|
$parsedCredCert = openssl_x509_parse($credCert);
|
||||||
|
$nonceExtension = isset($parsedCredCert['extensions']['1.2.840.113635.100.8.2']) ? $parsedCredCert['extensions']['1.2.840.113635.100.8.2'] : '';
|
||||||
|
|
||||||
|
// nonce padded by ASN.1 string: 30 24 A1 22 04 20
|
||||||
|
// 30 — type tag indicating sequence
|
||||||
|
// 24 — 36 byte following
|
||||||
|
// A1 — Enumerated [1]
|
||||||
|
// 22 — 34 byte following
|
||||||
|
// 04 — type tag indicating octet string
|
||||||
|
// 20 — 32 byte following
|
||||||
|
|
||||||
|
$asn1Padding = "\x30\x24\xA1\x22\x04\x20";
|
||||||
|
if (substr($nonceExtension, 0, strlen($asn1Padding)) === $asn1Padding) {
|
||||||
|
$nonceExtension = substr($nonceExtension, strlen($asn1Padding));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($nonceExtension !== $nonce) {
|
||||||
|
throw new WebAuthnException('nonce doesn\'t equal the value of the extension with OID 1.2.840.113635.100.8.2', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the credential public key equals the Subject Public Key of credCert.
|
||||||
|
$authKeyData = openssl_pkey_get_details(openssl_pkey_get_public($this->_authenticatorData->getPublicKeyPem()));
|
||||||
|
$authKey = is_array($authKeyData) && array_key_exists('key', $authKeyData) ? $authKeyData['key'] : null;
|
||||||
|
|
||||||
|
if ($key === null || $key !== $authKey) {
|
||||||
|
throw new WebAuthnException('credential public key doesn\'t equal the Subject Public Key of credCert', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ require_once 'Attestation/Format/FormatBase.php';
|
||||||
require_once 'Attestation/Format/None.php';
|
require_once 'Attestation/Format/None.php';
|
||||||
require_once 'Attestation/Format/AndroidKey.php';
|
require_once 'Attestation/Format/AndroidKey.php';
|
||||||
require_once 'Attestation/Format/AndroidSafetyNet.php';
|
require_once 'Attestation/Format/AndroidSafetyNet.php';
|
||||||
|
require_once 'Attestation/Format/Apple.php';
|
||||||
require_once 'Attestation/Format/Packed.php';
|
require_once 'Attestation/Format/Packed.php';
|
||||||
require_once 'Attestation/Format/Tpm.php';
|
require_once 'Attestation/Format/Tpm.php';
|
||||||
require_once 'Attestation/Format/U2f.php';
|
require_once 'Attestation/Format/U2f.php';
|
||||||
|
@ -42,6 +43,7 @@ class WebAuthn {
|
||||||
$this->_rpId = $rpId;
|
$this->_rpId = $rpId;
|
||||||
$this->_rpIdHash = \hash('sha256', $rpId, true);
|
$this->_rpIdHash = \hash('sha256', $rpId, true);
|
||||||
ByteBuffer::$useBase64UrlEncoding = !!$useBase64UrlEncoding;
|
ByteBuffer::$useBase64UrlEncoding = !!$useBase64UrlEncoding;
|
||||||
|
$supportedFormats = array('android-key', 'android-safetynet', 'apple', 'fido-u2f', 'none', 'packed', 'tpm');
|
||||||
|
|
||||||
if (!\function_exists('\openssl_open')) {
|
if (!\function_exists('\openssl_open')) {
|
||||||
throw new WebAuthnException('OpenSSL-Module not installed');;
|
throw new WebAuthnException('OpenSSL-Module not installed');;
|
||||||
|
@ -51,16 +53,16 @@ class WebAuthn {
|
||||||
throw new WebAuthnException('SHA256 not supported by this openssl installation.');
|
throw new WebAuthnException('SHA256 not supported by this openssl installation.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// default value
|
// default: all format
|
||||||
if (!is_array($allowedFormats)) {
|
if (!is_array($allowedFormats)) {
|
||||||
$allowedFormats = array('android-key', 'fido-u2f', 'packed', 'tpm');
|
$allowedFormats = $supportedFormats;
|
||||||
}
|
}
|
||||||
$this->_formats = $allowedFormats;
|
$this->_formats = $allowedFormats;
|
||||||
|
|
||||||
// validate formats
|
// validate formats
|
||||||
$invalidFormats = \array_diff($this->_formats, array('android-key', 'android-safetynet', 'fido-u2f', 'none', 'packed', 'tpm'));
|
$invalidFormats = \array_diff($this->_formats, $supportedFormats);
|
||||||
if (!$this->_formats || $invalidFormats) {
|
if (!$this->_formats || $invalidFormats) {
|
||||||
throw new WebAuthnException('Invalid formats on construct: ' . implode(', ', $invalidFormats));
|
throw new WebAuthnException('invalid formats on construct: ' . implode(', ', $invalidFormats));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,10 +108,13 @@ class WebAuthn {
|
||||||
* true = required
|
* true = required
|
||||||
* false = preferred
|
* false = preferred
|
||||||
* string 'required' 'preferred' 'discouraged'
|
* string 'required' 'preferred' 'discouraged'
|
||||||
|
* @param bool|null $crossPlatformAttachment true for cross-platform devices (eg. fido usb),
|
||||||
|
* false for platform devices (eg. windows hello, android safetynet),
|
||||||
|
* null for both
|
||||||
* @param array $excludeCredentialIds a array of ids, which are already registered, to prevent re-registration
|
* @param array $excludeCredentialIds a array of ids, which are already registered, to prevent re-registration
|
||||||
* @return \stdClass
|
* @return \stdClass
|
||||||
*/
|
*/
|
||||||
public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20, $requireResidentKey=false, $requireUserVerification=false, $excludeCredentialIds=array()) {
|
public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20, $requireResidentKey=false, $requireUserVerification=false, $crossPlatformAttachment=null, $excludeCredentialIds=array()) {
|
||||||
|
|
||||||
// validate User Verification Requirement
|
// validate User Verification Requirement
|
||||||
if (\is_bool($requireUserVerification)) {
|
if (\is_bool($requireUserVerification)) {
|
||||||
|
@ -133,6 +138,9 @@ class WebAuthn {
|
||||||
if ($requireResidentKey) {
|
if ($requireResidentKey) {
|
||||||
$args->publicKey->authenticatorSelection->requireResidentKey = true;
|
$args->publicKey->authenticatorSelection->requireResidentKey = true;
|
||||||
}
|
}
|
||||||
|
if (is_bool($crossPlatformAttachment)) {
|
||||||
|
$args->publicKey->authenticatorSelection->authenticatorAttachment = $crossPlatformAttachment ? 'cross-platform' : 'platform';
|
||||||
|
}
|
||||||
|
|
||||||
// user
|
// user
|
||||||
$args->publicKey->user = new \stdClass();
|
$args->publicKey->user = new \stdClass();
|
||||||
|
@ -278,22 +286,22 @@ class WebAuthn {
|
||||||
// 2. Let C, the client data claimed as collected during the credential creation,
|
// 2. Let C, the client data claimed as collected during the credential creation,
|
||||||
// be the result of running an implementation-specific JSON parser on JSONtext.
|
// be the result of running an implementation-specific JSON parser on JSONtext.
|
||||||
if (!\is_object($clientData)) {
|
if (!\is_object($clientData)) {
|
||||||
throw new WebAuthnException('Invalid client data', WebAuthnException::INVALID_DATA);
|
throw new WebAuthnException('invalid client data', WebAuthnException::INVALID_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Verify that the value of C.type is webauthn.create.
|
// 3. Verify that the value of C.type is webauthn.create.
|
||||||
if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.create') {
|
if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.create') {
|
||||||
throw new WebAuthnException('Invalid type', WebAuthnException::INVALID_TYPE);
|
throw new WebAuthnException('invalid type', WebAuthnException::INVALID_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.
|
// 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.
|
||||||
if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
|
if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
|
||||||
throw new WebAuthnException('Invalid challenge', WebAuthnException::INVALID_CHALLENGE);
|
throw new WebAuthnException('invalid challenge', WebAuthnException::INVALID_CHALLENGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Verify that the value of C.origin matches the Relying Party's origin.
|
// 5. Verify that the value of C.origin matches the Relying Party's origin.
|
||||||
if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
|
if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
|
||||||
throw new WebAuthnException('Invalid origin', WebAuthnException::INVALID_ORIGIN);
|
throw new WebAuthnException('invalid origin', WebAuthnException::INVALID_ORIGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attestation
|
// Attestation
|
||||||
|
@ -301,27 +309,27 @@ class WebAuthn {
|
||||||
|
|
||||||
// 9. Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP.
|
// 9. Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP.
|
||||||
if (!$attestationObject->validateRpIdHash($this->_rpIdHash)) {
|
if (!$attestationObject->validateRpIdHash($this->_rpIdHash)) {
|
||||||
throw new WebAuthnException('Invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
|
throw new WebAuthnException('invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature
|
// 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature
|
||||||
if (!$attestationObject->validateAttestation($clientDataHash)) {
|
if (!$attestationObject->validateAttestation($clientDataHash)) {
|
||||||
throw new WebAuthnException('Invalid certificate signature', WebAuthnException::INVALID_SIGNATURE);
|
throw new WebAuthnException('invalid certificate signature', WebAuthnException::INVALID_SIGNATURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 15. If validation is successful, obtain a list of acceptable trust anchors
|
// 15. If validation is successful, obtain a list of acceptable trust anchors
|
||||||
if (is_array($this->_caFiles) && !$attestationObject->validateRootCertificate($this->_caFiles)) {
|
if (is_array($this->_caFiles) && !$attestationObject->validateRootCertificate($this->_caFiles)) {
|
||||||
throw new WebAuthnException('Invalid root certificate', WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
throw new WebAuthnException('invalid root certificate', WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Verify that the User Present bit of the flags in authData is set.
|
// 10. Verify that the User Present bit of the flags in authData is set.
|
||||||
if ($requireUserPresent && !$attestationObject->getAuthenticatorData()->getUserPresent()) {
|
if ($requireUserPresent && !$attestationObject->getAuthenticatorData()->getUserPresent()) {
|
||||||
throw new WebAuthnException('User not present during authentication', WebAuthnException::USER_PRESENT);
|
throw new WebAuthnException('user not present during authentication', WebAuthnException::USER_PRESENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
|
// 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
|
||||||
if ($requireUserVerification && !$attestationObject->getAuthenticatorData()->getUserVerified()) {
|
if ($requireUserVerification && !$attestationObject->getAuthenticatorData()->getUserVerified()) {
|
||||||
throw new WebAuthnException('User not verificated during authentication', WebAuthnException::USER_VERIFICATED);
|
throw new WebAuthnException('user not verificated during authentication', WebAuthnException::USER_VERIFICATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
$signCount = $attestationObject->getAuthenticatorData()->getSignCount();
|
$signCount = $attestationObject->getAuthenticatorData()->getSignCount();
|
||||||
|
@ -379,38 +387,38 @@ class WebAuthn {
|
||||||
|
|
||||||
// 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
|
// 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
|
||||||
if (!\is_object($clientData)) {
|
if (!\is_object($clientData)) {
|
||||||
throw new WebAuthnException('Invalid client data', WebAuthnException::INVALID_DATA);
|
throw new WebAuthnException('invalid client data', WebAuthnException::INVALID_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Verify that the value of C.type is the string webauthn.get.
|
// 7. Verify that the value of C.type is the string webauthn.get.
|
||||||
if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.get') {
|
if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.get') {
|
||||||
throw new WebAuthnException('Invalid type', WebAuthnException::INVALID_TYPE);
|
throw new WebAuthnException('invalid type', WebAuthnException::INVALID_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Verify that the value of C.challenge matches the challenge that was sent to the
|
// 8. Verify that the value of C.challenge matches the challenge that was sent to the
|
||||||
// authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.
|
// authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.
|
||||||
if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
|
if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
|
||||||
throw new WebAuthnException('Invalid challenge', WebAuthnException::INVALID_CHALLENGE);
|
throw new WebAuthnException('invalid challenge', WebAuthnException::INVALID_CHALLENGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Verify that the value of C.origin matches the Relying Party's origin.
|
// 9. Verify that the value of C.origin matches the Relying Party's origin.
|
||||||
if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
|
if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
|
||||||
throw new WebAuthnException('Invalid origin', WebAuthnException::INVALID_ORIGIN);
|
throw new WebAuthnException('invalid origin', WebAuthnException::INVALID_ORIGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
|
// 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
|
||||||
if ($authenticatorObj->getRpIdHash() !== $this->_rpIdHash) {
|
if ($authenticatorObj->getRpIdHash() !== $this->_rpIdHash) {
|
||||||
throw new WebAuthnException('Invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
|
throw new WebAuthnException('invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12. Verify that the User Present bit of the flags in authData is set
|
// 12. Verify that the User Present bit of the flags in authData is set
|
||||||
if ($requireUserPresent && !$authenticatorObj->getUserPresent()) {
|
if ($requireUserPresent && !$authenticatorObj->getUserPresent()) {
|
||||||
throw new WebAuthnException('User not present during authentication', WebAuthnException::USER_PRESENT);
|
throw new WebAuthnException('user not present during authentication', WebAuthnException::USER_PRESENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 13. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.
|
// 13. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.
|
||||||
if ($requireUserVerification && !$authenticatorObj->getUserVerified()) {
|
if ($requireUserVerification && !$authenticatorObj->getUserVerified()) {
|
||||||
throw new WebAuthnException('User not verificated during authentication', WebAuthnException::USER_VERIFICATED);
|
throw new WebAuthnException('user not verificated during authentication', WebAuthnException::USER_VERIFICATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 14. Verify the values of the client extension outputs
|
// 14. Verify the values of the client extension outputs
|
||||||
|
@ -428,7 +436,7 @@ class WebAuthn {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256) !== 1) {
|
if (\openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256) !== 1) {
|
||||||
throw new WebAuthnException('Invalid signature', WebAuthnException::INVALID_SIGNATURE);
|
throw new WebAuthnException('invalid signature', WebAuthnException::INVALID_SIGNATURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 17. If the signature counter value authData.signCount is nonzero,
|
// 17. If the signature counter value authData.signCount is nonzero,
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\CBOR\CborDecoder;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Lukas Buchs
|
||||||
|
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||||
|
*/
|
||||||
|
class AttestationObject {
|
||||||
|
private $_authenticatorData;
|
||||||
|
private $_attestationFormat;
|
||||||
|
|
||||||
|
public function __construct($binary , $allowedFormats) {
|
||||||
|
$enc = CborDecoder::decode($binary);
|
||||||
|
// validation
|
||||||
|
if (!\is_array($enc) || !\array_key_exists('fmt', $enc) || !is_string($enc['fmt'])) {
|
||||||
|
throw new WebAuthnException('invalid attestation format', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('attStmt', $enc) || !\is_array($enc['attStmt'])) {
|
||||||
|
throw new WebAuthnException('invalid attestation format (attStmt not available)', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('authData', $enc) || !\is_object($enc['authData']) || !($enc['authData'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid attestation format (authData not available)', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_authenticatorData = new AuthenticatorData($enc['authData']->getBinaryString());
|
||||||
|
|
||||||
|
// Format ok?
|
||||||
|
if (!in_array($enc['fmt'], $allowedFormats)) {
|
||||||
|
throw new WebAuthnException('invalid atttestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($enc['fmt']) {
|
||||||
|
case 'android-key': $this->_attestationFormat = new Format\AndroidKey($enc, $this->_authenticatorData); break;
|
||||||
|
case 'android-safetynet': $this->_attestationFormat = new Format\AndroidSafetyNet($enc, $this->_authenticatorData); break;
|
||||||
|
case 'fido-u2f': $this->_attestationFormat = new Format\U2f($enc, $this->_authenticatorData); break;
|
||||||
|
case 'none': $this->_attestationFormat = new Format\None($enc, $this->_authenticatorData); break;
|
||||||
|
case 'packed': $this->_attestationFormat = new Format\Packed($enc, $this->_authenticatorData); break;
|
||||||
|
case 'tpm': $this->_attestationFormat = new Format\Tpm($enc, $this->_authenticatorData); break;
|
||||||
|
default: throw new WebAuthnException('invalid attestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the attestation public key in PEM format
|
||||||
|
* @return AuthenticatorData
|
||||||
|
*/
|
||||||
|
public function getAuthenticatorData() {
|
||||||
|
return $this->_authenticatorData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the certificate chain as PEM
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getCertificateChain() {
|
||||||
|
return $this->_attestationFormat->getCertificateChain();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return the certificate issuer as string
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificateIssuer() {
|
||||||
|
$pem = $this->getCertificatePem();
|
||||||
|
$issuer = '';
|
||||||
|
if ($pem) {
|
||||||
|
$certInfo = \openssl_x509_parse($pem);
|
||||||
|
if (\is_array($certInfo) && \is_array($certInfo['issuer'])) {
|
||||||
|
if ($certInfo['issuer']['CN']) {
|
||||||
|
$issuer .= \trim($certInfo['issuer']['CN']);
|
||||||
|
}
|
||||||
|
if ($certInfo['issuer']['O'] || $certInfo['issuer']['OU']) {
|
||||||
|
if ($issuer) {
|
||||||
|
$issuer .= ' (' . \trim($certInfo['issuer']['O'] . ' ' . $certInfo['issuer']['OU']) . ')';
|
||||||
|
} else {
|
||||||
|
$issuer .= \trim($certInfo['issuer']['O'] . ' ' . $certInfo['issuer']['OU']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return the certificate subject as string
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificateSubject() {
|
||||||
|
$pem = $this->getCertificatePem();
|
||||||
|
$subject = '';
|
||||||
|
if ($pem) {
|
||||||
|
$certInfo = \openssl_x509_parse($pem);
|
||||||
|
if (\is_array($certInfo) && \is_array($certInfo['subject'])) {
|
||||||
|
if ($certInfo['subject']['CN']) {
|
||||||
|
$subject .= \trim($certInfo['subject']['CN']);
|
||||||
|
}
|
||||||
|
if ($certInfo['subject']['O'] || $certInfo['subject']['OU']) {
|
||||||
|
if ($subject) {
|
||||||
|
$subject .= ' (' . \trim($certInfo['subject']['O'] . ' ' . $certInfo['subject']['OU']) . ')';
|
||||||
|
} else {
|
||||||
|
$subject .= \trim($certInfo['subject']['O'] . ' ' . $certInfo['subject']['OU']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
return $this->_attestationFormat->getCertificatePem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks validity of the signature
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
return $this->_attestationFormat->validateAttestation($clientDataHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
return $this->_attestationFormat->validateRootCertificate($rootCas);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if the RpId-Hash is valid
|
||||||
|
* @param string$rpIdHash
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function validateRpIdHash($rpIdHash) {
|
||||||
|
return $rpIdHash === $this->_authenticatorData->getRpIdHash();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,423 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\CBOR\CborDecoder;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Lukas Buchs
|
||||||
|
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||||
|
*/
|
||||||
|
class AuthenticatorData {
|
||||||
|
protected $_binary;
|
||||||
|
protected $_rpIdHash;
|
||||||
|
protected $_flags;
|
||||||
|
protected $_signCount;
|
||||||
|
protected $_attestedCredentialData;
|
||||||
|
protected $_extensionData;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Cose encoded keys
|
||||||
|
private static $_COSE_KTY = 1;
|
||||||
|
private static $_COSE_ALG = 3;
|
||||||
|
|
||||||
|
// Cose EC2 ES256 P-256 curve
|
||||||
|
private static $_COSE_CRV = -1;
|
||||||
|
private static $_COSE_X = -2;
|
||||||
|
private static $_COSE_Y = -3;
|
||||||
|
|
||||||
|
// Cose RSA PS256
|
||||||
|
private static $_COSE_N = -1;
|
||||||
|
private static $_COSE_E = -2;
|
||||||
|
|
||||||
|
private static $_EC2_TYPE = 2;
|
||||||
|
private static $_EC2_ES256 = -7;
|
||||||
|
private static $_EC2_P256 = 1;
|
||||||
|
|
||||||
|
private static $_RSA_TYPE = 3;
|
||||||
|
private static $_RSA_RS256 = -257;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsing the authenticatorData binary.
|
||||||
|
* @param string $binary
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function __construct($binary) {
|
||||||
|
if (!\is_string($binary) || \strlen($binary) < 37) {
|
||||||
|
throw new WebAuthnException('Invalid authenticatorData input', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
$this->_binary = $binary;
|
||||||
|
|
||||||
|
// Read infos from binary
|
||||||
|
// https://www.w3.org/TR/webauthn/#sec-authenticator-data
|
||||||
|
|
||||||
|
// RP ID
|
||||||
|
$this->_rpIdHash = \substr($binary, 0, 32);
|
||||||
|
|
||||||
|
// flags (1 byte)
|
||||||
|
$flags = \unpack('Cflags', \substr($binary, 32, 1))['flags'];
|
||||||
|
$this->_flags = $this->_readFlags($flags);
|
||||||
|
|
||||||
|
// signature counter: 32-bit unsigned big-endian integer.
|
||||||
|
$this->_signCount = \unpack('Nsigncount', \substr($binary, 33, 4))['signcount'];
|
||||||
|
|
||||||
|
$offset = 37;
|
||||||
|
// https://www.w3.org/TR/webauthn/#sec-attested-credential-data
|
||||||
|
if ($this->_flags->attestedDataIncluded) {
|
||||||
|
$this->_attestedCredentialData = $this->_readAttestData($binary, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->_flags->extensionDataIncluded) {
|
||||||
|
$this->_readExtensionData(\substr($binary, $offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticator Attestation Globally Unique Identifier, a unique number
|
||||||
|
* that identifies the model of the authenticator (not the specific instance
|
||||||
|
* of the authenticator)
|
||||||
|
* The aaguid may be 0 if the user is using a old u2f device and/or if
|
||||||
|
* the browser is using the fido-u2f format.
|
||||||
|
* @return string
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function getAAGUID() {
|
||||||
|
if (!($this->_attestedCredentialData instanceof \stdClass)) {
|
||||||
|
throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
return $this->_attestedCredentialData->aaguid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the authenticatorData as binary
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getBinary() {
|
||||||
|
return $this->_binary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the credentialId
|
||||||
|
* @return string
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function getCredentialId() {
|
||||||
|
if (!($this->_attestedCredentialData instanceof \stdClass)) {
|
||||||
|
throw new WebAuthnException('credential id not included in authenticator data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
return $this->_attestedCredentialData->credentialId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the public key in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getPublicKeyPem() {
|
||||||
|
$der = null;
|
||||||
|
switch ($this->_attestedCredentialData->credentialPublicKey->kty) {
|
||||||
|
case self::$_EC2_TYPE: $der = $this->_getEc2Der(); break;
|
||||||
|
case self::$_RSA_TYPE: $der = $this->_getRsaDer(); break;
|
||||||
|
default: throw new WebAuthnException('invalid key type', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pem = '-----BEGIN PUBLIC KEY-----' . "\n";
|
||||||
|
$pem .= \chunk_split(\base64_encode($der), 64, "\n");
|
||||||
|
$pem .= '-----END PUBLIC KEY-----' . "\n";
|
||||||
|
return $pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the public key in U2F format
|
||||||
|
* @return string
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function getPublicKeyU2F() {
|
||||||
|
if (!($this->_attestedCredentialData instanceof \stdClass)) {
|
||||||
|
throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
return "\x04" . // ECC uncompressed
|
||||||
|
$this->_attestedCredentialData->credentialPublicKey->x .
|
||||||
|
$this->_attestedCredentialData->credentialPublicKey->y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the SHA256 hash of the relying party id (=hostname)
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getRpIdHash() {
|
||||||
|
return $this->_rpIdHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the sign counter
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getSignCount() {
|
||||||
|
return $this->_signCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns true if the user is present
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function getUserPresent() {
|
||||||
|
return $this->_flags->userPresent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns true if the user is verified
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function getUserVerified() {
|
||||||
|
return $this->_flags->userVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// PRIVATE
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns DER encoded EC2 key
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function _getEc2Der() {
|
||||||
|
return $this->_der_sequence(
|
||||||
|
$this->_der_sequence(
|
||||||
|
$this->_der_oid("\x2A\x86\x48\xCE\x3D\x02\x01") . // OID 1.2.840.10045.2.1 ecPublicKey
|
||||||
|
$this->_der_oid("\x2A\x86\x48\xCE\x3D\x03\x01\x07") // 1.2.840.10045.3.1.7 prime256v1
|
||||||
|
) .
|
||||||
|
$this->_der_bitString($this->getPublicKeyU2F())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns DER encoded RSA key
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function _getRsaDer() {
|
||||||
|
return $this->_der_sequence(
|
||||||
|
$this->_der_sequence(
|
||||||
|
$this->_der_oid("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") . // OID 1.2.840.113549.1.1.1 rsaEncryption
|
||||||
|
$this->_der_nullValue()
|
||||||
|
) .
|
||||||
|
$this->_der_bitString(
|
||||||
|
$this->_der_sequence(
|
||||||
|
$this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->n) .
|
||||||
|
$this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->e)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reads the flags from flag byte
|
||||||
|
* @param string $binFlag
|
||||||
|
* @return \stdClass
|
||||||
|
*/
|
||||||
|
private function _readFlags($binFlag) {
|
||||||
|
$flags = new \stdClass();
|
||||||
|
|
||||||
|
$flags->bit_0 = !!($binFlag & 1);
|
||||||
|
$flags->bit_1 = !!($binFlag & 2);
|
||||||
|
$flags->bit_2 = !!($binFlag & 4);
|
||||||
|
$flags->bit_3 = !!($binFlag & 8);
|
||||||
|
$flags->bit_4 = !!($binFlag & 16);
|
||||||
|
$flags->bit_5 = !!($binFlag & 32);
|
||||||
|
$flags->bit_6 = !!($binFlag & 64);
|
||||||
|
$flags->bit_7 = !!($binFlag & 128);
|
||||||
|
|
||||||
|
// named flags
|
||||||
|
$flags->userPresent = $flags->bit_0;
|
||||||
|
$flags->userVerified = $flags->bit_2;
|
||||||
|
$flags->attestedDataIncluded = $flags->bit_6;
|
||||||
|
$flags->extensionDataIncluded = $flags->bit_7;
|
||||||
|
return $flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* read attested data
|
||||||
|
* @param string $binary
|
||||||
|
* @param int $endOffset
|
||||||
|
* @return \stdClass
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _readAttestData($binary, &$endOffset) {
|
||||||
|
$attestedCData = new \stdClass();
|
||||||
|
if (\strlen($binary) <= 55) {
|
||||||
|
throw new WebAuthnException('Attested data should be present but is missing', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The AAGUID of the authenticator
|
||||||
|
$attestedCData->aaguid = \substr($binary, 37, 16);
|
||||||
|
|
||||||
|
//Byte length L of Credential ID, 16-bit unsigned big-endian integer.
|
||||||
|
$length = \unpack('nlength', \substr($binary, 53, 2))['length'];
|
||||||
|
$attestedCData->credentialId = \substr($binary, 55, $length);
|
||||||
|
|
||||||
|
// set end offset
|
||||||
|
$endOffset = 55 + $length;
|
||||||
|
|
||||||
|
// extract public key
|
||||||
|
$attestedCData->credentialPublicKey = $this->_readCredentialPublicKey($binary, 55 + $length, $endOffset);
|
||||||
|
|
||||||
|
return $attestedCData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reads COSE key-encoded elliptic curve public key in EC2 format
|
||||||
|
* @param string $binary
|
||||||
|
* @param int $endOffset
|
||||||
|
* @return \stdClass
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _readCredentialPublicKey($binary, $offset, &$endOffset) {
|
||||||
|
$enc = CborDecoder::decodeInPlace($binary, $offset, $endOffset);
|
||||||
|
|
||||||
|
// COSE key-encoded elliptic curve public key in EC2 format
|
||||||
|
$credPKey = new \stdClass();
|
||||||
|
$credPKey->kty = $enc[self::$_COSE_KTY];
|
||||||
|
$credPKey->alg = $enc[self::$_COSE_ALG];
|
||||||
|
|
||||||
|
switch ($credPKey->alg) {
|
||||||
|
case self::$_EC2_ES256: $this->_readCredentialPublicKeyES256($credPKey, $enc); break;
|
||||||
|
case self::$_RSA_RS256: $this->_readCredentialPublicKeyRS256($credPKey, $enc); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $credPKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* extract ES256 informations from cose
|
||||||
|
* @param \stdClass $credPKey
|
||||||
|
* @param \stdClass $enc
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _readCredentialPublicKeyES256(&$credPKey, $enc) {
|
||||||
|
$credPKey->crv = $enc[self::$_COSE_CRV];
|
||||||
|
$credPKey->x = $enc[self::$_COSE_X] instanceof ByteBuffer ? $enc[self::$_COSE_X]->getBinaryString() : null;
|
||||||
|
$credPKey->y = $enc[self::$_COSE_Y] instanceof ByteBuffer ? $enc[self::$_COSE_Y]->getBinaryString() : null;
|
||||||
|
unset ($enc);
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if ($credPKey->kty !== self::$_EC2_TYPE) {
|
||||||
|
throw new WebAuthnException('public key not in EC2 format', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($credPKey->alg !== self::$_EC2_ES256) {
|
||||||
|
throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($credPKey->crv !== self::$_EC2_P256) {
|
||||||
|
throw new WebAuthnException('curve not P-256', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\strlen($credPKey->x) !== 32) {
|
||||||
|
throw new WebAuthnException('Invalid X-coordinate', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\strlen($credPKey->y) !== 32) {
|
||||||
|
throw new WebAuthnException('Invalid Y-coordinate', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* extract RS256 informations from COSE
|
||||||
|
* @param \stdClass $credPKey
|
||||||
|
* @param \stdClass $enc
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _readCredentialPublicKeyRS256(&$credPKey, $enc) {
|
||||||
|
$credPKey->n = $enc[self::$_COSE_N] instanceof ByteBuffer ? $enc[self::$_COSE_N]->getBinaryString() : null;
|
||||||
|
$credPKey->e = $enc[self::$_COSE_E] instanceof ByteBuffer ? $enc[self::$_COSE_E]->getBinaryString() : null;
|
||||||
|
unset ($enc);
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if ($credPKey->kty !== self::$_RSA_TYPE) {
|
||||||
|
throw new WebAuthnException('public key not in RSA format', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($credPKey->alg !== self::$_RSA_RS256) {
|
||||||
|
throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\strlen($credPKey->n) !== 256) {
|
||||||
|
throw new WebAuthnException('Invalid RSA modulus', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\strlen($credPKey->e) !== 3) {
|
||||||
|
throw new WebAuthnException('Invalid RSA public exponent', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reads cbor encoded extension data.
|
||||||
|
* @param string $binary
|
||||||
|
* @return array
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _readExtensionData($binary) {
|
||||||
|
$ext = CborDecoder::decode($binary);
|
||||||
|
if (!\is_array($ext)) {
|
||||||
|
throw new WebAuthnException('invalid extension data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// DER functions
|
||||||
|
// ---------------
|
||||||
|
|
||||||
|
private function _der_length($len) {
|
||||||
|
if ($len < 128) {
|
||||||
|
return \chr($len);
|
||||||
|
}
|
||||||
|
$lenBytes = '';
|
||||||
|
while ($len > 0) {
|
||||||
|
$lenBytes = \chr($len % 256) . $lenBytes;
|
||||||
|
$len = \intdiv($len, 256);
|
||||||
|
}
|
||||||
|
return \chr(0x80 | \strlen($lenBytes)) . $lenBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _der_sequence($contents) {
|
||||||
|
return "\x30" . $this->_der_length(\strlen($contents)) . $contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _der_oid($encoded) {
|
||||||
|
return "\x06" . $this->_der_length(\strlen($encoded)) . $encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _der_bitString($bytes) {
|
||||||
|
return "\x03" . $this->_der_length(\strlen($bytes) + 1) . "\x00" . $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _der_nullValue() {
|
||||||
|
return "\x05\x00";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _der_unsignedInteger($bytes) {
|
||||||
|
$len = \strlen($bytes);
|
||||||
|
|
||||||
|
// Remove leading zero bytes
|
||||||
|
for ($i = 0; $i < ($len - 1); $i++) {
|
||||||
|
if (\ord($bytes[$i]) !== 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($i !== 0) {
|
||||||
|
$bytes = \substr($bytes, $i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If most significant bit is set, prefix with another zero to prevent it being seen as negative number
|
||||||
|
if ((\ord($bytes[0]) & 0x80) !== 0) {
|
||||||
|
$bytes = "\x00" . $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\x02" . $this->_der_length(\strlen($bytes)) . $bytes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class AndroidKey extends FormatBase {
|
||||||
|
private $_alg;
|
||||||
|
private $_signature;
|
||||||
|
private $_x5c;
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check u2f data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||||
|
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) < 1) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_alg = $attStmt['alg'];
|
||||||
|
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||||
|
$this->_x5c = $attStmt['x5c'][0]->getBinaryString();
|
||||||
|
|
||||||
|
if (count($attStmt['x5c']) > 1) {
|
||||||
|
for ($i=1; $i<count($attStmt['x5c']); $i++) {
|
||||||
|
$this->_x5c_chain[] = $attStmt['x5c'][$i]->getBinaryString();
|
||||||
|
}
|
||||||
|
unset ($i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
return $this->_createCertificatePem($this->_x5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||||
|
// using the attestation public key in attestnCert with the algorithm specified in alg.
|
||||||
|
$dataToVerify = $this->_authenticatorData->getBinary();
|
||||||
|
$dataToVerify .= $clientDataHash;
|
||||||
|
|
||||||
|
$coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
|
||||||
|
|
||||||
|
// check certificate
|
||||||
|
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class AndroidSafetyNet extends FormatBase {
|
||||||
|
private $_signature;
|
||||||
|
private $_signedValue;
|
||||||
|
private $_x5c;
|
||||||
|
private $_payload;
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
if (!\array_key_exists('ver', $attStmt) || !$attStmt['ver']) {
|
||||||
|
throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('response', $attStmt) || !($attStmt['response'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $attStmt['response']->getBinaryString();
|
||||||
|
|
||||||
|
// Response is a JWS [RFC7515] object in Compact Serialization.
|
||||||
|
// JWSs have three segments separated by two period ('.') characters
|
||||||
|
$parts = \explode('.', $response);
|
||||||
|
unset ($response);
|
||||||
|
if (\count($parts) !== 3) {
|
||||||
|
throw new WebAuthnException('invalid JWS data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = $this->_base64url_decode($parts[0]);
|
||||||
|
$payload = $this->_base64url_decode($parts[1]);
|
||||||
|
$this->_signature = $this->_base64url_decode($parts[2]);
|
||||||
|
$this->_signedValue = $parts[0] . '.' . $parts[1];
|
||||||
|
unset ($parts);
|
||||||
|
|
||||||
|
$header = \json_decode($header);
|
||||||
|
$payload = \json_decode($payload);
|
||||||
|
|
||||||
|
if (!($header instanceof \stdClass)) {
|
||||||
|
throw new WebAuthnException('invalid JWS header', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
if (!($payload instanceof \stdClass)) {
|
||||||
|
throw new WebAuthnException('invalid JWS payload', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$header->x5c || !is_array($header->x5c) || count($header->x5c) === 0) {
|
||||||
|
throw new WebAuthnException('No X.509 signature in JWS Header', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// algorithm
|
||||||
|
if (!\in_array($header->alg, array('RS256', 'ES256'))) {
|
||||||
|
throw new WebAuthnException('invalid JWS algorithm ' . $header->alg, WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_x5c = \base64_decode($header->x5c[0]);
|
||||||
|
$this->_payload = $payload;
|
||||||
|
|
||||||
|
if (count($header->x5c) > 1) {
|
||||||
|
for ($i=1; $i<count($header->x5c); $i++) {
|
||||||
|
$this->_x5c_chain[] = \base64_decode($header->x5c[$i]);
|
||||||
|
}
|
||||||
|
unset ($i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
return $this->_createCertificatePem($this->_x5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
// Verify that the nonce in the response is identical to the Base64 encoding
|
||||||
|
// of the SHA-256 hash of the concatenation of authenticatorData and clientDataHash.
|
||||||
|
if (!$this->_payload->nonce || $this->_payload->nonce !== \base64_encode(\hash('SHA256', $this->_authenticatorData->getBinary() . $clientDataHash, true))) {
|
||||||
|
throw new WebAuthnException('invalid nonce in JWS payload', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that attestationCert is issued to the hostname "attest.android.com"
|
||||||
|
$certInfo = \openssl_x509_parse($this->getCertificatePem());
|
||||||
|
if (!\is_array($certInfo) || !$certInfo['subject'] || $certInfo['subject']['CN'] !== 'attest.android.com') {
|
||||||
|
throw new WebAuthnException('invalid certificate CN in JWS (' . $certInfo['subject']['CN']. ')', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the ctsProfileMatch attribute in the payload of response is true.
|
||||||
|
if (!$this->_payload->ctsProfileMatch) {
|
||||||
|
throw new WebAuthnException('invalid ctsProfileMatch in payload', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check certificate
|
||||||
|
return \openssl_verify($this->_signedValue, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* decode base64 url
|
||||||
|
* @param string $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function _base64url_decode($data) {
|
||||||
|
return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
|
||||||
|
|
||||||
|
abstract class FormatBase {
|
||||||
|
protected $_attestationObject = null;
|
||||||
|
protected $_authenticatorData = null;
|
||||||
|
protected $_x5c_chain = array();
|
||||||
|
protected $_x5c_tempFile = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param Array $AttestionObject
|
||||||
|
* @param \WebAuthn\Attestation\AuthenticatorData $authenticatorData
|
||||||
|
*/
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
$this->_attestationObject = $AttestionObject;
|
||||||
|
$this->_authenticatorData = $authenticatorData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function __destruct() {
|
||||||
|
// delete X.509 chain certificate file after use
|
||||||
|
if (\is_file($this->_x5c_tempFile)) {
|
||||||
|
\unlink($this->_x5c_tempFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the certificate chain in PEM format
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getCertificateChain() {
|
||||||
|
if (\is_file($this->_x5c_tempFile)) {
|
||||||
|
return \file_get_contents($this->_x5c_tempFile);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the key X.509 certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
// need to be overwritten
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks validity of the signature
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
// need to be overwritten
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
// need to be overwritten
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a PEM encoded certificate with X.509 binary data
|
||||||
|
* @param string $x5c
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function _createCertificatePem($x5c) {
|
||||||
|
$pem = '-----BEGIN CERTIFICATE-----' . "\n";
|
||||||
|
$pem .= \chunk_split(\base64_encode($x5c), 64, "\n");
|
||||||
|
$pem .= '-----END CERTIFICATE-----' . "\n";
|
||||||
|
return $pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a PEM encoded chain file
|
||||||
|
* @return type
|
||||||
|
*/
|
||||||
|
protected function _createX5cChainFile() {
|
||||||
|
$content = '';
|
||||||
|
if (\is_array($this->_x5c_chain) && \count($this->_x5c_chain) > 0) {
|
||||||
|
foreach ($this->_x5c_chain as $x5c) {
|
||||||
|
$certInfo = \openssl_x509_parse($this->_createCertificatePem($x5c));
|
||||||
|
// check if issuer = subject (self signed)
|
||||||
|
if (\is_array($certInfo) && \is_array($certInfo['issuer']) && \is_array($certInfo['subject'])) {
|
||||||
|
$selfSigned = true;
|
||||||
|
foreach ($certInfo['issuer'] as $k => $v) {
|
||||||
|
if ($certInfo['subject'][$k] !== $v) {
|
||||||
|
$selfSigned = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$selfSigned) {
|
||||||
|
$content .= "\n" . $this->_createCertificatePem($x5c) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($content) {
|
||||||
|
$this->_x5c_tempFile = \sys_get_temp_dir() . '/x5c_chain_' . \base_convert(\rand(), 10, 36) . '.pem';
|
||||||
|
if (\file_put_contents($this->_x5c_tempFile, $content) !== false) {
|
||||||
|
return $this->_x5c_tempFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the name and openssl key for provided cose number.
|
||||||
|
* @param int $coseNumber
|
||||||
|
* @return \stdClass|null
|
||||||
|
*/
|
||||||
|
protected function _getCoseAlgorithm($coseNumber) {
|
||||||
|
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms
|
||||||
|
$coseAlgorithms = array(
|
||||||
|
array(
|
||||||
|
'hash' => 'SHA1',
|
||||||
|
'openssl' => OPENSSL_ALGO_SHA1,
|
||||||
|
'cose' => array(
|
||||||
|
-65535 // RS1
|
||||||
|
)),
|
||||||
|
|
||||||
|
array(
|
||||||
|
'hash' => 'SHA256',
|
||||||
|
'openssl' => OPENSSL_ALGO_SHA256,
|
||||||
|
'cose' => array(
|
||||||
|
-257, // RS256
|
||||||
|
-37, // PS256
|
||||||
|
-7, // ES256
|
||||||
|
5 // HMAC256
|
||||||
|
)),
|
||||||
|
|
||||||
|
array(
|
||||||
|
'hash' => 'SHA384',
|
||||||
|
'openssl' => OPENSSL_ALGO_SHA384,
|
||||||
|
'cose' => array(
|
||||||
|
-258, // RS384
|
||||||
|
-38, // PS384
|
||||||
|
-35, // ES384
|
||||||
|
6 // HMAC384
|
||||||
|
)),
|
||||||
|
|
||||||
|
array(
|
||||||
|
'hash' => 'SHA512',
|
||||||
|
'openssl' => OPENSSL_ALGO_SHA512,
|
||||||
|
'cose' => array(
|
||||||
|
-259, // RS512
|
||||||
|
-39, // PS512
|
||||||
|
-36, // ES512
|
||||||
|
7 // HMAC512
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($coseAlgorithms as $coseAlgorithm) {
|
||||||
|
if (\in_array($coseNumber, $coseAlgorithm['cose'], true)) {
|
||||||
|
$return = new \stdClass();
|
||||||
|
$return->hash = $coseAlgorithm['hash'];
|
||||||
|
$return->openssl = $coseAlgorithm['openssl'];
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
|
||||||
|
class None extends FormatBase {
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class Packed extends FormatBase {
|
||||||
|
private $_alg;
|
||||||
|
private $_signature;
|
||||||
|
private $_x5c;
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check packed data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||||
|
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_alg = $attStmt['alg'];
|
||||||
|
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||||
|
|
||||||
|
// certificate for validation
|
||||||
|
if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
|
||||||
|
|
||||||
|
// The attestation certificate attestnCert MUST be the first element in the array
|
||||||
|
$attestnCert = array_shift($attStmt['x5c']);
|
||||||
|
|
||||||
|
if (!($attestnCert instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_x5c = $attestnCert->getBinaryString();
|
||||||
|
|
||||||
|
// certificate chain
|
||||||
|
foreach ($attStmt['x5c'] as $chain) {
|
||||||
|
if ($chain instanceof ByteBuffer) {
|
||||||
|
$this->_x5c_chain[] = $chain->getBinaryString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
if (!$this->_x5c) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $this->_createCertificatePem($this->_x5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
if ($this->_x5c) {
|
||||||
|
return $this->_validateOverX5c($clientDataHash);
|
||||||
|
} else {
|
||||||
|
return $this->_validateSelfAttestation($clientDataHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
if (!$this->_x5c) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validate if x5c is present
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
protected function _validateOverX5c($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||||
|
// using the attestation public key in attestnCert with the algorithm specified in alg.
|
||||||
|
$dataToVerify = $this->_authenticatorData->getBinary();
|
||||||
|
$dataToVerify .= $clientDataHash;
|
||||||
|
|
||||||
|
$coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
|
||||||
|
|
||||||
|
// check certificate
|
||||||
|
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validate if self attestation is in use
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function _validateSelfAttestation($clientDataHash) {
|
||||||
|
// Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||||
|
// using the credential public key with alg.
|
||||||
|
$dataToVerify = $this->_authenticatorData->getBinary();
|
||||||
|
$dataToVerify .= $clientDataHash;
|
||||||
|
|
||||||
|
$publicKey = $this->_authenticatorData->getPublicKeyPem();
|
||||||
|
|
||||||
|
// check certificate
|
||||||
|
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class Tpm extends FormatBase {
|
||||||
|
private $_TPM_GENERATED_VALUE = "\xFF\x54\x43\x47";
|
||||||
|
private $_TPM_ST_ATTEST_CERTIFY = "\x80\x17";
|
||||||
|
private $_alg;
|
||||||
|
private $_signature;
|
||||||
|
private $_pubArea;
|
||||||
|
private $_x5c;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ByteBuffer
|
||||||
|
*/
|
||||||
|
private $_certInfo;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check packed data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
if (!\array_key_exists('ver', $attStmt) || $attStmt['ver'] !== '2.0') {
|
||||||
|
throw new WebAuthnException('invalid tpm version: ' . $attStmt['ver'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||||
|
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('signature not found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('certInfo', $attStmt) || !\is_object($attStmt['certInfo']) || !($attStmt['certInfo'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('certInfo not found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('pubArea', $attStmt) || !\is_object($attStmt['pubArea']) || !($attStmt['pubArea'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('pubArea not found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_alg = $attStmt['alg'];
|
||||||
|
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||||
|
$this->_certInfo = $attStmt['certInfo'];
|
||||||
|
$this->_pubArea = $attStmt['pubArea'];
|
||||||
|
|
||||||
|
// certificate for validation
|
||||||
|
if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
|
||||||
|
|
||||||
|
// The attestation certificate attestnCert MUST be the first element in the array
|
||||||
|
$attestnCert = array_shift($attStmt['x5c']);
|
||||||
|
|
||||||
|
if (!($attestnCert instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_x5c = $attestnCert->getBinaryString();
|
||||||
|
|
||||||
|
// certificate chain
|
||||||
|
foreach ($attStmt['x5c'] as $chain) {
|
||||||
|
if ($chain instanceof ByteBuffer) {
|
||||||
|
$this->_x5c_chain[] = $chain->getBinaryString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new WebAuthnException('no x5c certificate found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
if (!$this->_x5c) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $this->_createCertificatePem($this->_x5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
return $this->_validateOverX5c($clientDataHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
if (!$this->_x5c) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validate if x5c is present
|
||||||
|
* @param string $clientDataHash
|
||||||
|
* @return bool
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
protected function _validateOverX5c($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concatenate authenticatorData and clientDataHash to form attToBeSigned.
|
||||||
|
$attToBeSigned = $this->_authenticatorData->getBinary();
|
||||||
|
$attToBeSigned .= $clientDataHash;
|
||||||
|
|
||||||
|
// Validate that certInfo is valid:
|
||||||
|
|
||||||
|
// Verify that magic is set to TPM_GENERATED_VALUE.
|
||||||
|
if ($this->_certInfo->getBytes(0, 4) !== $this->_TPM_GENERATED_VALUE) {
|
||||||
|
throw new WebAuthnException('tpm magic not TPM_GENERATED_VALUE', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that type is set to TPM_ST_ATTEST_CERTIFY.
|
||||||
|
if ($this->_certInfo->getBytes(4, 2) !== $this->_TPM_ST_ATTEST_CERTIFY) {
|
||||||
|
throw new WebAuthnException('tpm type not TPM_ST_ATTEST_CERTIFY', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$offset = 6;
|
||||||
|
$qualifiedSigner = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset);
|
||||||
|
$extraData = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset);
|
||||||
|
$coseAlg = $this->_getCoseAlgorithm($this->_alg);
|
||||||
|
|
||||||
|
// Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
|
||||||
|
if ($extraData->getBinaryString() !== \hash($coseAlg->hash, $attToBeSigned, true)) {
|
||||||
|
throw new WebAuthnException('certInfo:extraData not hash of attToBeSigned', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the sig is a valid signature over certInfo using the attestation
|
||||||
|
// public key in aikCert with the algorithm specified in alg.
|
||||||
|
return \openssl_verify($this->_certInfo->getBinaryString(), $this->_signature, $publicKey, $coseAlg->openssl) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns next part of ByteBuffer
|
||||||
|
* @param ByteBuffer $buffer
|
||||||
|
* @param int $offset
|
||||||
|
* @return ByteBuffer
|
||||||
|
*/
|
||||||
|
protected function _tpmReadLengthPrefixed(ByteBuffer $buffer, &$offset) {
|
||||||
|
$len = $buffer->getUint16Val($offset);
|
||||||
|
$data = $buffer->getBytes($offset + 2, $len);
|
||||||
|
$offset += (2 + $len);
|
||||||
|
|
||||||
|
return new ByteBuffer($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Attestation\Format;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
class U2f extends FormatBase {
|
||||||
|
private $_alg;
|
||||||
|
private $_signature;
|
||||||
|
private $_x5c;
|
||||||
|
|
||||||
|
public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
|
||||||
|
parent::__construct($AttestionObject, $authenticatorData);
|
||||||
|
|
||||||
|
// check u2f data
|
||||||
|
$attStmt = $this->_attestationObject['attStmt'];
|
||||||
|
|
||||||
|
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||||
|
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) !== 1) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) {
|
||||||
|
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_alg = $attStmt['alg'];
|
||||||
|
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||||
|
$this->_x5c = $attStmt['x5c'][0]->getBinaryString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* returns the key certificate in PEM format
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCertificatePem() {
|
||||||
|
$pem = '-----BEGIN CERTIFICATE-----' . "\n";
|
||||||
|
$pem .= \chunk_split(\base64_encode($this->_x5c), 64, "\n");
|
||||||
|
$pem .= '-----END CERTIFICATE-----' . "\n";
|
||||||
|
return $pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $clientDataHash
|
||||||
|
*/
|
||||||
|
public function validateAttestation($clientDataHash) {
|
||||||
|
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||||
|
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
|
||||||
|
$dataToVerify = "\x00";
|
||||||
|
$dataToVerify .= $this->_authenticatorData->getRpIdHash();
|
||||||
|
$dataToVerify .= $clientDataHash;
|
||||||
|
$dataToVerify .= $this->_authenticatorData->getCredentialId();
|
||||||
|
$dataToVerify .= $this->_authenticatorData->getPublicKeyU2F();
|
||||||
|
|
||||||
|
$coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
|
||||||
|
|
||||||
|
// check certificate
|
||||||
|
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the certificate against root certificates
|
||||||
|
* @param array $rootCas
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function validateRootCertificate($rootCas) {
|
||||||
|
$chainC = $this->_createX5cChainFile();
|
||||||
|
if ($chainC) {
|
||||||
|
$rootCas[] = $chainC;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||||
|
if ($v === -1) {
|
||||||
|
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,255 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\Binary;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/ByteBuffer.php
|
||||||
|
* Copyright © 2018 Thomas Bleeker - MIT licensed
|
||||||
|
* Modified by Lukas Buchs
|
||||||
|
* Thanks Thomas for your work!
|
||||||
|
*/
|
||||||
|
class ByteBuffer implements \JsonSerializable, \Serializable {
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public static $useBase64UrlEncoding = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $_data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $_length;
|
||||||
|
|
||||||
|
public function __construct($binaryData) {
|
||||||
|
$this->_data = $binaryData;
|
||||||
|
$this->_length = \strlen($binaryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// PUBLIC STATIC
|
||||||
|
// -----------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a ByteBuffer from a base64 url encoded string
|
||||||
|
* @param string $base64url
|
||||||
|
* @return \WebAuthn\Binary\ByteBuffer
|
||||||
|
*/
|
||||||
|
public static function fromBase64Url($base64url) {
|
||||||
|
$bin = self::_base64url_decode($base64url);
|
||||||
|
if ($bin === false) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid base64 url string', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return new ByteBuffer($bin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a ByteBuffer from a base64 url encoded string
|
||||||
|
* @param string $hex
|
||||||
|
* @return \WebAuthn\Binary\ByteBuffer
|
||||||
|
*/
|
||||||
|
public static function fromHex($hex) {
|
||||||
|
$bin = \hex2bin($hex);
|
||||||
|
if ($bin === false) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid hex string', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return new ByteBuffer($bin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a random ByteBuffer
|
||||||
|
* @param string $length
|
||||||
|
* @return \WebAuthn\Binary\ByteBuffer
|
||||||
|
*/
|
||||||
|
public static function randomBuffer($length) {
|
||||||
|
if (\function_exists('random_bytes')) { // >PHP 7.0
|
||||||
|
return new ByteBuffer(\random_bytes($length));
|
||||||
|
|
||||||
|
} else if (\function_exists('openssl_random_pseudo_bytes')) {
|
||||||
|
return new ByteBuffer(\openssl_random_pseudo_bytes($length));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new WebAuthnException('ByteBuffer: cannot generate random bytes', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// PUBLIC
|
||||||
|
// -----------------------
|
||||||
|
|
||||||
|
public function getBytes($offset, $length) {
|
||||||
|
if ($offset < 0 || $length < 0 || ($offset + $length > $this->_length)) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset or length', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return \substr($this->_data, $offset, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getByteVal($offset) {
|
||||||
|
if ($offset < 0 || $offset >= $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return \ord(\substr($this->_data, $offset, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLength() {
|
||||||
|
return $this->_length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUint16Val($offset) {
|
||||||
|
if ($offset < 0 || ($offset + 2) > $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return unpack('n', $this->_data, $offset)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUint32Val($offset) {
|
||||||
|
if ($offset < 0 || ($offset + 4) > $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
$val = unpack('N', $this->_data, $offset)[1];
|
||||||
|
|
||||||
|
// Signed integer overflow causes signed negative numbers
|
||||||
|
if ($val < 0) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUint64Val($offset) {
|
||||||
|
if (PHP_INT_SIZE < 8) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: 64-bit values not supported by this system', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
if ($offset < 0 || ($offset + 8) > $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
$val = unpack('J', $this->_data, $offset)[1];
|
||||||
|
|
||||||
|
// Signed integer overflow causes signed negative numbers
|
||||||
|
if ($val < 0) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHalfFloatVal($offset) {
|
||||||
|
//FROM spec pseudo decode_half(unsigned char *halfp)
|
||||||
|
$half = $this->getUint16Val($offset);
|
||||||
|
|
||||||
|
$exp = ($half >> 10) & 0x1f;
|
||||||
|
$mant = $half & 0x3ff;
|
||||||
|
|
||||||
|
if ($exp === 0) {
|
||||||
|
$val = $mant * (2 ** -24);
|
||||||
|
} elseif ($exp !== 31) {
|
||||||
|
$val = ($mant + 1024) * (2 ** ($exp - 25));
|
||||||
|
} else {
|
||||||
|
$val = ($mant === 0) ? INF : NAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($half & 0x8000) ? -$val : $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFloatVal($offset) {
|
||||||
|
if ($offset < 0 || ($offset + 4) > $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return unpack('G', $this->_data, $offset)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDoubleVal($offset) {
|
||||||
|
if ($offset < 0 || ($offset + 8) > $this->_length) {
|
||||||
|
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||||
|
}
|
||||||
|
return unpack('E', $this->_data, $offset)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getBinaryString() {
|
||||||
|
return $this->_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $buffer
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function equals($buffer) {
|
||||||
|
return is_string($this->_data) && $this->_data === $buffer->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getHex() {
|
||||||
|
return \bin2hex($this->_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isEmpty() {
|
||||||
|
return $this->_length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jsonSerialize interface
|
||||||
|
* return binary data in RFC 1342-Like serialized string
|
||||||
|
* @return \stdClass
|
||||||
|
*/
|
||||||
|
public function jsonSerialize() {
|
||||||
|
if (ByteBuffer::$useBase64UrlEncoding) {
|
||||||
|
return self::_base64url_encode($this->_data);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return '=?BINARY?B?' . \base64_encode($this->_data) . '?=';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializable-Interface
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function serialize() {
|
||||||
|
return \serialize($this->_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializable-Interface
|
||||||
|
* @param string $serialized
|
||||||
|
*/
|
||||||
|
public function unserialize($serialized) {
|
||||||
|
$this->_data = \unserialize($serialized);
|
||||||
|
$this->_length = \strlen($this->_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// PROTECTED STATIC
|
||||||
|
// -----------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* base64 url decoding
|
||||||
|
* @param string $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function _base64url_decode($data) {
|
||||||
|
return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* base64 url encoding
|
||||||
|
* @param string $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function _base64url_encode($data) {
|
||||||
|
return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '=');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,220 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace WebAuthn\CBOR;
|
||||||
|
use WebAuthn\WebAuthnException;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/CborDecoder.php
|
||||||
|
* Copyright © 2018 Thomas Bleeker - MIT licensed
|
||||||
|
* Modified by Lukas Buchs
|
||||||
|
* Thanks Thomas for your work!
|
||||||
|
*/
|
||||||
|
class CborDecoder {
|
||||||
|
const CBOR_MAJOR_UNSIGNED_INT = 0;
|
||||||
|
const CBOR_MAJOR_TEXT_STRING = 3;
|
||||||
|
const CBOR_MAJOR_FLOAT_SIMPLE = 7;
|
||||||
|
const CBOR_MAJOR_NEGATIVE_INT = 1;
|
||||||
|
const CBOR_MAJOR_ARRAY = 4;
|
||||||
|
const CBOR_MAJOR_TAG = 6;
|
||||||
|
const CBOR_MAJOR_MAP = 5;
|
||||||
|
const CBOR_MAJOR_BYTE_STRING = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ByteBuffer|string $bufOrBin
|
||||||
|
* @return mixed
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public static function decode($bufOrBin) {
|
||||||
|
$buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin);
|
||||||
|
|
||||||
|
$offset = 0;
|
||||||
|
$result = self::_parseItem($buf, $offset);
|
||||||
|
if ($offset !== $buf->getLength()) {
|
||||||
|
throw new WebAuthnException('Unused bytes after data item.', WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ByteBuffer|string $bufOrBin
|
||||||
|
* @param int $startOffset
|
||||||
|
* @param int|null $endOffset
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function decodeInPlace($bufOrBin, $startOffset, &$endOffset = null) {
|
||||||
|
$buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin);
|
||||||
|
|
||||||
|
$offset = $startOffset;
|
||||||
|
$data = self::_parseItem($buf, $offset);
|
||||||
|
$endOffset = $offset;
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// protected
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ByteBuffer $buf
|
||||||
|
* @param int $offset
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected static function _parseItem(ByteBuffer $buf, &$offset) {
|
||||||
|
$first = $buf->getByteVal($offset++);
|
||||||
|
$type = $first >> 5;
|
||||||
|
$val = $first & 0b11111;
|
||||||
|
|
||||||
|
if ($type === self::CBOR_MAJOR_FLOAT_SIMPLE) {
|
||||||
|
return self::_parseFloatSimple($val, $buf, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
$val = self::_parseExtraLength($val, $buf, $offset);
|
||||||
|
|
||||||
|
return self::_parseItemData($type, $val, $buf, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function _parseFloatSimple($val, ByteBuffer $buf, &$offset) {
|
||||||
|
switch ($val) {
|
||||||
|
case 24:
|
||||||
|
$val = $buf->getByteVal($offset);
|
||||||
|
$offset++;
|
||||||
|
return self::_parseSimple($val);
|
||||||
|
|
||||||
|
case 25:
|
||||||
|
$floatValue = $buf->getHalfFloatVal($offset);
|
||||||
|
$offset += 2;
|
||||||
|
return $floatValue;
|
||||||
|
|
||||||
|
case 26:
|
||||||
|
$floatValue = $buf->getFloatVal($offset);
|
||||||
|
$offset += 4;
|
||||||
|
return $floatValue;
|
||||||
|
|
||||||
|
case 27:
|
||||||
|
$floatValue = $buf->getDoubleVal($offset);
|
||||||
|
$offset += 8;
|
||||||
|
return $floatValue;
|
||||||
|
|
||||||
|
case 28:
|
||||||
|
case 29:
|
||||||
|
case 30:
|
||||||
|
throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR);
|
||||||
|
|
||||||
|
case 31:
|
||||||
|
throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::_parseSimple($val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $val
|
||||||
|
* @return mixed
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
protected static function _parseSimple($val) {
|
||||||
|
if ($val === 20) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($val === 21) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($val === 22) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new WebAuthnException(sprintf('Unsupported simple value %d.', $val), WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function _parseExtraLength($val, ByteBuffer $buf, &$offset) {
|
||||||
|
switch ($val) {
|
||||||
|
case 24:
|
||||||
|
$val = $buf->getByteVal($offset);
|
||||||
|
$offset++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 25:
|
||||||
|
$val = $buf->getUint16Val($offset);
|
||||||
|
$offset += 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 26:
|
||||||
|
$val = $buf->getUint32Val($offset);
|
||||||
|
$offset += 4;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 27:
|
||||||
|
$val = $buf->getUint64Val($offset);
|
||||||
|
$offset += 8;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 28:
|
||||||
|
case 29:
|
||||||
|
case 30:
|
||||||
|
throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR);
|
||||||
|
|
||||||
|
case 31:
|
||||||
|
throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function _parseItemData($type, $val, ByteBuffer $buf, &$offset) {
|
||||||
|
switch ($type) {
|
||||||
|
case self::CBOR_MAJOR_UNSIGNED_INT: // uint
|
||||||
|
return $val;
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_NEGATIVE_INT:
|
||||||
|
return -1 - $val;
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_BYTE_STRING:
|
||||||
|
$data = $buf->getBytes($offset, $val);
|
||||||
|
$offset += $val;
|
||||||
|
return new ByteBuffer($data); // bytes
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_TEXT_STRING:
|
||||||
|
$data = $buf->getBytes($offset, $val);
|
||||||
|
$offset += $val;
|
||||||
|
return $data; // UTF-8
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_ARRAY:
|
||||||
|
return self::_parseArray($buf, $offset, $val);
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_MAP:
|
||||||
|
return self::_parseMap($buf, $offset, $val);
|
||||||
|
|
||||||
|
case self::CBOR_MAJOR_TAG:
|
||||||
|
return self::_parseItem($buf, $offset); // 1 embedded data item
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should never be reached
|
||||||
|
throw new WebAuthnException(sprintf('Unknown major type %d.', $type), WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function _parseMap(ByteBuffer $buf, &$offset, $count) {
|
||||||
|
$map = array();
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$mapKey = self::_parseItem($buf, $offset);
|
||||||
|
$mapVal = self::_parseItem($buf, $offset);
|
||||||
|
|
||||||
|
if (!\is_int($mapKey) && !\is_string($mapKey)) {
|
||||||
|
throw new WebAuthnException('Can only use strings or integers as map keys', WebAuthnException::CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
$map[$mapKey] = $mapVal; // todo dup
|
||||||
|
}
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function _parseArray(ByteBuffer $buf, &$offset, $count) {
|
||||||
|
$arr = array();
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$arr[] = self::_parseItem($buf, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arr;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright © 2019 Lukas Buchs
|
||||||
|
Copyright © 2018 Thomas Bleeker (CBOR & ByteBuffer part)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,487 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WebAuthn;
|
||||||
|
use WebAuthn\Binary\ByteBuffer;
|
||||||
|
require_once 'WebAuthnException.php';
|
||||||
|
require_once 'Binary/ByteBuffer.php';
|
||||||
|
require_once 'Attestation/AttestationObject.php';
|
||||||
|
require_once 'Attestation/AuthenticatorData.php';
|
||||||
|
require_once 'Attestation/Format/FormatBase.php';
|
||||||
|
require_once 'Attestation/Format/None.php';
|
||||||
|
require_once 'Attestation/Format/AndroidKey.php';
|
||||||
|
require_once 'Attestation/Format/AndroidSafetyNet.php';
|
||||||
|
require_once 'Attestation/Format/Packed.php';
|
||||||
|
require_once 'Attestation/Format/Tpm.php';
|
||||||
|
require_once 'Attestation/Format/U2f.php';
|
||||||
|
require_once 'CBOR/CborDecoder.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebAuthn
|
||||||
|
* @author Lukas Buchs
|
||||||
|
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||||
|
*/
|
||||||
|
class WebAuthn {
|
||||||
|
// relying party
|
||||||
|
private $_rpName;
|
||||||
|
private $_rpId;
|
||||||
|
private $_rpIdHash;
|
||||||
|
private $_challenge;
|
||||||
|
private $_signatureCounter;
|
||||||
|
private $_caFiles;
|
||||||
|
private $_formats;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a new WebAuthn server
|
||||||
|
* @param string $rpName the relying party name
|
||||||
|
* @param string $rpId the relying party ID = the domain name
|
||||||
|
* @param bool $useBase64UrlEncoding true to use base64 url encoding for binary data in json objects. Default is a RFC 1342-Like serialized string.
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function __construct($rpName, $rpId, $allowedFormats=null, $useBase64UrlEncoding=false) {
|
||||||
|
$this->_rpName = $rpName;
|
||||||
|
$this->_rpId = $rpId;
|
||||||
|
$this->_rpIdHash = \hash('sha256', $rpId, true);
|
||||||
|
ByteBuffer::$useBase64UrlEncoding = !!$useBase64UrlEncoding;
|
||||||
|
|
||||||
|
if (!\function_exists('\openssl_open')) {
|
||||||
|
throw new WebAuthnException('OpenSSL-Module not installed');;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\in_array('SHA256', \array_map('\strtoupper', \openssl_get_md_methods()))) {
|
||||||
|
throw new WebAuthnException('SHA256 not supported by this openssl installation.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// default value
|
||||||
|
if (!is_array($allowedFormats)) {
|
||||||
|
$allowedFormats = array('android-key', 'fido-u2f', 'packed', 'tpm');
|
||||||
|
}
|
||||||
|
$this->_formats = $allowedFormats;
|
||||||
|
|
||||||
|
// validate formats
|
||||||
|
$invalidFormats = \array_diff($this->_formats, array('android-key', 'android-safetynet', 'fido-u2f', 'none', 'packed', 'tpm'));
|
||||||
|
if (!$this->_formats || $invalidFormats) {
|
||||||
|
throw new WebAuthnException('Invalid formats on construct: ' . implode(', ', $invalidFormats));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add a root certificate to verify new registrations
|
||||||
|
* @param string $path file path of / directory with root certificates
|
||||||
|
*/
|
||||||
|
public function addRootCertificates($path) {
|
||||||
|
if (!\is_array($this->_caFiles)) {
|
||||||
|
$this->_caFiles = array();
|
||||||
|
}
|
||||||
|
$path = \rtrim(\trim($path), '\\/');
|
||||||
|
if (\is_dir($path)) {
|
||||||
|
foreach (\scandir($path) as $ca) {
|
||||||
|
if (\is_file($path . '/' . $ca)) {
|
||||||
|
$this->addRootCertificates($path . '/' . $ca);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (\is_file($path) && !\in_array(\realpath($path), $this->_caFiles)) {
|
||||||
|
$this->_caFiles[] = \realpath($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the generated challenge to save for later validation
|
||||||
|
* @return ByteBuffer
|
||||||
|
*/
|
||||||
|
public function getChallenge() {
|
||||||
|
return $this->_challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generates the object for a key registration
|
||||||
|
* provide this data to navigator.credentials.create
|
||||||
|
* @param string $userId
|
||||||
|
* @param string $userName
|
||||||
|
* @param string $userDisplayName
|
||||||
|
* @param int $timeout timeout in seconds
|
||||||
|
* @param bool $requireResidentKey true, if the key should be stored by the authentication device
|
||||||
|
* @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation
|
||||||
|
* if the response does not have the UV flag set.
|
||||||
|
* Valid values:
|
||||||
|
* true = required
|
||||||
|
* false = preferred
|
||||||
|
* string 'required' 'preferred' 'discouraged'
|
||||||
|
* @param array $excludeCredentialIds a array of ids, which are already registered, to prevent re-registration
|
||||||
|
* @return \stdClass
|
||||||
|
*/
|
||||||
|
public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20, $requireResidentKey=false, $requireUserVerification=false, $excludeCredentialIds=array()) {
|
||||||
|
|
||||||
|
// validate User Verification Requirement
|
||||||
|
if (\is_bool($requireUserVerification)) {
|
||||||
|
$requireUserVerification = $requireUserVerification ? 'required' : 'preferred';
|
||||||
|
} else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) {
|
||||||
|
$requireUserVerification = \strtolower($requireUserVerification);
|
||||||
|
} else {
|
||||||
|
$requireUserVerification = 'preferred';
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = new \stdClass();
|
||||||
|
$args->publicKey = new \stdClass();
|
||||||
|
|
||||||
|
// relying party
|
||||||
|
$args->publicKey->rp = new \stdClass();
|
||||||
|
$args->publicKey->rp->name = $this->_rpName;
|
||||||
|
$args->publicKey->rp->id = $this->_rpId;
|
||||||
|
|
||||||
|
$args->publicKey->authenticatorSelection = new \stdClass();
|
||||||
|
$args->publicKey->authenticatorSelection->userVerification = $requireUserVerification;
|
||||||
|
if ($requireResidentKey) {
|
||||||
|
$args->publicKey->authenticatorSelection->requireResidentKey = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// user
|
||||||
|
$args->publicKey->user = new \stdClass();
|
||||||
|
$args->publicKey->user->id = new ByteBuffer($userId); // binary
|
||||||
|
$args->publicKey->user->name = $userName;
|
||||||
|
$args->publicKey->user->displayName = $userDisplayName;
|
||||||
|
|
||||||
|
$args->publicKey->pubKeyCredParams = array();
|
||||||
|
$tmp = new \stdClass();
|
||||||
|
$tmp->type = 'public-key';
|
||||||
|
$tmp->alg = -7; // ES256
|
||||||
|
$args->publicKey->pubKeyCredParams[] = $tmp;
|
||||||
|
unset ($tmp);
|
||||||
|
|
||||||
|
$tmp = new \stdClass();
|
||||||
|
$tmp->type = 'public-key';
|
||||||
|
$tmp->alg = -257; // RS256
|
||||||
|
$args->publicKey->pubKeyCredParams[] = $tmp;
|
||||||
|
unset ($tmp);
|
||||||
|
|
||||||
|
// if there are root certificates added, we need direct attestation to validate
|
||||||
|
// against the root certificate. If there are no root-certificates added,
|
||||||
|
// anonymization ca are also accepted, because we can't validate the root anyway.
|
||||||
|
$attestation = 'indirect';
|
||||||
|
if (\is_array($this->_caFiles)) {
|
||||||
|
$attestation = 'direct';
|
||||||
|
}
|
||||||
|
|
||||||
|
$args->publicKey->attestation = \count($this->_formats) === 1 && \in_array('none', $this->_formats) ? 'none' : $attestation;
|
||||||
|
$args->publicKey->extensions = new \stdClass();
|
||||||
|
$args->publicKey->extensions->exts = true;
|
||||||
|
$args->publicKey->timeout = $timeout * 1000; // microseconds
|
||||||
|
$args->publicKey->challenge = $this->_createChallenge(); // binary
|
||||||
|
|
||||||
|
//prevent re-registration by specifying existing credentials
|
||||||
|
$args->publicKey->excludeCredentials = array();
|
||||||
|
|
||||||
|
if (is_array($excludeCredentialIds)) {
|
||||||
|
foreach ($excludeCredentialIds as $id) {
|
||||||
|
$tmp = new \stdClass();
|
||||||
|
$tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary
|
||||||
|
$tmp->type = 'public-key';
|
||||||
|
$tmp->transports = array('usb', 'ble', 'nfc', 'internal');
|
||||||
|
$args->publicKey->excludeCredentials[] = $tmp;
|
||||||
|
unset ($tmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generates the object for key validation
|
||||||
|
* Provide this data to navigator.credentials.get
|
||||||
|
* @param array $credentialIds binary
|
||||||
|
* @param int $timeout timeout in seconds
|
||||||
|
* @param bool $allowUsb allow removable USB
|
||||||
|
* @param bool $allowNfc allow Near Field Communication (NFC)
|
||||||
|
* @param bool $allowBle allow Bluetooth
|
||||||
|
* @param bool $allowInternal allow client device-specific transport. These authenticators are not removable from the client device.
|
||||||
|
* @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation
|
||||||
|
* if the response does not have the UV flag set.
|
||||||
|
* Valid values:
|
||||||
|
* true = required
|
||||||
|
* false = preferred
|
||||||
|
* string 'required' 'preferred' 'discouraged'
|
||||||
|
* @return \stdClass
|
||||||
|
*/
|
||||||
|
public function getGetArgs($credentialIds=array(), $timeout=20, $allowUsb=true, $allowNfc=true, $allowBle=true, $allowInternal=true, $requireUserVerification=false) {
|
||||||
|
|
||||||
|
// validate User Verification Requirement
|
||||||
|
if (\is_bool($requireUserVerification)) {
|
||||||
|
$requireUserVerification = $requireUserVerification ? 'required' : 'preferred';
|
||||||
|
} else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) {
|
||||||
|
$requireUserVerification = \strtolower($requireUserVerification);
|
||||||
|
} else {
|
||||||
|
$requireUserVerification = 'preferred';
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = new \stdClass();
|
||||||
|
$args->publicKey = new \stdClass();
|
||||||
|
$args->publicKey->timeout = $timeout * 1000; // microseconds
|
||||||
|
$args->publicKey->challenge = $this->_createChallenge(); // binary
|
||||||
|
$args->publicKey->userVerification = $requireUserVerification;
|
||||||
|
$args->publicKey->rpId = $this->_rpId;
|
||||||
|
|
||||||
|
if (\is_array($credentialIds) && \count($credentialIds) > 0) {
|
||||||
|
$args->publicKey->allowCredentials = array();
|
||||||
|
|
||||||
|
foreach ($credentialIds as $id) {
|
||||||
|
$tmp = new \stdClass();
|
||||||
|
$tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary
|
||||||
|
$tmp->transports = array();
|
||||||
|
|
||||||
|
if ($allowUsb) {
|
||||||
|
$tmp->transports[] = 'usb';
|
||||||
|
}
|
||||||
|
if ($allowNfc) {
|
||||||
|
$tmp->transports[] = 'nfc';
|
||||||
|
}
|
||||||
|
if ($allowBle) {
|
||||||
|
$tmp->transports[] = 'ble';
|
||||||
|
}
|
||||||
|
if ($allowInternal) {
|
||||||
|
$tmp->transports[] = 'internal';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp->type = 'public-key';
|
||||||
|
$args->publicKey->allowCredentials[] = $tmp;
|
||||||
|
unset ($tmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the new signature counter value.
|
||||||
|
* returns null if there is no counter
|
||||||
|
* @return ?int
|
||||||
|
*/
|
||||||
|
public function getSignatureCounter() {
|
||||||
|
return \is_int($this->_signatureCounter) ? $this->_signatureCounter : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process a create request and returns data to save for future logins
|
||||||
|
* @param string $clientDataJSON binary from browser
|
||||||
|
* @param string $attestationObject binary from browser
|
||||||
|
* @param string|ByteBuffer $challenge binary used challange
|
||||||
|
* @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin)
|
||||||
|
* @param bool $requireUserPresent false, if the device must NOT check user presence (e.g. by pressing a button)
|
||||||
|
* @return \stdClass
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true) {
|
||||||
|
$clientDataHash = \hash('sha256', $clientDataJSON, true);
|
||||||
|
$clientData = \json_decode($clientDataJSON);
|
||||||
|
$challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge);
|
||||||
|
|
||||||
|
// security: https://www.w3.org/TR/webauthn/#registering-a-new-credential
|
||||||
|
|
||||||
|
// 2. Let C, the client data claimed as collected during the credential creation,
|
||||||
|
// be the result of running an implementation-specific JSON parser on JSONtext.
|
||||||
|
if (!\is_object($clientData)) {
|
||||||
|
throw new WebAuthnException('Invalid client data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Verify that the value of C.type is webauthn.create.
|
||||||
|
if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.create') {
|
||||||
|
throw new WebAuthnException('Invalid type', WebAuthnException::INVALID_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.
|
||||||
|
if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
|
||||||
|
throw new WebAuthnException('Invalid challenge', WebAuthnException::INVALID_CHALLENGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Verify that the value of C.origin matches the Relying Party's origin.
|
||||||
|
if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
|
||||||
|
throw new WebAuthnException('Invalid origin', WebAuthnException::INVALID_ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attestation
|
||||||
|
$attestationObject = new Attestation\AttestationObject($attestationObject, $this->_formats);
|
||||||
|
|
||||||
|
// 9. Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP.
|
||||||
|
if (!$attestationObject->validateRpIdHash($this->_rpIdHash)) {
|
||||||
|
throw new WebAuthnException('Invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature
|
||||||
|
if (!$attestationObject->validateAttestation($clientDataHash)) {
|
||||||
|
throw new WebAuthnException('Invalid certificate signature', WebAuthnException::INVALID_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 15. If validation is successful, obtain a list of acceptable trust anchors
|
||||||
|
if (is_array($this->_caFiles) && !$attestationObject->validateRootCertificate($this->_caFiles)) {
|
||||||
|
throw new WebAuthnException('Invalid root certificate', WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Verify that the User Present bit of the flags in authData is set.
|
||||||
|
if ($requireUserPresent && !$attestationObject->getAuthenticatorData()->getUserPresent()) {
|
||||||
|
throw new WebAuthnException('User not present during authentication', WebAuthnException::USER_PRESENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
|
||||||
|
if ($requireUserVerification && !$attestationObject->getAuthenticatorData()->getUserVerified()) {
|
||||||
|
throw new WebAuthnException('User not verificated during authentication', WebAuthnException::USER_VERIFICATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
$signCount = $attestationObject->getAuthenticatorData()->getSignCount();
|
||||||
|
if ($signCount > 0) {
|
||||||
|
$this->_signatureCounter = $signCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare data to store for future logins
|
||||||
|
$data = new \stdClass();
|
||||||
|
$data->rpId = $this->_rpId;
|
||||||
|
$data->credentialId = $attestationObject->getAuthenticatorData()->getCredentialId();
|
||||||
|
$data->credentialPublicKey = $attestationObject->getAuthenticatorData()->getPublicKeyPem();
|
||||||
|
$data->certificateChain = $attestationObject->getCertificateChain();
|
||||||
|
$data->certificate = $attestationObject->getCertificatePem();
|
||||||
|
$data->certificateIssuer = $attestationObject->getCertificateIssuer();
|
||||||
|
$data->certificateSubject = $attestationObject->getCertificateSubject();
|
||||||
|
$data->signatureCounter = $this->_signatureCounter;
|
||||||
|
$data->AAGUID = $attestationObject->getAuthenticatorData()->getAAGUID();
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process a get request
|
||||||
|
* @param string $clientDataJSON binary from browser
|
||||||
|
* @param string $authenticatorData binary from browser
|
||||||
|
* @param string $signature binary from browser
|
||||||
|
* @param string $credentialPublicKey string PEM-formated public key from used credentialId
|
||||||
|
* @param string|ByteBuffer $challenge binary from used challange
|
||||||
|
* @param int $prevSignatureCnt signature count value of the last login
|
||||||
|
* @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin)
|
||||||
|
* @param bool $requireUserPresent true, if the device must check user presence (e.g. by pressing a button)
|
||||||
|
* @return boolean true if get is successful
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
public function processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, $prevSignatureCnt=null, $requireUserVerification=false, $requireUserPresent=true) {
|
||||||
|
$authenticatorObj = new Attestation\AuthenticatorData($authenticatorData);
|
||||||
|
$clientDataHash = \hash('sha256', $clientDataJSON, true);
|
||||||
|
$clientData = \json_decode($clientDataJSON);
|
||||||
|
$challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge);
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/webauthn/#verifying-assertion
|
||||||
|
|
||||||
|
// 1. If the allowCredentials option was given when this authentication ceremony was initiated,
|
||||||
|
// verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.
|
||||||
|
// -> TO BE VERIFIED BY IMPLEMENTATION
|
||||||
|
|
||||||
|
// 2. If credential.response.userHandle is present, verify that the user identified
|
||||||
|
// by this value is the owner of the public key credential identified by credential.id.
|
||||||
|
// -> TO BE VERIFIED BY IMPLEMENTATION
|
||||||
|
|
||||||
|
// 3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is
|
||||||
|
// inappropriate for your use case), look up the corresponding credential public key.
|
||||||
|
// -> TO BE LOOKED UP BY IMPLEMENTATION
|
||||||
|
|
||||||
|
// 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
|
||||||
|
if (!\is_object($clientData)) {
|
||||||
|
throw new WebAuthnException('Invalid client data', WebAuthnException::INVALID_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Verify that the value of C.type is the string webauthn.get.
|
||||||
|
if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.get') {
|
||||||
|
throw new WebAuthnException('Invalid type', WebAuthnException::INVALID_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Verify that the value of C.challenge matches the challenge that was sent to the
|
||||||
|
// authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.
|
||||||
|
if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
|
||||||
|
throw new WebAuthnException('Invalid challenge', WebAuthnException::INVALID_CHALLENGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Verify that the value of C.origin matches the Relying Party's origin.
|
||||||
|
if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
|
||||||
|
throw new WebAuthnException('Invalid origin', WebAuthnException::INVALID_ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
|
||||||
|
if ($authenticatorObj->getRpIdHash() !== $this->_rpIdHash) {
|
||||||
|
throw new WebAuthnException('Invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Verify that the User Present bit of the flags in authData is set
|
||||||
|
if ($requireUserPresent && !$authenticatorObj->getUserPresent()) {
|
||||||
|
throw new WebAuthnException('User not present during authentication', WebAuthnException::USER_PRESENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 13. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.
|
||||||
|
if ($requireUserVerification && !$authenticatorObj->getUserVerified()) {
|
||||||
|
throw new WebAuthnException('User not verificated during authentication', WebAuthnException::USER_VERIFICATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 14. Verify the values of the client extension outputs
|
||||||
|
// (extensions not implemented)
|
||||||
|
|
||||||
|
// 16. Using the credential public key looked up in step 3, verify that sig is a valid signature
|
||||||
|
// over the binary concatenation of authData and hash.
|
||||||
|
$dataToVerify = '';
|
||||||
|
$dataToVerify .= $authenticatorData;
|
||||||
|
$dataToVerify .= $clientDataHash;
|
||||||
|
|
||||||
|
$publicKey = \openssl_pkey_get_public($credentialPublicKey);
|
||||||
|
if ($publicKey === false) {
|
||||||
|
throw new WebAuthnException('public key invalid', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256) !== 1) {
|
||||||
|
throw new WebAuthnException('Invalid signature', WebAuthnException::INVALID_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 17. If the signature counter value authData.signCount is nonzero,
|
||||||
|
// if less than or equal to the signature counter value stored,
|
||||||
|
// is a signal that the authenticator may be cloned
|
||||||
|
$signatureCounter = $authenticatorObj->getSignCount();
|
||||||
|
if ($signatureCounter > 0) {
|
||||||
|
$this->_signatureCounter = $signatureCounter;
|
||||||
|
if ($prevSignatureCnt !== null && $prevSignatureCnt >= $signatureCounter) {
|
||||||
|
throw new WebAuthnException('signature counter not valid', WebAuthnException::SIGNATURE_COUNTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// PRIVATE
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if the origin matchs the RP ID
|
||||||
|
* @param string $origin
|
||||||
|
* @return boolean
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _checkOrigin($origin) {
|
||||||
|
// https://www.w3.org/TR/webauthn/#rp-id
|
||||||
|
|
||||||
|
// The origin's scheme must be https
|
||||||
|
if ($this->_rpId !== 'localhost' && \parse_url($origin, PHP_URL_SCHEME) !== 'https') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract host from origin
|
||||||
|
$host = \parse_url($origin, PHP_URL_HOST);
|
||||||
|
$host = \trim($host, '.');
|
||||||
|
|
||||||
|
// The RP ID must be equal to the origin's effective domain, or a registrable
|
||||||
|
// domain suffix of the origin's effective domain.
|
||||||
|
return \preg_match('/' . \preg_quote($this->_rpId) . '$/i', $host) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generates a new challange
|
||||||
|
* @param int $length
|
||||||
|
* @return string
|
||||||
|
* @throws WebAuthnException
|
||||||
|
*/
|
||||||
|
private function _createChallenge($length = 32) {
|
||||||
|
if (!$this->_challenge) {
|
||||||
|
$this->_challenge = ByteBuffer::randomBuffer($length);
|
||||||
|
}
|
||||||
|
return $this->_challenge;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
namespace WebAuthn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Lukas Buchs
|
||||||
|
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||||
|
*/
|
||||||
|
class WebAuthnException extends \Exception {
|
||||||
|
const INVALID_DATA = 1;
|
||||||
|
const INVALID_TYPE = 2;
|
||||||
|
const INVALID_CHALLENGE = 3;
|
||||||
|
const INVALID_ORIGIN = 4;
|
||||||
|
const INVALID_RELYING_PARTY = 5;
|
||||||
|
const INVALID_SIGNATURE = 6;
|
||||||
|
const INVALID_PUBLIC_KEY = 7;
|
||||||
|
const CERTIFICATE_NOT_TRUSTED = 8;
|
||||||
|
const USER_PRESENT = 9;
|
||||||
|
const USER_VERIFICATED = 10;
|
||||||
|
const SIGNATURE_COUNTER = 11;
|
||||||
|
const CRYPTO_STRONG = 13;
|
||||||
|
const BYTEBUFFER = 14;
|
||||||
|
const CBOR = 15;
|
||||||
|
|
||||||
|
public function __construct($message = "", $code = 0, $previous = null) {
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
68:1d:01:6c:7a:3c:e3:02:25:a5:01:94:28:47:57:71
|
||||||
|
|
||||||
|
Signature Algorithm: ecdsa-with-SHA384
|
||||||
|
|
||||||
|
Issuer:
|
||||||
|
stateOrProvinceName = California
|
||||||
|
organizationName = Apple Inc.
|
||||||
|
commonName = Apple WebAuthn Root CA
|
||||||
|
|
||||||
|
Validity
|
||||||
|
Not Before: Mar 18 18:21:32 2020 GMT
|
||||||
|
Not After : Mar 15 00:00:00 2045 GMT
|
||||||
|
|
||||||
|
Subject:
|
||||||
|
stateOrProvinceName = California
|
||||||
|
organizationName = Apple Inc.
|
||||||
|
commonName = Apple WebAuthn Root CA
|
||||||
|
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: id-ecPublicKey
|
||||||
|
ASN1 OID: secp384r1
|
||||||
|
|
||||||
|
X509v3 extensions:
|
||||||
|
X509v3 Basic Constraints: critical
|
||||||
|
CA:TRUE
|
||||||
|
X509v3 Subject Key Identifier:
|
||||||
|
26:D7:64:D9:C5:78:C2:5A:67:D1:A7:DE:6B:12:D0:1B:63:F1:C6:D7
|
||||||
|
X509v3 Key Usage: critical
|
||||||
|
Certificate Sign, CRL Sign
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
|
||||||
|
HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
|
||||||
|
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
|
||||||
|
NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
|
||||||
|
A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
|
||||||
|
AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
|
||||||
|
xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
|
||||||
|
pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
|
||||||
|
2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
|
||||||
|
MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
|
||||||
|
jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
|
||||||
|
1bWeT0vT
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,37 @@
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
04:00:00:00:00:01:0f:86:26:e6:0d
|
||||||
|
Signature Algorithm: sha1WithRSAEncryption
|
||||||
|
Issuer: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
|
||||||
|
Validity
|
||||||
|
Not Before: Dec 15 08:00:00 2006 GMT
|
||||||
|
Not After : Dec 15 08:00:00 2021 GMT
|
||||||
|
Subject: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: rsaEncryption
|
||||||
|
Public-Key: (2048 bit)
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
|
||||||
|
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
|
||||||
|
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
|
||||||
|
MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
|
||||||
|
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
|
||||||
|
hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
|
||||||
|
v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
|
||||||
|
eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
|
||||||
|
tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
|
||||||
|
C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
|
||||||
|
zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
|
||||||
|
mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
|
||||||
|
V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
|
||||||
|
bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
|
||||||
|
3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
|
||||||
|
J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
|
||||||
|
291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
|
||||||
|
ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
|
||||||
|
AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
|
||||||
|
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,130 @@
|
||||||
|
Google Hardware Attestation Root certificate
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
https://developer.android.com/training/articles/security-key-attestation.html
|
||||||
|
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
e8:fa:19:63:14:d2:fa:18
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
Issuer: serialNumber = f92009e853b6b045
|
||||||
|
Validity
|
||||||
|
Not Before: May 26 16:28:52 2016 GMT
|
||||||
|
Not After : May 24 16:28:52 2026 GMT
|
||||||
|
Subject: serialNumber = f92009e853b6b045
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: rsaEncryption
|
||||||
|
Public-Key: (4096 bit)
|
||||||
|
Exponent: 65537 (0x10001)
|
||||||
|
X509v3 extensions:
|
||||||
|
X509v3 Subject Key Identifier:
|
||||||
|
36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||||
|
X509v3 Authority Key Identifier:
|
||||||
|
keyid:36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||||
|
|
||||||
|
X509v3 Basic Constraints: critical
|
||||||
|
CA:TRUE
|
||||||
|
X509v3 Key Usage: critical
|
||||||
|
Digital Signature, Certificate Sign, CRL Sign
|
||||||
|
X509v3 CRL Distribution Points:
|
||||||
|
|
||||||
|
Full Name:
|
||||||
|
URI:https://android.googleapis.com/attestation/crl/
|
||||||
|
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number: 15352756130135856819 (0xd50ff25ba3f2d6b3)
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
Issuer:
|
||||||
|
serialNumber = f92009e853b6b045
|
||||||
|
Validity
|
||||||
|
Not Before: Nov 22 20:37:58 2019 GMT
|
||||||
|
Not After : Nov 18 20:37:58 2034 GMT
|
||||||
|
Subject:
|
||||||
|
serialNumber = f92009e853b6b045
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: rsaEncryption
|
||||||
|
Public-Key: (4096 bit)
|
||||||
|
Exponent: 65537 (0x10001)
|
||||||
|
X509v3 extensions:
|
||||||
|
X509v3 Subject Key Identifier:
|
||||||
|
36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||||
|
X509v3 Authority Key Identifier:
|
||||||
|
keyid:36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||||
|
|
||||||
|
X509v3 Basic Constraints: critical
|
||||||
|
CA:TRUE
|
||||||
|
X509v3 Key Usage: critical
|
||||||
|
Certificate Sign
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
ex0SdDrx+tWUDqG8At2JHA==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,31 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,56 @@
|
||||||
|
HyperFIDO U2F Security Key Attestation CA
|
||||||
|
https://hypersecu.com/support/downloads/attestation
|
||||||
|
|
||||||
|
Last Update: 2017-01-01
|
||||||
|
|
||||||
|
HyperFIDO U2F Security Key devices which contain attestation certificates signed by a set of CAs.
|
||||||
|
This file contains the CA certificates that Relying Parties (RP) need to configure their software
|
||||||
|
with to be able to verify U2F device certificates.
|
||||||
|
|
||||||
|
The file will be updated as needed when we publish more CA certificates.
|
||||||
|
|
||||||
|
Issuer: CN=FT FIDO 0100
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBjTCCATOgAwIBAgIBATAKBggqhkjOPQQDAjAXMRUwEwYDVQQDEwxGVCBGSURP
|
||||||
|
IDAxMDAwHhcNMTQwNzAxMTUzNjI2WhcNNDQwNzAzMTUzNjI2WjAXMRUwEwYDVQQD
|
||||||
|
EwxGVCBGSURPIDAxMDAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASxdLxJx8ol
|
||||||
|
S3DS5cIHzunPF0gg69d+o8ZVCMJtpRtlfBzGuVL4YhaXk2SC2gptPTgmpZCV2vbN
|
||||||
|
fAPi5gOF0vbZo3AwbjAdBgNVHQ4EFgQUXt4jWlYDgwhaPU+EqLmeM9LoPRMwPwYD
|
||||||
|
VR0jBDgwNoAUXt4jWlYDgwhaPU+EqLmeM9LoPROhG6QZMBcxFTATBgNVBAMTDEZU
|
||||||
|
IEZJRE8gMDEwMIIBATAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQC2
|
||||||
|
D9o9cconKTo8+4GZPyZBJ3amc8F0/kzyidX9dhrAIAIgM9ocs5BW/JfmshVP9Mb+
|
||||||
|
Joa/kgX4dWbZxrk0ioTfJZg=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
|
||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number: 4107 (0x100b)
|
||||||
|
Signature Algorithm: ecdsa-with-SHA256
|
||||||
|
Issuer:
|
||||||
|
commonName = HYPERFIDO 0200
|
||||||
|
organizationName = HYPERSECU
|
||||||
|
countryName = CA
|
||||||
|
Validity
|
||||||
|
Not Before: Jan 1 00:00:00 2018 GMT
|
||||||
|
Not After : Dec 31 23:59:59 2047 GMT
|
||||||
|
Subject:
|
||||||
|
commonName = HYPERFIDO 0200
|
||||||
|
organizationName = HYPERSECU
|
||||||
|
countryName = CA
|
||||||
|
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBxzCCAWygAwIBAgICEAswCgYIKoZIzj0EAwIwOjELMAkGA1UEBhMCQ0ExEjAQ
|
||||||
|
BgNVBAoMCUhZUEVSU0VDVTEXMBUGA1UEAwwOSFlQRVJGSURPIDAyMDAwIBcNMTgw
|
||||||
|
MTAxMDAwMDAwWhgPMjA0NzEyMzEyMzU5NTlaMDoxCzAJBgNVBAYTAkNBMRIwEAYD
|
||||||
|
VQQKDAlIWVBFUlNFQ1UxFzAVBgNVBAMMDkhZUEVSRklETyAwMjAwMFkwEwYHKoZI
|
||||||
|
zj0CAQYIKoZIzj0DAQcDQgAErKUI1G0S7a6IOLlmHipLlBuxTYjsEESQvzQh3dB7
|
||||||
|
dvxxWWm7kWL91rq6S7ayZG0gZPR+zYqdFzwAYDcG4+aX66NgMF4wHQYDVR0OBBYE
|
||||||
|
FLZYcfMMwkQAGbt3ryzZFPFypmsIMB8GA1UdIwQYMBaAFLZYcfMMwkQAGbt3ryzZ
|
||||||
|
FPFypmsIMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMC
|
||||||
|
A0kAMEYCIQCG2/ppMGt7pkcRie5YIohS3uDPIrmiRcTjqDclKVWg0gIhANcPNDZH
|
||||||
|
E2/zZ+uB5ThG9OZus+xSb4knkrbAyXKX2zm/
|
||||||
|
-----END CERTIFICATE-----
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,41 @@
|
||||||
|
Solokeys FIDO2/U2F Device Attestation CA
|
||||||
|
========================================
|
||||||
|
Data:
|
||||||
|
Version: 1 (0x0)
|
||||||
|
Serial Number: 14143382635911888524 (0xc44763928ff4be8c)
|
||||||
|
Signature Algorithm: ecdsa-with-SHA256
|
||||||
|
|
||||||
|
Issuer:
|
||||||
|
emailAddress = hello@solokeys.com
|
||||||
|
commonName = solokeys.com
|
||||||
|
organizationalUnitName = Root CA
|
||||||
|
organizationName = Solo Keys
|
||||||
|
stateOrProvinceName = Maryland
|
||||||
|
countryName = US
|
||||||
|
|
||||||
|
Validity
|
||||||
|
Not Before: Nov 11 12:51:42 2018 GMT
|
||||||
|
Not After : Oct 29 12:51:42 2068 GMT
|
||||||
|
|
||||||
|
Subject:
|
||||||
|
emailAddress = hello@solokeys.com
|
||||||
|
commonName = solokeys.com
|
||||||
|
organizationalUnitName = Root CA
|
||||||
|
organizationName = Solo Keys
|
||||||
|
stateOrProvinceName = Maryland
|
||||||
|
countryName = US
|
||||||
|
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIB9DCCAZoCCQDER2OSj/S+jDAKBggqhkjOPQQDAjCBgDELMAkGA1UEBhMCVVMx
|
||||||
|
ETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQKDAlTb2xvIEtleXMxEDAOBgNVBAsM
|
||||||
|
B1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlzLmNvbTEhMB8GCSqGSIb3DQEJARYS
|
||||||
|
aGVsbG9Ac29sb2tleXMuY29tMCAXDTE4MTExMTEyNTE0MloYDzIwNjgxMDI5MTI1
|
||||||
|
MTQyWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQK
|
||||||
|
DAlTb2xvIEtleXMxEDAOBgNVBAsMB1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlz
|
||||||
|
LmNvbTEhMB8GCSqGSIb3DQEJARYSaGVsbG9Ac29sb2tleXMuY29tMFkwEwYHKoZI
|
||||||
|
zj0CAQYIKoZIzj0DAQcDQgAEWHAN0CCJVZdMs0oktZ5m93uxmB1iyq8ELRLtqVFL
|
||||||
|
SOiHQEab56qRTB/QzrpGAY++Y2mw+vRuQMNhBiU0KzwjBjAKBggqhkjOPQQDAgNI
|
||||||
|
ADBFAiEAz9SlrAXIlEu87vra54rICPs+4b0qhp3PdzcTg7rvnP0CIGjxzlteQQx+
|
||||||
|
jQGd7rwSZuE5RWUPVygYhUstQO9zNUOs
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,42 @@
|
||||||
|
Yubico U2F Device Attestation CA
|
||||||
|
================================
|
||||||
|
|
||||||
|
Last Update: 2014-09-01
|
||||||
|
|
||||||
|
Yubico manufacturer U2F devices that contains device attestation
|
||||||
|
certificates signed by a set of Yubico CAs. This file contains the CA
|
||||||
|
certificates that Relying Parties (RP) need to configure their
|
||||||
|
software with to be able to verify U2F device certificates.
|
||||||
|
|
||||||
|
This file has been signed with OpenPGP and you should verify the
|
||||||
|
signature and the authenticity of the public key before trusting the
|
||||||
|
content. The signature is located next to the file:
|
||||||
|
|
||||||
|
https://developers.yubico.com/u2f/yubico-u2f-ca-certs.txt
|
||||||
|
https://developers.yubico.com/u2f/yubico-u2f-ca-certs.txt.sig
|
||||||
|
|
||||||
|
We will update this file from time to time when we publish more CA
|
||||||
|
certificates.
|
||||||
|
|
||||||
|
Name: Yubico U2F Root CA Serial 457200631
|
||||||
|
Issued: 2014-08-01
|
||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ
|
||||||
|
dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw
|
||||||
|
MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290
|
||||||
|
IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
|
||||||
|
AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk
|
||||||
|
5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep
|
||||||
|
8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw
|
||||||
|
nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT
|
||||||
|
9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw
|
||||||
|
LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ
|
||||||
|
hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN
|
||||||
|
BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4
|
||||||
|
MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt
|
||||||
|
hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k
|
||||||
|
LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U
|
||||||
|
sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc
|
||||||
|
U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -181,7 +181,7 @@ $SHOW_LAST_LOGIN = true;
|
||||||
$FIDO2_UV_FLAG_REGISTER = 'preferred';
|
$FIDO2_UV_FLAG_REGISTER = 'preferred';
|
||||||
$FIDO2_UV_FLAG_LOGIN = 'preferred'; // iOS ignores the key via NFC if required - known issue
|
$FIDO2_UV_FLAG_LOGIN = 'preferred'; // iOS ignores the key via NFC if required - known issue
|
||||||
$FIDO2_USER_PRESENT_FLAG = true;
|
$FIDO2_USER_PRESENT_FLAG = true;
|
||||||
$FIDO2_FORMATS = array('android-key', 'android-safetynet', 'fido-u2f', 'none', 'packed', 'tpm');
|
$FIDO2_FORMATS = array('apple', 'android-key', 'android-safetynet', 'fido-u2f', 'none', 'packed', 'tpm');
|
||||||
|
|
||||||
// Set visible Rspamd maps in mailcow UI, do not change unless you know what you are doing
|
// Set visible Rspamd maps in mailcow UI, do not change unless you know what you are doing
|
||||||
$RSPAMD_MAPS = array(
|
$RSPAMD_MAPS = array(
|
||||||
|
|
Loading…
Reference in New Issue