Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

move to a library and add in cert validation

  • Loading branch information...
commit 7b331662bf208cf3c6ebd71cc68561f82600ce79 1 parent a4fa872
Andy McKay authored
241 docs/conf.py
... ... @@ -0,0 +1,241 @@
  1 +# -*- coding: utf-8 -*-
  2 +#
  3 +# receipts documentation build configuration file, created by
  4 +# sphinx-quickstart on Fri Apr 27 10:08:34 2012.
  5 +#
  6 +# This file is execfile()d with the current directory set to its containing dir.
  7 +#
  8 +# Note that not all possible configuration values are present in this
  9 +# autogenerated file.
  10 +#
  11 +# All configuration values have a default; values that are commented out
  12 +# serve to show the default.
  13 +
  14 +import sys, os
  15 +
  16 +# If extensions (or modules to document with autodoc) are in another directory,
  17 +# add these directories to sys.path here. If the directory is relative to the
  18 +# documentation root, use os.path.abspath to make it absolute, like shown here.
  19 +#sys.path.insert(0, os.path.abspath('.'))
  20 +
  21 +# -- General configuration -----------------------------------------------------
  22 +
  23 +# If your documentation needs a minimal Sphinx version, state it here.
  24 +#needs_sphinx = '1.0'
  25 +
  26 +# Add any Sphinx extension module names here, as strings. They can be extensions
  27 +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
  28 +extensions = []
  29 +
  30 +# Add any paths that contain templates here, relative to this directory.
  31 +templates_path = ['_templates']
  32 +
  33 +# The suffix of source filenames.
  34 +source_suffix = '.rst'
  35 +
  36 +# The encoding of source files.
  37 +#source_encoding = 'utf-8-sig'
  38 +
  39 +# The master toctree document.
  40 +master_doc = 'index'
  41 +
  42 +# General information about the project.
  43 +project = u'receipts'
  44 +copyright = u'2012, Andy McKay'
  45 +
  46 +# The version info for the project you're documenting, acts as replacement for
  47 +# |version| and |release|, also used in various other places throughout the
  48 +# built documents.
  49 +#
  50 +# The short X.Y version.
  51 +version = '0.2'
  52 +# The full version, including alpha/beta/rc tags.
  53 +release = '0.2'
  54 +
  55 +# The language for content autogenerated by Sphinx. Refer to documentation
  56 +# for a list of supported languages.
  57 +#language = None
  58 +
  59 +# There are two options for replacing |today|: either, you set today to some
  60 +# non-false value, then it is used:
  61 +#today = ''
  62 +# Else, today_fmt is used as the format for a strftime call.
  63 +#today_fmt = '%B %d, %Y'
  64 +
  65 +# List of patterns, relative to source directory, that match files and
  66 +# directories to ignore when looking for source files.
  67 +exclude_patterns = ['_build']
  68 +
  69 +# The reST default role (used for this markup: `text`) to use for all documents.
  70 +#default_role = None
  71 +
  72 +# If true, '()' will be appended to :func: etc. cross-reference text.
  73 +#add_function_parentheses = True
  74 +
  75 +# If true, the current module name will be prepended to all description
  76 +# unit titles (such as .. function::).
  77 +#add_module_names = True
  78 +
  79 +# If true, sectionauthor and moduleauthor directives will be shown in the
  80 +# output. They are ignored by default.
  81 +#show_authors = False
  82 +
  83 +# The name of the Pygments (syntax highlighting) style to use.
  84 +pygments_style = 'sphinx'
  85 +
  86 +# A list of ignored prefixes for module index sorting.
  87 +#modindex_common_prefix = []
  88 +
  89 +
  90 +# -- Options for HTML output ---------------------------------------------------
  91 +
  92 +# The theme to use for HTML and HTML Help pages. See the documentation for
  93 +# a list of builtin themes.
  94 +html_theme = 'default'
  95 +
  96 +# Theme options are theme-specific and customize the look and feel of a theme
  97 +# further. For a list of options available for each theme, see the
  98 +# documentation.
  99 +#html_theme_options = {}
  100 +
  101 +# Add any paths that contain custom themes here, relative to this directory.
  102 +#html_theme_path = []
  103 +
  104 +# The name for this set of Sphinx documents. If None, it defaults to
  105 +# "<project> v<release> documentation".
  106 +#html_title = None
  107 +
  108 +# A shorter title for the navigation bar. Default is the same as html_title.
  109 +#html_short_title = None
  110 +
  111 +# The name of an image file (relative to this directory) to place at the top
  112 +# of the sidebar.
  113 +#html_logo = None
  114 +
  115 +# The name of an image file (within the static path) to use as favicon of the
  116 +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
  117 +# pixels large.
  118 +#html_favicon = None
  119 +
  120 +# Add any paths that contain custom static files (such as style sheets) here,
  121 +# relative to this directory. They are copied after the builtin static files,
  122 +# so a file named "default.css" will overwrite the builtin "default.css".
  123 +html_static_path = ['_static']
  124 +
  125 +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
  126 +# using the given strftime format.
  127 +#html_last_updated_fmt = '%b %d, %Y'
  128 +
  129 +# If true, SmartyPants will be used to convert quotes and dashes to
  130 +# typographically correct entities.
  131 +#html_use_smartypants = True
  132 +
  133 +# Custom sidebar templates, maps document names to template names.
  134 +#html_sidebars = {}
  135 +
  136 +# Additional templates that should be rendered to pages, maps page names to
  137 +# template names.
  138 +#html_additional_pages = {}
  139 +
  140 +# If false, no module index is generated.
  141 +#html_domain_indices = True
  142 +
  143 +# If false, no index is generated.
  144 +#html_use_index = True
  145 +
  146 +# If true, the index is split into individual pages for each letter.
  147 +#html_split_index = False
  148 +
  149 +# If true, links to the reST sources are added to the pages.
  150 +#html_show_sourcelink = True
  151 +
  152 +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
  153 +#html_show_sphinx = True
  154 +
  155 +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
  156 +#html_show_copyright = True
  157 +
  158 +# If true, an OpenSearch description file will be output, and all pages will
  159 +# contain a <link> tag referring to it. The value of this option must be the
  160 +# base URL from which the finished HTML is served.
  161 +#html_use_opensearch = ''
  162 +
  163 +# This is the file name suffix for HTML files (e.g. ".xhtml").
  164 +#html_file_suffix = None
  165 +
  166 +# Output file base name for HTML help builder.
  167 +htmlhelp_basename = 'receiptsdoc'
  168 +
  169 +
  170 +# -- Options for LaTeX output --------------------------------------------------
  171 +
  172 +latex_elements = {
  173 +# The paper size ('letterpaper' or 'a4paper').
  174 +#'papersize': 'letterpaper',
  175 +
  176 +# The font size ('10pt', '11pt' or '12pt').
  177 +#'pointsize': '10pt',
  178 +
  179 +# Additional stuff for the LaTeX preamble.
  180 +#'preamble': '',
  181 +}
  182 +
  183 +# Grouping the document tree into LaTeX files. List of tuples
  184 +# (source start file, target name, title, author, documentclass [howto/manual]).
  185 +latex_documents = [
  186 + ('index', 'receipts.tex', u'receipts Documentation',
  187 + u'Andy McKay', 'manual'),
  188 +]
  189 +
  190 +# The name of an image file (relative to this directory) to place at the top of
  191 +# the title page.
  192 +#latex_logo = None
  193 +
  194 +# For "manual" documents, if this is true, then toplevel headings are parts,
  195 +# not chapters.
  196 +#latex_use_parts = False
  197 +
  198 +# If true, show page references after internal links.
  199 +#latex_show_pagerefs = False
  200 +
  201 +# If true, show URL addresses after external links.
  202 +#latex_show_urls = False
  203 +
  204 +# Documents to append as an appendix to all manuals.
  205 +#latex_appendices = []
  206 +
  207 +# If false, no module index is generated.
  208 +#latex_domain_indices = True
  209 +
  210 +
  211 +# -- Options for manual page output --------------------------------------------
  212 +
  213 +# One entry per manual page. List of tuples
  214 +# (source start file, name, description, authors, manual section).
  215 +man_pages = [
  216 + ('index', 'receipts', u'receipts Documentation',
  217 + [u'Andy McKay'], 1)
  218 +]
  219 +
  220 +# If true, show URL addresses after external links.
  221 +#man_show_urls = False
  222 +
  223 +
  224 +# -- Options for Texinfo output ------------------------------------------------
  225 +
  226 +# Grouping the document tree into Texinfo files. List of tuples
  227 +# (source start file, target name, title, author,
  228 +# dir menu entry, description, category)
  229 +texinfo_documents = [
  230 + ('index', 'receipts', u'receipts Documentation', u'Andy McKay',
  231 + 'receipts', 'One line description of project.', 'Miscellaneous'),
  232 +]
  233 +
  234 +# Documents to append as an appendix to all manuals.
  235 +#texinfo_appendices = []
  236 +
  237 +# If false, no module index is generated.
  238 +#texinfo_domain_indices = True
  239 +
  240 +# How to display URL addresses: 'footnote', 'no', or 'inline'.
  241 +#texinfo_show_urls = 'footnote'
