def verify(self, assertion, audience=None, now=None):
        """Verify the certificate chain for the receipt
        """
        if now is None:
            now = int(time.time())

        # This catches KeyError and turns it into ValueError.
        # It saves having to test for the existence of individual
        # items in the various payloads.
        try:
            # Grab the assertion, check that it has not expired.
            # No point doing all that crypto if we're going to fail out anyway.
            certificates, assertion = unbundle_certs_and_assertion(assertion)
            assertion = self.parse_jwt(assertion)
            if assertion.payload["exp"] < now:
                raise ExpiredSignatureError(assertion.payload["exp"])

            # Parse out the list of certificates.
            certificates = [self.parse_jwt(c) for c in certificates]

            # Verify the entire chain of certificates.
            cert = self.verify_certificate_chain(certificates, now=now)

            # Check the signature on the assertion.
            if not self.check_token_signature(assertion, cert):
                raise InvalidSignatureError("invalid signature on assertion")
        except KeyError:
            raise ValueError("Malformed JWT")
        # Looks good!
        return True
Exemple #2
0
    def verify_certificate_chain(self, certificates, now=None):
        """Verify a certificate chain using a powerhose worker.

        The main difference with the base LocalVerifier class is that we
        are using the issuer name as a key to give the information to the
        worker, so we don't need to pass along the certificate.

        In case there is a list of certificates, the last one is returned by
        this function.
        """
        if not certificates:
            raise ValueError("chain must have at least one certificate")
        if now is None:
            now = int(time.time() * 1000)

        def _check_cert_validity(cert):
            if cert.payload["exp"] < now:
                raise ExpiredSignatureError("expired certificate in chain")

        # Here, two different use cases are being handled.
        # if there is only one bundled certificate, then send the data with
        # the hostname. If there are more than one certificate, checks need
        # to be done directly using the certificate data from the bundled
        # certificates, so all the information is passed along

        issuer = certificates[0].payload["iss"]
        current_key = None
        for cert in certificates:
            _check_cert_validity(cert)
            if not self.check_token_signature(cert, current_key,
                                              hostname=issuer):
                raise InvalidSignatureError("bad signature in chain")
            current_key = cert.payload["public-key"]
        return cert
    def verify_certificate_chain(self, certificates, now=None):
        """Verify a signed chain of certificates.

        This function checks the signatures on the given chain of JWT
        certificates.  It looks up the public key for the issuer of the
        first certificate, then uses each certificate in turn to check the
        signature on its successor.

        If the entire chain is valid then to final certificate is returned.
        """
        if not certificates:
            raise ValueError("chain must have at least one certificate")
        if now is None:
            now = int(time.time())
        root_issuer = certificates[0].payload["iss"]
        root_key = self.certs[root_issuer]
        current_key = root_key
        for cert in certificates:
            if cert.payload["exp"] < now:
                raise ExpiredSignatureError("expired certificate in chain")
            if not cert.check_signature(current_key):
                raise InvalidSignatureError("bad signature in chain by: '%s'" %
                                            current_key['kid'])
            current_key = cert.payload["jwk"][0]
        return cert
Exemple #4
0
class RemoteVerifier(object):
    implements(IBrowserIdVerifier)

    def __init__(self,
                 audiences=None,
                 trusted_issuers=None,
                 allowed_issuers=None,
                 verifier_url=None):
        # Since we don't parse the assertion locally, we cannot support
        # list- or pattern-based audience strings.
        if audiences is not None:
            assert isinstance(audiences, basestring)
        self.audiences = audiences
        if isinstance(trusted_issuers, basestring):
            trusted_issuers = trusted_issuers.split()
        self.trusted_issuers = trusted_issuers
        if isinstance(allowed_issuers, basestring):
            allowed_issuers = allowed_issuers.split()
        self.allowed_issuers = allowed_issuers
        if verifier_url is None:
            verifier_url = "https://verifier.accounts.firefox.com/v2"
        self.verifier_url = verifier_url
        self.session = requests.Session()
        self.session.verify = True

    def verify(self, assertion, audience=None):
        if audience is None:
            audience = self.audiences

        body = {'assertion': assertion, 'audience': audience}
        if self.trusted_issuers is not None:
            body['trustedIssuers'] = self.trusted_issuers
        headers = {'content-type': 'application/json'}
        try:
            response = self.session.post(self.verifier_url,
                                         data=json.dumps(body),
                                         headers=headers)
        except (socket.error, requests.RequestException), e:
            msg = "Failed to POST %s. Reason: %s" % (self.verifier_url, str(e))
            raise ConnectionError(msg)

        if response.status_code != 200:
            raise ConnectionError('server returned invalid response')
        try:
            data = json.loads(response.text)
        except ValueError:
            raise ConnectionError("server returned invalid response")

        if data.get('status') != "okay":
            reason = data.get('reason', 'unknown error')
            if "audience mismatch" in reason:
                raise AudienceMismatchError(data.get("audience"), audience)
            if "expired" in reason or "issued later than" in reason:
                raise ExpiredSignatureError(reason)
            raise InvalidSignatureError(reason)
        if self.allowed_issuers is not None:
            issuer = data.get('issuer')
            if issuer not in self.allowed_issuers:
                raise InvalidIssuerError("Issuer not allowed: %s" % (issuer, ))
        return data
