def __init__(self, logger, config): self._logger = logger self._config = config if config.yhsm: _aead = YHSM_OATHAEAD(logger, config) self._data = _aead.to_dict(plaintext=True) return # No local YubiHSM - access one remotely from another instance of this # API and it's function aead_gen. claims = { 'version': 1, 'nonce': os.urandom(8).encode( 'hex'), # Not AEAD nonce, just 'verify response' nonce 'length': 20, # OATH is HMAC-SHA1 == 160 bits == 20 bytes 'plaintext': True, # Need the plaintext to share with the user } url = self._config.oath_aead_gen_url req = eduid_api.request.MakeRequest(claims, self._logger, self._config) api_key = self._config.keys.lookup_by_url(url) if not api_key: self._logger.error("No API Key found for URL {!r}".format(url)) raise EduIDAPIError("No API Key found for OATH AEAD service") req.send_request(url, 'request', api_key[0]) data = req.decrypt_response() self._logger.debug("Got response: {!r}".format(data)) if data['status'] != 'OK': self._logger.error("OATH AEAD generation failed: {!r}".format( data.get('reason'))) raise EduIDAPIError("OATH AEAD generation failed") self._data = data['aead']
def __init__(self, filename, debug): self.section = _CONFIG_SECTION _CONFIG_DEFAULTS['debug'] = str(debug) self.config = ConfigParser.ConfigParser(_CONFIG_DEFAULTS) if not self.config.read([filename]): raise EduIDAPIError( "Failed loading config file {!r}".format(filename)) # split on comma and strip. cache result. tmp_add_raw_allow = str(self.config.get(self.section, 'add_raw_allow')) # for pylint self._parsed_add_raw_allow = \ [x.strip() for x in tmp_add_raw_allow.split(',')] self.keys = eduid_api.keystore.KeyStore(self.keystore_fn) self._parsed_oath_aead_keyhandle = None self.yhsm = None kh_str = self.config.get(self.section, 'oath_aead_keyhandle') if self.oath_yhsm_device or kh_str: try: import pyhsm if kh_str: self._parsed_oath_aead_keyhandle = pyhsm.util.key_handle_to_int( kh_str.strip()) try: self.yhsm = pyhsm.YHSM(device=self.oath_yhsm_device) # stir up the pool for _ in xrange(10): self.yhsm.random(32) except pyhsm.exception.YHSM_Error: raise EduIDAPIError('YubiHSM init error') except ImportError: raise EduIDAPIError( "yhsm settings present, but import of pyhsm failed")
def __init__(self, parsed_req): self._parsed_req = parsed_req for req_field in ['digits', 'issuer', 'account']: if req_field not in self._parsed_req: raise EduIDAPIError( "No {!r} in 'OATH' part of request".format(req_field)) if self.type not in ['hotp', 'totp']: raise EduIDAPIError("Invalid type in 'OATH' part of request")
def __init__(self, request, remote_ip, logger, config): BaseRequest.__init__(self, request, remote_ip, 'aead_gen', logger, config) for req_field in ['length', 'version']: if req_field not in self._parsed_req: raise EduIDAPIError("No {!r} in request".format(req_field)) if int(self._parsed_req['version']) != 1: raise EduIDAPIError("Unknown version in request".format(req_field))
def to_string(self, remote_key=None, remote_ip=None): """ Sign and encrypt this response. :param remote_key: Encrypt response to this key :param remote_ip: If no remote_key, look up a key for this remote_ip :type remote_key: str or None :type remote_ip: str or None :rtype: str """ sign_key = self._config.keys.private_key self._logger.debug("Signing response using key {!r}".format(sign_key)) jws = jose.sign(self._data, sign_key.jwk, alg=self._config.jose_alg) signed_claims = {'v1': jose.serialize_compact(jws)} self._logger.debug("Signed response: {!r}".format(signed_claims)) encrypt_key = remote_key if not encrypt_key: # default to the first key found using the remote_ip in case no key was supplied ip_keys = self._config.keys.lookup_by_ip(remote_ip) if not ip_keys: self._logger.warning( "Found no key for IP {!r}, can't encrypt response:\n{!r}". format(remote_ip, self._data)) raise EduIDAPIError( "No API Key found - can't encrypt response") encrypt_key = ip_keys[0] self._logger.debug("Encrypting claims to key {!r}".format(encrypt_key)) jwe = jose.encrypt(signed_claims, encrypt_key.jwk) return jose.serialize_compact(jwe)
def _decrypt(self, request): """ Decrypt a JOSE encrypted request. :param request: Request to parse :type request: str :rtype: bool or jose.JWS """ jwe = jose.deserialize_compact(request.replace("\n", '')) decrypted = None decr_key = self._config.keys.private_key if not decr_key: self._logger.error("No asymmetric private key (named '_private') found in the keystore") return False self._logger.debug("Trying to decrypt request with key {!r}".format(decr_key)) try: decrypted = jose.decrypt(jwe, decr_key.jwk, expiry_seconds = decr_key.expiry_seconds) self._logger.debug("Decrypted {!r}".format(decrypted)) except jose.Expired as ex: self._logger.warning("Request encrypted with key {!r} has expired: {!r}".format(decr_key, ex)) except jose.Error as ex: self._logger.warning("Failed decrypt with key {!r}: {!r}".format(decr_key, ex)) raise EduIDAPIError('Could not decrypt request') if not 'v1' in decrypted.claims: self._logger.error("No 'v1' in decrypted claims: {!r}".format(decrypted)) return False to_verify = jose.deserialize_compact(decrypted.claims['v1']) #logger.debug("Decrypted claims to verify: {!r}".format(to_verify)) return to_verify
def __init__(self, request, authstore, logger, config): self._request = request self._authstore = authstore self._logger = logger self._config = config self._user = self._authstore.get_authuser(request.token.user_id) if not self._user: raise EduIDAPIError('Unknown user') if self._user.owner != self._request.signing_key.owner: self._logger.info( "Denied authentiation request for user {!r} (owner {!r}) " "made with signing key {!r}/{!r}".format( self._user, self._user.owner, self._request.signing_key, self._request.signing_key.owner)) raise EduIDAPIError('Wrong administrative domain') self._status = None
def __init__(self, parsed_req): self._parsed_req = parsed_req for req_field in [ 'appId', 'challenge', 'clientData', 'registrationData', 'version' ]: if req_field not in self._parsed_req: raise EduIDAPIError( "No {!r} in 'U2F' part of request".format(req_field))
def __init__(self, request, remote_ip, logger, config): BaseRequest.__init__(self, request, remote_ip, 'mfa_auth', logger, config) for req_field in ['nonce', 'version']: if req_field not in self._parsed_req: raise EduIDAPIError("No {!r} in request".format(req_field)) if int(self._parsed_req['version']) != 1: raise EduIDAPIError("Unknown version in request".format(req_field)) if self.token_type == 'OATH': if 'OATH' not in self._parsed_req: raise EduIDAPIError("No 'OATH' in request") self.token = AuthOATHTokenRequest(self._parsed_req['OATH']) elif self.token_type == 'U2F': raise NotImplemented("U2F authentication not implemented yet") else: raise EduIDAPIError("Unknown token type")
def decrypt_response(self, ciphertext=None, return_jwt=False): """ Decrypt the response returned from send_request. :param ciphertext: Ciphertext to decrypt. If not supplied the last request response is used. :param return_jwt: Return the whole JOSE JWT or just the claims :type ciphertext: None | str | unicode :type return_jwt: bool :return: Decrypted result :rtype: dict | jose.JWT """ if ciphertext is None: ciphertext = self._request_result.text jwe = jose.deserialize_compact(ciphertext.replace("\n", '')) priv_key = self._config.keys.private_key if not priv_key.keytype == 'jose': raise EduIDAPIError("Non-jose private key unusuable with decrypt_response") decrypted = jose.decrypt(jwe, priv_key.jwk) if not 'v1' in decrypted.claims: self._logger.error("No 'v1' in decrypted claims: {!r}\n\n".format(decrypted)) raise EduIDAPIError("No 'v1' in decrypted claims") to_verify = jose.deserialize_compact(decrypted.claims['v1']) jwt = jose.verify(to_verify, self._api_key.jwk, alg = self._config.jose_alg) self._logger.debug("Good signature on response to request using key: {!r}".format( self._api_key.jwk )) if 'nonce' in self._claims: # there was a nonce in the request, verify it is also present in the response if not 'nonce' in jwt.claims: self._logger.warning("Nonce was present in request, but not in response:\n{!r}".format( jwt.claims )) raise EduIDAPIError("Request-Response nonce validation error") if jwt.claims['nonce'] != self._claims['nonce']: self._logger.warning("Response nonce {!r} does not match expected {!r}".format( jwt.claims['nonce'], self._claims['nonce'] )) if return_jwt: return jwt return jwt.claims
def __init__(self, request, remote_ip, logger, config): BaseRequest.__init__(self, request, remote_ip, 'mfa_add', logger, config) for req_field in ['nonce', 'token_type', 'version']: if req_field not in self._parsed_req: raise EduIDAPIError("No {!r} in request".format(req_field)) if int(self._parsed_req['version']) != 1: raise EduIDAPIError("Unknown version in request".format( self._parsed_req['version'])) if self.token_type == 'OATH': if 'OATH' not in self._parsed_req: raise EduIDAPIError("No 'OATH' in request") self.token = AddOATHTokenRequest(self._parsed_req['OATH']) elif self.token_type == 'U2F': self.token = AddU2FTokenRequest(self._parsed_req['U2F']) else: raise EduIDAPIError("Unknown token type")
def jwk(self): """ :return: JWK dict :rtype: dict """ if not self._key: if 'file' in self._data.get('JWK'): # If configuration looks like this: # {"dash-fre-1": {"JWK": {"file": "/path/to/client-dash-fre-1.pem"}, # ... # then load the private key from the path supplied. _keyfile = self._data.get('JWK')['file'] try: fd = open(_keyfile) self._key = dict(k = fd.read()) except Exception as ex: raise EduIDAPIError("Failed loading key file {!r}: {!r}".format(_keyfile, ex)) else: self._key = self._data.get('JWK') if not 'k' in self._key: EduIDAPIError("Bad JWK for key {!r}: {!r}".format(self, self._key)) return self._key
def __init__(self, keystore_fn): if not keystore_fn: self._keys = [] return try: f = open(keystore_fn, 'r') data = f.read() f.close() data = simplejson.loads(data) assert (type(data) == dict) except Exception as ex: raise EduIDAPIError("Failed loading config file {!r}: {!r}".format(keystore_fn, ex)) self._keys = [APIKey(name, value) for (name, value) in data.items()]
def send_request(self, url, name, apikey): """ Encrypt the claims and POST it to url. :param url: The URL to POST the data to :param name: The HTTP parameter name to put the data in :param apikey: API Key to encrypt data with before posting :return: :type url: str | unicode :type apikey: eduid_api.keystore.APIKey """ self._logger.debug("Encrypting signed request using {!r}".format(apikey)) if not apikey.keytype == 'jose': raise EduIDAPIError("Non-jose API key unusuable with send_request") self._api_key = apikey jwe = jose.encrypt(self.signed_claims, apikey.jwk) data = {name: jose.serialize_compact(jwe)} self._logger.debug("Sending signed and encrypted request to {!r}".format(url)) self._request_result = requests.post(url, data = data) self._logger.debug("Result of request: {!r}".format(self._request_result)) return self._request_result
def __init__(self, logger, config, num_bytes=20): self.keyhandle = config.oath_aead_keyhandle self._logger = logger if not self.keyhandle: raise EduIDAPIError('No OATH AEAD keyhandle configured') self._logger.debug( "Generating {!r} bytes AEAD using key_handle 0x{:x}".format( num_bytes, self.keyhandle)) from_os = os.urandom(num_bytes).encode('hex') from_hsm = config.yhsm.random(num_bytes) # XOR together num_bytes from the YubiHSM's RNG with nu_bytes from /dev/urandom. xored = ''.join( [chr(ord(a) ^ ord(b)) for (a, b) in zip(from_hsm, from_os)]) self.secret = xored # Make the key inside the AEAD only usable with the YubiHSM YSM_HMAC_SHA1_GENERATE function # Enabled flags 00010000 = YSM_HMAC_SHA1_GENERATE flags = '\x00\x00\x01\x00' # struct.pack("< I", 0x10000) aead = config.yhsm.generate_aead_simple(chr(0x0), self.keyhandle, self.secret + flags) self.aead = aead.data.encode('hex') self.nonce = aead.nonce.encode('hex')
def __init__(self, request, remote_ip, name, logger, config): self._logger = logger self._config = config self._signing_key = None if isinstance(request, dict) and _TESTING: # really only accept a dict when testing, to avoid accidental # acceptance of unsigned requests parsed = request else: try: decrypted = self._decrypt(request) if not decrypted: self._logger.warning("Could not decrypt request from {!r}".format(remote_ip)) raise EduIDAPIError("Failed decrypting request") verified = self._verify(decrypted, remote_ip) if not verified: self._logger.warning("Could not verify decrypted request from {!r}".format(remote_ip)) raise EduIDAPIError("Failed verifying signature") parsed = verified.claims except Exception: logger.error("Failed decrypting/verifying request:\n{!r}\n-----\n".format(request), traceback=True) raise EduIDAPIError("Failed parsing request") assert(isinstance(parsed, dict)) for req_field in ['version']: if req_field not in parsed: raise EduIDAPIError("No {!r} in request".format(req_field)) if parsed['version'] is not 1: # really handle missing version below raise EduIDAPIError("Unknown request version: {!r}".format(parsed['version'])) if not name in self.signing_key.allowed_commands: raise EduIDAPIError("Method {!r} not allowed with this key".format(name)) self._parsed_req = parsed cherrypy.request.eduid_api_parsed_req = parsed
def __init__(self, parsed_req): self._parsed_req = parsed_req for req_field in ['user_id', 'user_code']: if req_field not in self._parsed_req: raise EduIDAPIError( "No {!r} in 'OATH' part of request".format(req_field))