22 docs/index.rst
Source Rendered
... ... @@ -0,0 +1,22 @@
  1 +.. receipts documentation master file, created by
  2 + sphinx-quickstart on Fri Apr 27 10:08:34 2012.
  3 + You can adapt this file completely to your liking, but it should at least
  4 + contain the root `toctree` directive.
  5 +
  6 +Welcome to receipts's documentation!
  7 +====================================
  8 +
  9 +Contents:
  10 +
  11 +.. toctree::
  12 + :maxdepth: 2
  13 +
  14 +
  15 +
  16 +Indices and tables
  17 +==================
  18 +
  19 +* :ref:`genindex`
  20 +* :ref:`modindex`
  21 +* :ref:`search`
  22 +
111 receipts.py
... ... @@ -1,111 +0,0 @@
1   -import json
2   -import os
3   -import sys
4   -import argparse
5   -from gettext import ngettext
6   -import urlparse
7   -import ConfigParser
8   -import jwt
9   -import urllib2
10   -from pprint import pprint
11   -
12   -
13   -directory = os.path.expanduser('~/Library/Application Support/Firefox')
14   -ini = os.path.join(directory, 'profiles.ini')
15   -filename = 'webapps/webapps.json'
16   -server = 'https://receiptcheck-marketplace-dev.allizom.org/verify/%s'
17   -
18   -
19   -class Apps(object):
20   - GREEN = "\033[1m\033[92m"
21   - RED = '\033[1m\033[91m'
22   - RESET = "\x1B[m"
23   -
24   - def __init__(self):
25   - self.data = None
26   - self.path = None
27   -
28   - def _get_domain(self, url):
29   - return urlparse.urlparse(url)[1]
30   -
31   - def _good(self, text):
32   - return '%s%s%s' % (self.GREEN, text, self.RESET)
33   -
34   - def _bad(self, text):
35   - return '%s%s%s' % (self.RED, text, self.RESET)
36   -
37   - def profile(self, profile):
38   - config = ConfigParser.ConfigParser()
39   - config.readfp(open(ini, 'r'))
40   - for section in config.sections():
41   - if (config.has_option(section, 'Name') and
42   - config.get(section, 'Name') == profile):
43   - self.path = os.path.join(directory,
44   - config.get(section, 'Path'),
45   - filename)
46   - break
47   -
48   - if not self.path or not os.path.exists(self.path):
49   - print 'No webapps.json found.'
50   - sys.exit(1)
51   -
52   - self.data = json.loads(open(self.path, 'r').read())
53   -
54   - def list(self, *args):
55   - for k, v in self.data.items():
56   - rcs = len(v.get('receipts', []))
57   - receipt_text = ngettext('1 receipt', '%s receipts' % rcs, rcs)
58   - print u'%s: %s' % (self._good(self._get_domain(v['origin'])),
59   - receipt_text)
60   -
61   - def check(self, domain):
62   - for k, v in self.data.items():
63   - this_domain = self._get_domain(v['origin'])
64   - if not domain or domain == this_domain:
65   - print ('Checking receipts for domain: %s'
66   - % self._good(this_domain))
67   - for r in v.get('receipts', []):
68   - cert, receipt = r.split('~')
69   - result = jwt.decode(receipt.encode('ascii'), verify=False)
70   - verify = result['verify']
71   - print 'Verifying at: %s' % self._get_domain(verify)
72   - try:
73   - response = urllib2.urlopen(result['verify'], r)
74   - except urllib2.URLError, error:
75   - print 'Server returned: %s' % self._bad(error.code)
76   - continue
77   - res = json.loads(response.read())['status']
78   - print 'Server returned: %s' % self._good(res)
79   - print
80   -
81   - def expand(self, domain):
82   - for k, v in self.data.items():
83   - this_domain = self._get_domain(v['origin'])
84   - if not domain or domain == this_domain:
85   - print ('Expanding receipt for domain: %s'
86   - % self._good(this_domain))
87   - receipts = v.pop('receipts')
88   - pprint(v)
89   - for r in receipts:
90   - cert, receipt = r.split('~')
91   - pprint(jwt.decode(cert.encode('ascii'), verify=False))
92   - pprint(jwt.decode(receipt.encode('ascii'), verify=False))
93   -
94   -
95   -def main():
96   - parser = argparse.ArgumentParser()
97   - parser.add_argument('-p', '--profile', nargs='?', default='default')
98   - parser.add_argument('-l', '--list', nargs='?', default=False)
99   - parser.add_argument('-e', '--expand', nargs='?', default=False)
100   - parser.add_argument('-c', '--check', nargs='?', default=False)
101   - result = parser.parse_args()
102   - apps = Apps()
103   - for k in ['profile', 'expand', 'list', 'check']:
104   - v = result.__dict__[k]
105   - if v is not False:
106   - getattr(apps, k)(v)
107   -
108   -
109   -if __name__=='__main__':
110   - main()
111   -
0  receipts/__init__.py
No changes.
170 receipts/certs.py
... ... @@ -0,0 +1,170 @@
  1 +# This is copied from https://github.com/rtilder/trunion/
  2 +import browserid
  3 +import browserid.certificates
  4 +import browserid.jwt
  5 +from browserid.verifiers import local
  6 +from browserid.utils import unbundle_certs_and_assertion
  7 +from browserid.utils import decode_bytes
  8 +from browserid.utils import decode_json_bytes
  9 +from browserid.errors import (ConnectionError, InvalidIssuerError,
  10 + InvalidSignatureError, ExpiredSignatureError)
  11 +
  12 +import requests
  13 +from requests.exceptions import RequestException
  14 +
  15 +from binascii import hexlify
  16 +import time
  17 +import json
  18 +
  19 +
  20 +def fetch_public_key(url, *args):
  21 + """Fetch the public key from the given URL."""
  22 + # Try to find the public key. If it can't be found then we
  23 + # raise an InvalidIssuerError. Any other connection-related
  24 + # errors are passed back up to the caller.
  25 + response = _get(url)
  26 + if response.status_code == 200:
  27 + try:
  28 + try:
  29 + key = parse_jwt(response.text).payload['jwk'][0]
  30 + except ValueError:
  31 + key = json.loads(response.text)['jwk'][0]
  32 + except (ValueError, KeyError):
  33 + raise InvalidIssuerError('Host %r has malformed public key '
  34 + 'document' % url)
  35 + else:
  36 + raise InvalidIssuerError('Can not retrieve key from "%s"' % url)
  37 +
  38 + return key
  39 +
  40 +
  41 +def _get(url):
  42 + """Fetch resource with requests."""
  43 + try:
  44 + return requests.get(url)
  45 + except RequestException, e:
  46 + msg = "Impossible to get %s. Reason: %s" % (url, str(e))
  47 + raise ConnectionError(msg)
  48 +
  49 +
  50 +def parse_jwt(data):
  51 + """Parse a JWT from a string."""
  52 + header, payload, signature = data.split(".")
  53 + signed_data = header + "." + payload
  54 + try:
  55 + header = decode_json_bytes(header)
  56 + except KeyError:
  57 + raise ValueError("badly formed JWT header")
  58 + payload = decode_json_bytes(payload)
  59 + signature = decode_bytes(signature)
  60 + return ReceiptJWT(header, payload, signature, signed_data)
  61 +
  62 +
  63 +def jwt_cert_to_key(jwtoken):
  64 + """Converts a JWT encapsulated JWK key into
  65 + something usable by PyBrowserID.jwt"""
  66 + if type(jwtoken) != dict:
  67 + jwtoken = parse_jwt(jwtoken)
  68 + return jwk_to_key(jwtoken.payload['jwk'][0], jwtoken.header['alg'])
  69 +
  70 +
  71 +def jwk_to_key(jwk, alg):
  72 + """Quick'n'simple format conversion"""
  73 + jwk['e'] = long(hexlify(browserid.jwt.decode_bytes(jwk['exp'])), 16)
  74 + jwk['n'] = long(hexlify(browserid.jwt.decode_bytes(jwk['mod'])), 16)
  75 + return browserid.jwt.load_key(alg, jwk)
  76 +
  77 +
  78 +class ReceiptJWT(browserid.jwt.JWT):
  79 + """Class to override PyBrowserID's JWT parser"""
  80 +
  81 + def __init__(self, header, payload, signature, signed_data):
  82 + self.header = header
  83 + self.algorithm = header['alg']
  84 + self.payload = payload
  85 + self.signature = signature
  86 + self.signed_data = signed_data
  87 +
  88 + def check_signature(self, key_data):
  89 + """Do proper parsing of a JWS signed JWT as defined by
  90 + http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-01"""
  91 + if not self.algorithm.startswith(key_data["alg"][0:1]):
  92 + return False
  93 + key = jwk_to_key(key_data, self.algorithm)
  94 + return key.verify(self.signed_data, self.signature)
  95 +
  96 +
  97 +class ReceiptVerifier(local.LocalVerifier):
  98 +
  99 + def parse_jwt(self, data):
  100 + return parse_jwt(data)
  101 +
  102 + def verify(self, assertion, audience=None, now=None):
  103 + """Verify the certificate chain for the receipt
  104 + """
  105 + if now is None:
  106 + now = int(time.time())
  107 +
  108 + # This catches KeyError and turns it into ValueError.
  109 + # It saves having to test for the existence of individual
  110 + # items in the various payloads.
  111 + try:
  112 + # Grab the assertion, check that it has not expired.
  113 + # No point doing all that crypto if we're going to fail out anyway.
  114 + certificates, assertion = unbundle_certs_and_assertion(assertion)
  115 + assertion = self.parse_jwt(assertion)
  116 + if assertion.payload["exp"] < now:
  117 + raise ExpiredSignatureError(assertion.payload["exp"])
  118 +
  119 + # Parse out the list of certificates.
  120 + certificates = [self.parse_jwt(c) for c in certificates]
  121 +
  122 + # Verify the entire chain of certificates.
  123 + cert = self.verify_certificate_chain(certificates, now=now)
  124 +
  125 + # Check the signature on the assertion.
  126 + if not self.check_token_signature(assertion, cert):
  127 + raise InvalidSignatureError("invalid signature on assertion")
  128 + except KeyError:
  129 + raise ValueError("Malformed JWT")
  130 + # Looks good!
  131 + return True
  132 +
  133 + def check_token_signature(self, data, cert):
  134 + return data.check_signature(cert.payload["jwk"][0])
  135 +
  136 + def verify_certificate_chain(self, certificates, now=None):
  137 + """Verify a signed chain of certificates.
  138 +
  139 + This function checks the signatures on the given chain of JWT
  140 + certificates. It looks up the public key for the issuer of the
  141 + first certificate, then uses each certificate in turn to check the
  142 + signature on its successor.
  143 +
  144 + If the entire chain is valid then to final certificate is returned.
  145 + """
  146 + if not certificates:
  147 + raise ValueError("chain must have at least one certificate")
  148 + if now is None:
  149 + now = int(time.time())
  150 + root_issuer = certificates[0].payload["iss"]
  151 + root_key = self.certs[root_issuer]
  152 + current_key = root_key
  153 + for cert in certificates:
  154 + if cert.payload["exp"] < now:
  155 + raise ExpiredSignatureError("expired certificate in "
  156 + "chain: %s < %s" %
  157 + (cert.payload['exp'], now))
  158 + if not cert.check_signature(current_key):
  159 + raise InvalidSignatureError("bad signature in chain by: '%s'"
  160 + % current_key['kid'])
  161 + current_key = cert.payload["jwk"][0]
  162 + return cert
  163 +
  164 +#
  165 +# MONKEY PATCH TIME!
  166 +#
  167 +browserid.certificates.fetch_public_key_orig = (browserid.certificates
  168 + .fetch_public_key)
  169 +browserid.certificates.fetch_public_key = fetch_public_key
  170 +
