def build(seed: bytes, path: str, seed_method: SeedMethod = SeedMethod.SEED_METHOD_BIP39, password: str = '') -> 'KeyPairSecrets': """ Build a valid key pair secrets. :param seed: key seed :param path: key path :param seed_method: seed method (SEED_METHOD_NONE or SEED_METHOD_BIP39) default=SEED_METHOD_BIP39 :param password: key password (secrets) (Optional: can be empty string) :return: valid key pair secrets :raises: IdentityValidationError: if invalid key seed IdentityValidationError: if invalid key path """ if seed_method == SeedMethod.SEED_METHOD_NONE: if len(seed) < MIN_SEED_METHOD_NONE_LEN: raise IdentityValidationError(f'Invalid seed length for method \'SEED_METHOD_NONE\', ' f'must be at least {MIN_SEED_METHOD_NONE_LEN} bytes') elif seed_method == SeedMethod.SEED_METHOD_BIP39: KeyPairSecretsHelper.validate_bip39_seed(seed) else: raise IdentityValidationError(f'Invalid seed method \'{seed_method}\', ' f'must be in {[m.name for m in SeedMethod]}') if not path.startswith(KEY_PAIR_PATH_PREFIX): raise IdentityValidationError(f'Invalid key pair path \'{path}\', ' f'must start with {KEY_PAIR_PATH_PREFIX}') return KeyPairSecrets(seed, path, seed_method, password)
def decode_and_verify_token(token: str, public_base58: str, audience: str): """ Decode a jwt token and verifying it. :param token: jwt token :param public_base58: token public base58 key :param audience: token audience :return: decoded verified token :raises: IdentityValidationError: if invalid token IdentityValidationError: if invalid token signature IdentityValidationError: if expired token """ try: key = KeysHelper.get_public_ECDSA_from_base58(public_base58) return jwt.decode( token, key, audience=audience, algorithms=[TOKEN_ALGORITHM], # type: ignore verify=True, options={'verify_signature': True}) except jwt.exceptions.InvalidSignatureError as err: raise IdentityValidationError( f'Invalid token signature: \'{err}\'') from err except jwt.exceptions.ExpiredSignatureError as err: raise IdentityValidationError(f'Expired token: \'{err}\'') from err except jwt.exceptions.DecodeError as err: raise IdentityValidationError( f'Can not decode invalid token: \'{err}\'') from err
def build(label: Optional[str], comment: Optional[str], url: Optional[str]): """ Build register metadata. :param label: metadata label :param comment: metadata comment :param url: metadata url :return: valid register metadata :raises: IdentityValidationError: if invalid label IdentityValidationError: if invalid comment IdentityValidationError: if invalid url """ if label and len(label) > DOCUMENT_MAX_LABEL_LENGTH: raise IdentityValidationError( f'Document metadata label it too long, max size: \'{label}\'') if comment and len(comment) > DOCUMENT_MAX_COMMENT_LENGTH: raise IdentityValidationError( f'Document metadata comment it too long, max size: \'{comment}\'' ) if url and len(url) > DOCUMENT_MAX_URL_LENGTH: raise IdentityValidationError( f'Document metadata url it too long, max size: \'{url}\'') return Metadata(label, comment, url)
def verify_authentication(resolver_client: ResolverClient, token: str) -> dict: """ Verify if the authentication token is allowed for authentication. :param resolver_client: resolver client interface :param token: jwt authentication token :return: decoded verified authentication token :raises: IdentityAuthenticationFailed: if not allowed for authentication """ try: unverified_token = JwtTokenHelper.decode_token(token) for field in ('iss', 'sub', 'aud', 'iat', 'exp'): if field not in unverified_token: raise IdentityValidationError(f'Invalid token, missing {field} field') issuer = Issuer.from_string(unverified_token['iss']) doc = resolver_client.get_document(issuer.did) get_controller_doc = resolver_client.get_document issuer_key = RegisterDocumentHelper.get_valid_issuer_key_for_auth(doc, issuer.name, get_controller_doc) if not issuer_key: raise IdentityInvalidRegisterIssuerError(f'Invalid issuer {issuer}') verified_token = JwtTokenHelper.decode_and_verify_token(token, issuer_key.public_key_base58, unverified_token['aud']) IdentityAuthValidation.validate_allowed_for_auth(resolver_client, issuer_key.issuer, verified_token['sub']) return {'iss': verified_token['iss'], 'sub': verified_token['sub'], 'aud': verified_token['aud'], 'iat': verified_token['iat'], 'exp': verified_token['exp']} except (IdentityValidationError, IdentityResolverError, IdentityInvalidRegisterIssuerError, IdentityNotAllowed) as err: raise IdentityAuthenticationFailed('Not authenticated') from err
def create_doc_token(issuer: Issuer, audience: str, doc: RegisterDocument, private_key: ec.EllipticCurvePrivateKey) -> str: """ Create a register document jwt token. :param issuer: document issuer :param audience: token audience :param doc: register document :param private_key: issuer private key :return: encoded jwt token :raises: IdentityValidationError: if can not encode the token """ try: return jwt.encode( { 'iss': str(issuer), 'aud': audience, 'doc': doc.to_dict() }, private_key, # type: ignore algorithm=TOKEN_ALGORITHM) except (TypeError, ValueError) as err: raise IdentityValidationError( f'Can not create document token for {issuer}: \'{err}\'' ) from err
def from_challenge_token(resolver_client: ResolverClient, challenge_token: str) -> 'Proof': """ Build proof from challenge token. :param resolver_client: resolver client to get the registered documents :param challenge_token: jwt challenge token :return: valid proof :raises: IdentityValidationError: if invalid challenge token """ decoded_token = JwtTokenHelper.decode_token(challenge_token) iss = decoded_token.get('iss') aud = decoded_token.get('aud') if not iss or not aud: raise IdentityValidationError( 'Invalid challenge token, missing \'iss\' or \'aud\'') issuer = Issuer.from_string(iss) doc = resolver_client.get_document(issuer.did) get_controller_doc = resolver_client.get_document issuer_key = RegisterDocumentHelper.get_valid_issuer_key_for_control_only( doc, issuer.name, get_controller_doc) if not issuer_key: raise IdentityInvalidRegisterIssuerError( f'Invalid issuer {issuer}') verified_token = JwtTokenHelper.decode_and_verify_token( challenge_token, issuer_key.public_key_base58, aud) return Proof(issuer_key.issuer, aud.encode('ascii'), verified_token['proof'])
def create_auth_token( iss: str, sub: str, aud: str, duration: int, private_key: ec.EllipticCurvePrivateKey, start_offset: int = DEFAULT_TOKEN_START_OFFSET_SECONDS) -> str: """ Create an authentication jwt token. :param iss: issuer as string :param sub: subject document did :param aud: token audience :param duration: token duration (seconds) :param private_key: issuer private key :param start_offset: offset for token valid-from time used (default=DEFAULT_TOKEN_START_OFFSET_SECONDS) :return: encoded jwt token :raises: IdentityValidationError: if invalid duration (<=0) IdentityValidationError: if can not encode the token """ now = int(datetime.now().timestamp()) if duration < 0: raise IdentityValidationError( f'Can not create auth token with duration={duration}, must be >0' ) try: return jwt.encode( { 'iss': iss, 'aud': aud, 'sub': sub, 'iat': now + start_offset, 'exp': now + duration }, private_key, algorithm='ES256') # type: ignore except (TypeError, ValueError) as err: raise IdentityValidationError( f'Can not create auth token for {iss}/{sub}: \'{err}\'' ) from err
def create_seed(length: Optional[int] = 256) -> bytes: """ Create a new seed (secrets). :param length: seed length :return: seed :raises: IdentityValidationError: if invalid seed length """ if length not in (128, 256): raise IdentityValidationError('length must be 128 or 256') return secrets.token_bytes(nbytes=int(length / 8))
def mnemonic_bip39_to_seed(mnemonic: str, lang: str = 'english') -> bytes: """ Take mnemonic string and return seed bytes :param mnemonic: mnemonic string :param lang: language :return: seed bytes :raises: IdentityValidationError: if invalid seed IdentityValidationError: if invalid lang IdentityDependencyError: if incompatible Mnemonic dependency """ try: men = Mnemonic(lang) return men.to_entropy(mnemonic) except ConfigurationError as err: raise IdentityDependencyError(f'Dependency Mnemonic Internal Error: {err}') from err except (LookupError, ValueError) as err: raise IdentityValidationError(f'{err}') from err except OSError as err: raise IdentityValidationError(f'Invalid language for mnemonic: {err}') from err
def validate_issuer_string(issuer: str): """ Validate issuer. :param issuer: issuer as string :raises: IdentityValidationError: if invalid issuer """ result = re.match(ISSUER_PATTERN, issuer) if result is None: raise IdentityValidationError( f'Identifier does not match pattern {issuer} - {ISSUER_PATTERN}' )
def validate_identifier(did: str): """ Validate decentralised identifier. :param did: decentralised identifier :raises: IdentityValidationError: if invalid identifier """ result = re.match(IDENTIFIER_ID_PATTERN, did) if result is None: raise IdentityValidationError( f'Identifier does not match pattern {did} - {IDENTIFIER_ID_PATTERN}' )
def validate_key_name(name: str) -> bool: """ Validate key name. :param name: key name :raises: IdentityValidationError: if invalid key name """ m = re.match(IDENTIFIER_NAME_PATTERN, name) if m is None: raise IdentityValidationError( f'Name is not valid: {name} - {IDENTIFIER_NAME_PATTERN}') return True
def seed_bip39_to_mnemonic(seed: bytes, lang: str = 'english') -> str: """ Convert a BIP39 seed to mnemonic. :param seed: BIP39 seed :param lang: a mnemonic language :return: a mnemonic :raises: IdentityValidationError: if invalid seed IdentityValidationError: if invalid lang IdentityDependencyError: if incompatible Mnemonic dependency """ try: men = Mnemonic(lang) return men.to_mnemonic(seed) except ConfigurationError as err: raise IdentityDependencyError(f'Dependency Mnemonic Internal Error: {err}') from err except TypeError as err: raise IdentityValidationError(f'Invalid seed format for method \'SEED_METHOD_BIP39_LEN\': {err}') from err except ValueError as err: raise IdentityValidationError(f'Invalid seed length for method \'SEED_METHOD_BIP39_LEN\': {err}') from err except OSError as err: raise IdentityValidationError(f'Invalid language for mnemonic: {err}') from err
def from_string(issuer_string: str) -> 'Issuer': """ Build a valid issuer from issuer string. :param issuer_string: issuer string :return: valid issuer :raises: IdentityValidationError: if invalid issuer string """ parts = issuer_string.split(ISSUER_SEPARATOR) if len(parts) != 2: raise IdentityValidationError( f'Invalid issuer string {issuer_string} should be of the form of [did]{ISSUER_SEPARATOR}[name]' ) return Issuer.build(parts[0], f'{ISSUER_SEPARATOR}{parts[1]}')
def from_dict(data: dict): """ Build a register public key from dict. :param data: register public key as dict :return: valid register public key :raises: IdentityValidationError: if invalid register public key as dict """ try: return RegisterPublicKey.build(data['id'], data['publicKeyBase58'], data.get('revoked', False)) except (TypeError, KeyError, ValueError) as err: raise IdentityValidationError(f'Can not parse invalid register public key: \'{err}\'') from err
def from_dict(data: dict): """ Build a register delegation public key from dict. :param data: register delegation public key as dict :return: valid register delegation key :raises: IdentityValidationError: if invalid register delegation public key as dict """ try: controller = Issuer.from_string(data['controller']) proof_type = DelegationProofType(data.get('proofType', DelegationProofType.DID.value)) return RegisterDelegationProof.build(data['id'], controller, data['proof'], data.get('revoked', False), proof_type) except (TypeError, KeyError, ValueError) as err: raise IdentityValidationError(f'Can not parse invalid register delegation proof: \'{err}\'') from err
def decode_token(token: str) -> dict: """ Decode a jwt token without verifying it. :param token: jwt token :return: decoded token :raises: IdentityValidationError: if invalid token """ try: return jwt.decode(token, options={'verify_signature': False}, algorithms=[TOKEN_ALGORITHM], verify=False) except jwt.exceptions.DecodeError as err: raise IdentityValidationError( f'Can not decode invalid token: \'{err}\'') from err
def get_public_ECDSA_from_base58( public_base58: str) -> ec.EllipticCurvePublicKey: """ Get public key ECDSA from public key base58 :param public_base58: public key base58 :return: public key ECDSA :raises: IdentityValidationError: if invalid public base58 key """ try: public_bytes = base58.b58decode(public_base58) public_ecdsa = ec.EllipticCurvePublicKey.from_encoded_point( ec.SECP256K1(), public_bytes) return public_ecdsa except ValueError as err: raise IdentityValidationError( f'Can not convert public key base58 to ECDSA: \'{err}\'' ) from err
def get_private_key(key_pair_secrets: KeyPairSecrets) -> ec.EllipticCurvePrivateKey: """ Get private key from key pair secrets :param key_pair_secrets: key pair secrets :return: private key :raise: IdentityValidationError: if invalid seed method IdentityValidationError: if invalid lang IdentityDependencyError: if incompatible Mnemonic dependency IdentityDependencyError: if incompatible EllipticCurve dependency """ if key_pair_secrets.seed_method == SeedMethod.SEED_METHOD_NONE: result = hmac.new(key_pair_secrets.seed, key_pair_secrets.password.encode(), sha512).digest() elif key_pair_secrets.seed_method == SeedMethod.SEED_METHOD_BIP39: men = KeyPairSecretsHelper.seed_bip39_to_mnemonic(key_pair_secrets.seed) result = Mnemonic.to_seed(men, key_pair_secrets.password) else: raise IdentityValidationError(f'Invalid seed method \'{key_pair_secrets.seed_method}\', ' f'must be in {[m.name for m in SeedMethod]}') private_expo = hmac.new(result, key_pair_secrets.path.encode(), sha256).hexdigest() return KeysHelper.get_private_ECDSA(private_expo)
def validate_delegation_from_doc(doc_id: str, controller_doc: RegisterDocument, deleg_proof: RegisterDelegationProof): """ Validate register delegation proof against the deleagtion controller register document. :param doc_id: decentralised id of the register document owning the register delegation proof :param controller_doc: delegation controller register document :param deleg_proof: register delegation proof under validation :raises: IdentityInvalidDocumentDelegationError: if controller issuer does not belongs to the controller document public keys IdentityInvalidDocumentDelegationError: if invalid register delegation proof signature """ try: controller_issuer = deleg_proof.controller public_key = controller_doc.public_keys.get(controller_issuer.name) if not public_key: raise IdentityValidationError(f'Public key \'{controller_issuer.name}\' not found' f' on controller doc \'{controller_doc.did}\'') DelegationValidation._is_valid_issuer_or_reusable_proof(deleg_proof, public_key, doc_id) except IdentityValidationError as err: raise IdentityInvalidDocumentDelegationError(f'Invalid delegation for doc \'{doc_id}\'' f' with controller: \'{deleg_proof.name}\': {err}') from err
def build_new_challenge_token(proof: Proof, private_key: ec.EllipticCurvePrivateKey) -> str: """ Build a new challenge token from a proof. :param proof: proof :param private_key: private key :return: jwt challenge token :raises: IdentityValidationError: if can not encode the token """ try: return jwt.encode( { 'iss': str(proof.issuer), 'aud': proof.content.decode('ascii'), 'proof': proof.signature }, private_key, algorithm=TOKEN_ALGORITHM) # type: ignore except (TypeError, ValueError) as err: raise IdentityValidationError( f'Can not create challenge token for {proof}: \'{err}\'') from err