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
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
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
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
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
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