Exemple #1
0
    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']
Exemple #2
0
    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")
Exemple #3
0
 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")
Exemple #4
0
    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))
Exemple #5
0
    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)
Exemple #6
0
    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
Exemple #7
0
 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
Exemple #8
0
 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))
Exemple #9
0
    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")
Exemple #10
0
    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
Exemple #11
0
    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")
Exemple #12
0
 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
Exemple #13
0
    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()]
Exemple #14
0
    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
Exemple #15
0
    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')
Exemple #16
0
    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
Exemple #17
0
 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))