111 receipts/fx.py
... ... @@ -0,0 +1,111 @@
  1 +import argparse
  2 +import ConfigParser
  3 +from gettext import ngettext
  4 +import json
  5 +from pprint import pprint
  6 +import os
  7 +import sys
  8 +import warnings
  9 +
  10 +from receipts import Install, VerificationError
  11 +
  12 +
  13 +directory = os.path.expanduser('~/Library/Application Support/Firefox')
  14 +ini = os.path.join(directory, 'profiles.ini')
  15 +filename = 'webapps/webapps.json'
  16 +
  17 +
  18 +class Firefox(object):
  19 + GREEN = "\033[1m\033[92m"
  20 + RED = '\033[1m\033[91m'
  21 + RESET = "\x1B[m"
  22 +
  23 + def __init__(self):
  24 + self.data = None
  25 + self.path = None
  26 + self.installs = []
  27 +
  28 + def _good(self, text):
  29 + return '%s%s%s' % (self.GREEN, text, self.RESET)
  30 +
  31 + def _bad(self, text):
  32 + return '%s%s%s' % (self.RED, text, self.RESET)
  33 +
  34 + def profile(self, profile):
  35 + config = ConfigParser.ConfigParser()
  36 + config.readfp(open(ini, 'r'))
  37 + for section in config.sections():
  38 + if (config.has_option(section, 'Name') and
  39 + config.get(section, 'Name') == profile):
  40 + self.path = os.path.join(directory,
  41 + config.get(section, 'Path'),
  42 + filename)
  43 + break
  44 +
  45 + if not self.path or not os.path.exists(self.path):
  46 + print 'No webapps.json found.'
  47 + sys.exit(1)
  48 +
  49 + for k, v in json.loads(open(self.path).read()).items():
  50 + self.installs.append(Install(v))
  51 +
  52 + def list(self, *args):
  53 + for i in self.installs:
  54 + rcs = len(i.receipts)
  55 + receipt_text = ngettext('1 receipt', '%s receipts' % rcs, rcs)
  56 + print u'%s: %s' % (self._good(i.origin), receipt_text)
  57 +
  58 + def check(self, domain):
  59 + for i in self.installs:
  60 + if not domain or domain == i.origin:
  61 + print 'Checking receipts for domain: %s' % self._good(i.origin)
  62 + for r in i.receipts:
  63 + print 'Verifying at: %s' % r.verifier
  64 + try:
  65 + res = r.verify_server()['status']
  66 + except VerificationError, error:
  67 + print 'Server error: %s' % self._bad(error)
  68 + else:
  69 + print 'Server returned: %s' % self._good(res)
  70 + try:
  71 + res = r.verify_crypto()
  72 + except VerificationError, error:
  73 + print 'Validity error: %s' % self._bad(error)
  74 + else:
  75 + states = {True: self._good('good'),
  76 + False: self._bad('bad')}
  77 + print 'Validity check: %s' % states[res]
  78 + print
  79 +
  80 + def expand(self, domain):
  81 + for i in self.installs:
  82 + if not domain or domain == i.origin:
  83 + print 'Expanding receipt for domain: %s' % self._good(i.origin)
  84 + print self._good('Install data')
  85 + pprint(i.data)
  86 + for r in i.receipts:
  87 + if r.cert:
  88 + print self._good('Certificate data')
  89 + pprint(r.cert_decoded)
  90 + print self._good('Receipt data')
  91 + pprint(r.receipt_decoded)
  92 + print
  93 +
  94 +
  95 +def main():
  96 + warnings.filterwarnings("ignore", category=FutureWarning)
  97 + parser = argparse.ArgumentParser()
  98 + parser.add_argument('-p', '--profile', nargs='?', default='default')
  99 + parser.add_argument('-l', '--list', nargs='?', default=False)
  100 + parser.add_argument('-e', '--expand', nargs='?', default=False)
  101 + parser.add_argument('-c', '--check', nargs='?', default=False)
  102 + result = parser.parse_args()
  103 + apps = Firefox()
  104 + for k in ['profile', 'expand', 'list', 'check']:
  105 + v = result.__dict__[k]
  106 + if v is not False:
  107 + getattr(apps, k)(v)
  108 +
  109 +
  110 +if __name__=='__main__':
  111 + main()