Exemple #5
0
    def verify(self, assertion, audience=None):
        """Verify the given BrowserID assertion.

        This method posts the given BrowserID assertion to the remote verifier
        service.  If it is successfully verified then a dict giving the
        email and audience is returned.  If it is not valid then an error
        is raised.

        If the 'audience' argument is given, it first verifies that the
        audience of the assertion matches the one given.  This can help
        avoid doing lots of crypto for assertions that can't be valid.
        If you don't specify an audience, you *MUST* validate the audience
        value returned by this method.
        """
        # Check the audience locally.
        # No point talking to the network if we know it's going to fail.
        # If no explicit audience was given, this will also parse it out
        # for inclusion in the request to the remote verifier service.
        audience = self.check_audience(assertion, audience)

        response = netutils.post(self.verifier_url, {
            'assertion': assertion,
            'audience': audience
        })

        # BrowserID server sends "500 server error" for broken assertions.
        # For now, just translate that directly.  Should check by hand.
        if response.status_code == 500:
            raise ValueError('Malformed assertion')

        try:
            data = json.loads(response.text)
        except ValueError:
            raise ConnectionError("server returned invalid response")

        # Did it come back clean?
        if data.get('status') != "okay":
            raise InvalidSignatureError(str(data))
        if data.get('audience') != audience:
            raise AudienceMismatchError(data.get("audience"), audience)
        return data
    def verify(self, assertion, audience=None):
        if audience is None:
            audience = self.audiences

        body = {'assertion': assertion, 'audience': audience}
        if self.trusted_issuers is not None:
            body['trustedIssuers'] = self.trusted_issuers
        headers = {'content-type': 'application/json'}
        try:
            response = self.session.post(self.verifier_url,
                                         data=json.dumps(body),
                                         headers=headers,
                                         timeout=self.timeout)
        except (socket.error, requests.RequestException) as e:
            msg = "Failed to POST %s. Reason: %s" % (self.verifier_url, str(e))
            raise ConnectionError(msg)

        if response.status_code != 200:
            raise ConnectionError('server returned invalid response code')
        try:
            data = json.loads(response.text)
        except ValueError:
            raise ConnectionError("server returned invalid response body")

        if data.get('status') != "okay":
            reason = data.get('reason', 'unknown error')
            if "audience mismatch" in reason:
                raise AudienceMismatchError(data.get("audience"), audience)
            if "expired" in reason or "issued later than" in reason:
                raise ExpiredSignatureError(reason)
            raise InvalidSignatureError(reason)
        if self.allowed_issuers is not None:
            issuer = data.get('issuer')
            if issuer not in self.allowed_issuers:
                raise InvalidIssuerError("Issuer not allowed: %s" % (issuer,))
        return data
