def _readCredentialPublicKey(self, binary, offset): enc, end_offset = cbor.loads_in_place(binary[offset:]) # print(enc) # COSE key-encoded elliptic curve public key in EC2 format credPKey = {} credPKey.update({'kty': enc[self.COSE_KTY]}) credPKey.update({'alg': enc[self.COSE_ALG]}) credPKey.update({'crv': enc[self.COSE_CRV]}) credPKey.update({'x': enc[self.COSE_X]}) credPKey.update({'y': enc[self.COSE_Y]}) # Validation if credPKey['kty'] != self.EC2_TYPE: raise WebAuthnException('public key not in EC2 format', ErrCodes.INVALID_PUBLIC_KEY) if credPKey['alg'] != self.EC2_ES256: raise WebAuthnException('signature algorithm not ES256', ErrCodes.INVALID_PUBLIC_KEY) if credPKey['crv'] != self.EC2_P256: raise WebAuthnException('curve not P-256', ErrCodes.INVALID_PUBLIC_KEY) if len(credPKey['x']) != 32: raise WebAuthnException('Invalid X-coordinate', ErrCodes.INVALID_PUBLIC_KEY) if len(credPKey['y']) != 32: raise WebAuthnException('Invalid Y-coordinate', ErrCodes.INVALID_PUBLIC_KEY) return (credPKey, end_offset)
def __init__(self, attestation_object, authenticator_data): super().__init__(attestation_object, authenticator_data) # check u2f data att_stmt = self._attestation_object['attStmt'] if 'alg' in att_stmt and att_stmt[ 'alg'] != self._SHA256_cose_identifier: # SHA256 raise WebAuthnException( 'only SHA256 acceptable but got: ' + att_stmt['alg'], ErrCodes.INVALID_DATA) if 'sig' not in att_stmt: raise WebAuthnException('No signature found', ErrCodes.INVALID_DATA) if 'x5c' not in att_stmt or len(att_stmt['x5c']) < 1: raise WebAuthnException('invalid x5c certificate', ErrCodes.INVALID_DATA) self._signature = att_stmt['sig'] self._x5c = att_stmt['x5c'][0] if len(att_stmt['x5c']) > 1: for x5c in att_stmt['x5c']: self._x5c_chain.append(x5c)
def __init__(self, binary): super().__init__() if not isinstance(binary, bytes) or len(binary) < 37: raise WebAuthnException("Invalid authenticatorData input", ErrCodes.INVALID_DATA) self._binary = binary # Read infos from binary # https://www.w3.org/TR/webauthn/#sec-authenticator-data # https://medium.com/@herrjemand/verifying-fido2-responses-4691288c8770 #RP ID self._rpIdHash = binary[0:32] #flags (1 byte) flags = struct.unpack(">B", binary[32:33])[0] self._flags = self._readFlags(flags) # signature counter: 4 bytes unsigned big-endian integer. self._signCount = struct.unpack(">I", binary[33:37])[0] offset = 37 # https://www.w3.org/TR/webauthn/#sec-attested-credential-data self._attestedCredentialData = None if self._flags['attestedDataIncluded']: self._attestedCredentialData, offset = self._readAttestData( binary, offset) self._extensionData = None if self._flags['extensionDataIncluded']: self._extensionData = self._readExtensionData(binary[offset:])
def __init__(self, binary, allowFormat=[ 'none', 'android-key', 'packed', 'fido-u2f', 'android-safetynet' ]): self.allowedFormats = allowFormat self.authenticatorData = {} self.attestationFormat = {} enc = cbor.loads(binary) #validation if not isinstance(enc, dict) or 'fmt' not in enc: raise WebAuthnException('invalid attestation format', ErrCodes.INVALID_DATA) if 'attStmt' not in enc: raise WebAuthnException( 'invalid attestation format (attStmt not available)', ErrCodes.INVALID_DATA) if 'authData' not in enc: raise WebAuthnException( 'invalid attestation format (authData not available)', ErrCodes.INVALID_DATA) self._authenticatorData = AuthenticatorData(enc['authData']) if enc['fmt'] not in self.allowedFormats: raise WebAuthnException( 'invalid atttestation format: ' + enc['fmt'], ErrCodes.INVALID_DATA) if enc['fmt'] == 'none': self._attestationFormat = NoneType(enc, self._authenticatorData) elif enc['fmt'] == 'fido-u2f': self._attestationFormat = U2F(enc, self._authenticatorData) elif enc['fmt'] == 'packed': self._attestationFormat = Packed(enc, self._authenticatorData) elif enc['fmt'] == 'android-key': self._attestationFormat = AndroidKey(enc, self._authenticatorData) elif enc['fmt'] == 'android-safetynet': self._attestationFormat = AndroidSafetyNet(enc, self._authenticatorData) else: raise WebAuthnException( 'atttestation format: ' + enc['fmt'] + ' not supported', ErrCodes.INVALID_DATA)
def getPublicKeyU2F(self): if self._attestedCredentialData == None: raise WebAuthnException( "credential id not included in authenticator data", ErrCodes.CREDENTIAL_NOT_INCLUDE) # ECC uncompressed return b"\x04" + self._attestedCredentialData['credentialPublicKey'][ 'x'] + self._attestedCredentialData['credentialPublicKey']['y']
def __init__(self, attestation_object, authenticator_data): super().__init__(attestation_object, authenticator_data) att_stmt = self._attestation_object['attStmt'] if 'ver' not in att_stmt: raise WebAuthnException('invalid Android Safety Net Format ', ErrCodes.INVALID_DATA) if 'response' not in att_stmt: raise WebAuthnException('invalid Android Safety Net Format ', ErrCodes.INVALID_DATA) response = att_stmt['response'] # Response is a JWS [RFC7515] object in Compact Serialization. # JWSs have three segments separated by two period ('.') characters parts = response.decode('ascii').split('.') if len(parts) != 3: raise WebAuthnException('invalid JWS data', ErrCodes.INVALID_DATA) header = base64.b64decode(parts[0] + '===', altchars="-_").decode('ascii') payload = base64.b64decode(parts[1] + '===', altchars="-_").decode('ascii') self._signature = base64.b64decode(parts[2] + '===', altchars="-_") self._signedValue = bytes(parts[0] + '.' + parts[1], 'ascii') header = json.loads(header) payload = json.loads(payload) if 'x5c' not in header: raise WebAuthnException('No X.509 signature in JWS Header', ErrCodes.INVALID_DATA) if 'alg' not in header or header['alg'] not in ['RS256', 'ES256']: raise WebAuthnException('invalid JWS algorithm '.header['alg'], ErrCodes.INVALID_DATA) self._x5c = base64.b64decode(header['x5c'][0]) self._payload = payload if len(header['x5c']) > 1: for x5c in header['x5c']: self._x5c_chain.append(base64.b64decode(x5c))
def validateAttestation(self, clientDataHash): cert = x509.load_pem_x509_certificate(self.getCertificatePem(), default_backend()) public_key = cert.public_key() if not public_key: raise WebAuthnException('invalid public key: ', ErrCodes.INVALID_PUBLIC_KEY) # 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. m = hashlib.sha256() m.update(self._authenticator_data.getBinary() + clientDataHash) hash_data = m.digest() if 'nonce' not in self._payload or bytes( self._payload['nonce'], 'ascii') != base64.b64encode(hash_data): raise WebAuthnException('invalid nonce in JWS payload', ErrCodes.INVALID_DATA) if 'ctsProfileMatch' not in self._payload or not self._payload[ 'ctsProfileMatch']: raise WebAuthnException('invalid ctsProfileMatch in payload', ErrCodes.INVALID_DATA) CN = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) if len(CN) == 0 or CN[0].value != 'attest.android.com': raise WebAuthnException( 'The common name is not set to "attest.android.com"!', ErrCodes.INVALID_DATA) try: public_key.verify( self._signature, self._signedValue, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256()) return True except InvalidSignature: return False except Exception as e: raise e
def validateAttestation(self, clientDataHash): cert = x509.load_pem_x509_certificate(self.getCertificatePem(), default_backend()) public_key = cert.public_key() if not public_key: raise WebAuthnException('invalid public key: ', ErrCodes.INVALID_PUBLIC_KEY) verify_data = b"\x00" + self._authenticator_data.getRpIdHash() + clientDataHash \ + self._authenticator_data.getCredentialId() + self._authenticator_data.getPublicKeyU2F() try: public_key.verify(self._signature, verify_data, ec.ECDSA(hashes.SHA256())) return True except InvalidSignature: return False except Exception as e: raise e
def validateAttestation(self, clientDataHash): cert = x509.load_pem_x509_certificate(self.getCertificatePem(), default_backend()) public_key = cert.public_key() if not public_key: raise WebAuthnException('invalid public key: ', ErrCodes.INVALID_PUBLIC_KEY) verify_data = self._authenticator_data.getBinary() + clientDataHash try: import base64 # print(base64.b64encode(self._signature).decode("ascii")) print( base64.b64encode( self._authenticator_data.getBinary()).decode('ascii')) public_key.verify(self._signature, verify_data, ec.ECDSA(hashes.SHA256())) return True except InvalidSignature: return False except Exception as e: raise e
def _readAttestData(self, binary, offset): attested_CData = {} end_offset = offset + 16 if (len(binary) <= 55): raise WebAuthnException( 'Attested data should be present but is missing', ErrCodes.MISSING_ATTESTED_DATA) # The AAGUID of the authenticator (16 bytes) attested_CData.update({'aaguid': binary[37:end_offset]}) end_offset = end_offset + 2 # Byte length L of Credential ID, 2 bytes unsigned big-endian integer. length = struct.unpack('>H', binary[53:end_offset])[0] end_offset = end_offset + length attested_CData.update({'credentialId': binary[55:end_offset]}) # extract public key public_key, byte_reads = self._readCredentialPublicKey( binary, 55 + length) attested_CData.update({'credentialPublicKey': public_key}) end_offset = end_offset + byte_reads return (attested_CData, end_offset)
def getCredentialId(self): if self._attestedCredentialData == None: raise WebAuthnException( "credential id not included in authenticator data", ErrCodes.CREDENTIAL_NOT_INCLUDE) return self._attestedCredentialData["credentialId"]