67 receipts/receipts.py
... ... @@ -0,0 +1,67 @@
  1 +import argparse
  2 +import ConfigParser
  3 +from gettext import ngettext
  4 +import json
  5 +import jwt
  6 +from pprint import pprint
  7 +import os
  8 +import sys
  9 +import urlparse
  10 +
  11 +import requests
  12 +from requests.exceptions import RequestException
  13 +
  14 +import certs
  15 +
  16 +
  17 +class VerificationError(Exception):
  18 + pass
  19 +
  20 +
  21 +class Receipt(object):
  22 +
  23 + def __init__(self, data):
  24 + self.receipt = ''
  25 + self.cert = ''
  26 + self.full = data
  27 + if '~' in data:
  28 + self.cert, self.receipt = data.split('~')
  29 + else:
  30 + self.receipt = data
  31 +
  32 + def cert_decoded(self):
  33 + return jwt.decode(self.cert.encode('ascii'), verify=False)
  34 +
  35 + def receipt_decoded(self):
  36 + return jwt.decode(self.receipt.encode('ascii'), verify=False)
  37 +
  38 + @property
  39 + def verifier(self):
  40 + return self.receipt_decoded()['verify']
  41 +
  42 + def verify_server(self):
  43 + try:
  44 + response = requests.post(self.verifier, self.receipt)
  45 + except RequestException, error:
  46 + raise VerificationError(error)
  47 + return json.loads(response.text)
  48 +
  49 + def verify_crypto(self):
  50 + try:
  51 + return certs.ReceiptVerifier().verify(self.full)
  52 + except Exception, error:
  53 + raise VerificationError(error)
  54 +
  55 +
  56 +class Install(object):
  57 +
  58 + def __init__(self, data):
  59 + self.data = data
  60 + self.receipts = []
  61 + for r in self.data.get('receipts', []):
  62 + self.receipts.append(Receipt(r))
  63 + del self.data['receipts']
  64 +
  65 + @property
  66 + def origin(self):
  67 + return urlparse.urlparse(self.data['origin'])[1]
11 setup.py
... ... @@ -1,23 +1,22 @@
1   -import os
2   -from setuptools import setup
3   -
  1 +from setuptools import setup, find_packages
4 2
5 3 setup(
6 4 name='receipts',
7   - version='0.1.6',
  5 + version='0.2',
8 6 description='Verify web app receipts',
9 7 long_description=open('readme.rst').read(),
10 8 author='Andy McKay',
11 9 author_email='andym@mozilla.com',
12 10 license='BSD',
13   - install_requires=['pyjwt'],
  11 + install_requires=['pyjwt', 'requests', 'PyBrowserID'],
14 12 py_modules=['receipts'],
15 13 entry_points={
16 14 'console_scripts': [
17   - 'receipts = receipts:main'
  15 + 'receipts = receipts.fx:main'
18 16 ]
19 17 },
20 18 url='https://github.com/andymckay/receipts',
  19 + packages=find_packages(exclude=['ez_setup']),
21 20 include_package_data=True,
22 21 zip_safe=False,
23 22 classifiers=[

0 comments on commit 7b33166

Please sign in to comment.
Something went wrong with that request. Please try again.