Exemple #7
0
    def verify(self, assertion, audience=None, now=None):
        """Verify the given BrowserID assertion.

        This method parses a BrowserID identity assertion, verifies the
        bundled chain of certificates and signatures, and returns the
        extracted email address and audience.

        If the 'audience' argument is given, it first verifies that the
        audience of the assertion matches the one given.  This can help
        avoid doing lots of crypto for assertions that can't be valid.
        If you don't specify an audience, you *MUST* validate the audience
        value returned by this method.

        If the 'now' argument is given, it is used as the current time in
        milliseconds.  This lets you verify expired assertions, e.g. for
        testing purposes.
        """
        if now is None:
            now = int(time.time() * 1000)

        # This catches KeyError and turns it into ValueError.
        # It saves having to test for the existence of individual
        # items in the various assertion payloads.
        try:
            # Check the audience against the given value, or the wildcards.
            self.check_audience(assertion, audience)

            # Grab the assertion, check that it has not expired.
            # No point doing all that crypto if we're going to fail out anyway.
            certificates, assertion = unbundle_certs_and_assertion(assertion)
            if len(certificates) > 1:
                raise UnsupportedCertChainError("too many certs")
            assertion = self.parse_jwt(assertion)
            if assertion.payload["exp"] < now:
                raise ExpiredSignatureError(assertion.payload["exp"])

            # Parse out the list of certificates.
            certificates = [self.parse_jwt(c) for c in certificates]

            # Extract the email, and the hostname of its provider.
            email = certificates[-1].payload["principal"]["email"]
            match = VALID_EMAIL.match(email)
            if match is None:
                raise ValueError("invalid email in assertion")
            provider = match.group(2)

            # Check that the root issuer is trusted.
            # No point doing all that crypto if we're going to fail out anyway.
            root_issuer = certificates[0].payload["iss"]
            if not self.is_trusted_issuer(provider, root_issuer):
                msg = "untrusted root issuer: %s" % (root_issuer, )
                raise InvalidSignatureError(msg)

            # Verify the entire chain of certificates.
            cert = self.verify_certificate_chain(certificates, now=now)

            # Check the signature on the assertion.
            if not self.check_token_signature(assertion, cert):
                raise InvalidSignatureError("invalid signature on assertion")
        except KeyError:
            raise ValueError("Malformed JWT")
        # Looks good!
        res = {
            "status": "okay",
            "audience": assertion.payload["aud"],
            "email": email,
            "issuer": root_issuer,
        }
        idpClaims = extract_extra_claims(certificates[-1])
        if idpClaims:
            res["idpClaims"] = idpClaims
        userClaims = extract_extra_claims(assertion)
        if userClaims:
            res["userClaims"] = userClaims
        return res
Exemple #8
0
class RemoteVerifier(Verifier):
    """Class for remote verification of BrowserID identity assertions.

    This class submits assertions to the browserid.org verifier service
    for remote verification.  It's slower but potentially a little bit
    safer than the still-under-development LocalVerifier class.
    """
    def __init__(self, audiences=None, verifier_url=None):
        if verifier_url is None:
            verifier_url = BROWSERID_VERIFIER_URL
        super(RemoteVerifier, self).__init__(audiences)
        self.verifier_url = verifier_url

    def verify(self, assertion, audience=None):
        """Verify the given BrowserID assertion.

        This method posts the given BrowserID assertion to the remote verifier
        service.  If it is successfully verified then a dict giving the
        email and audience is returned.  If it is not valid then an error
        is raised.

        If the 'audience' argument is given, it first verifies that the
        audience of the assertion matches the one given.  This can help
        avoid doing lots of crypto for assertions that can't be valid.
        If you don't specify an audience, you *MUST* validate the audience
        value returned by this method.
        """
        # Check the audience locally.
        # No point talking to the network if we know it's going to fail.
        # If no explicit audience was given, this will also parse it out
        # for inclusion in the request to the remote verifier service.
        audience = self.check_audience(assertion, audience)
        # Encode the data into x-www-form-urlencoded.
        post_data = {"assertion": assertion, "audience": audience}
        post_data = "&".join("%s=%s" % item for item in post_data.items())
        # Post it to the verifier.
        try:
            resp = secure_urlopen(self.verifier_url, post_data)
        except ConnectionError, e:
            # BrowserID server sends "500 server error" for broken assertions.
            # For now, just translate that directly.  Should check by hand.
            if "500" in str(e):
                raise ValueError("Malformed assertion")
            raise
        # Read the response, being careful to raise an appropriate
        # error if the server does something funny.
        try:
            try:
                info = resp.info()
            except AttributeError:
                info = {}
            content_length = info.get("Content-Length")
            if content_length is None:
                data = resp.read()
            else:
                data = resp.read(int(content_length))
            data = json.loads(data)
        except ValueError:
            raise ConnectionError("server returned invalid response")
        # Did it come back clean?
        if data.get('status') != "okay":
            raise InvalidSignatureError(str(data))
        if data.get('audience') != audience:
            raise AudienceMismatchError(data.get("audience"), audience)
        return data