aboutsummaryrefslogtreecommitdiff
path: root/potpourri/rrdp-fetch-from-tal
blob: 08d245dd443eca47ed2eb88384d75c3f5f1e8085 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#!/usr/bin/env python
# $Id$
#
# Copyright (C) 2014  Dragon Research Labs ("DRL")
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND DRL DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS.  IN NO EVENT SHALL DRL BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.

"""
Fetch RPKI data using RRDP starting from a TAL.

Work in progress, don't be too surprised by anything this does or
doesn't do.
"""

import rpki.relaxng
import rpki.x509
import lxml.etree
import argparse
import urlparse
import urllib2            
import sys
import os


class Tags(object):
  def __init__(self, *tags):
    for tag in tags:
      setattr(self, tag, rpki.relaxng.rrdp.xmlns + tag)

tags = Tags("notification", "delta", "snapshot", "publish", "withdraw")


class RSyncHandler(urllib2.BaseHandler):
  """
  Jam support for rsync:// URIs into urllib2 framework.
  Very basic, probably not paranoid enough.
  """

  _n = 0

  def rsync_open(self, req):
    import subprocess, mimetools
    u = req.get_full_url()
    if u.endswith("/"):
      raise urllib2.URLError("rsync directory URI not allowed")
    t = "/tmp/rrdp-fetch-from-tal.%d.%d" % (os.getpid(), self._n)
    self._n += 1
    subprocess.check_call(("rsync", u, t))
    h = mimetools.Message(open("/dev/null"))
    h["Content-type"] = "text/plain"
    h["Content-length"] = str(os.stat(t).st_size)
    f = open(t, "rb")
    os.unlink(t)
    return urllib2.addinfourl(f, h, u)

urllib2.install_opener(urllib2.build_opener(RSyncHandler))


class main(object):

  def __init__(self):
    parser = argparse.ArgumentParser(description = __doc__)
    parser.add_argument("--rcynic-tree", default = "rcynic-data/unauthenticated",
                        help = "directory tree in which to write extracted RPKI objects")
    parser.add_argument("--serial-filename", # default handled later
                        help = "file name in which to store RRDP serial number")
    parser.add_argument("tal", help = "trust anchor locator")
    self.args = parser.parse_args()
    if not os.path.isdir(self.args.rcynic_tree):
      os.makedirs(self.args.rcynic_tree)
    self.urls = set()
    self.ta = self.ta_fetch()
    url = self.ta.get_sia_rrdp_notify()
    if url is None:
      sys.exit("Couldn't get RRDP URI from trust anchor")
    self.rrdp_fetch(url)
    self.write_ta()

  def rrdp_fetch(self, url):
    if url in self.urls:
      print "Already fetched %s, skipping" % url
      return
    self.urls.add(url)
    xml = lxml.etree.ElementTree(file = urllib2.urlopen(url)).getroot()
    rpki.relaxng.rrdp.assertValid(xml)
    if xml.tag[len(rpki.relaxng.rrdp.xmlns):] != "notification":
      sys.exit("Expected notification at %s, found %s" % (url, xml.tag))
    self.prettyprint_notification(xml)

    # We should be checking session_id here, but we're not storing it yet

    old_serial = self.get_serial()
    new_serial = int(xml.get("serial"))
    deltas = dict((int(elt.get("serial")), elt)
                  for elt in xml.iterchildren(tags.delta))
    if old_serial == 0 or not all(serial + 1 in deltas
                                  for serial in xrange(old_serial, new_serial)):
      return self.snapshot_fetch(xml.iterchildren(tags.snapshot).next())
    for serial in sorted(deltas):
      if serial > old_serial:
        self.delta_fetch(deltas[serial])

  def prettyprint_notification(self, xml):
    print "Notification version %s session %s serial %s" % (
      xml.get("version"), xml.get("session_id"), xml.get("serial"))
    elt = xml.iterchildren(tags.snapshot).next()
    print " Snapshot URI %s hash %s" % (
      elt.get("uri"), elt.get("hash"))
    for elt in xml.iterchildren(tags.delta):
      print " Delta %6s URI %s hash %s" % (
        elt.get("serial"), elt.get("uri"), elt.get("hash"))

  def ta_fetch(self):
    with open(self.args.tal, "r") as f:
      tal = f.read()
    uris, key = tal.split("\n\n", 2)
    key = rpki.x509.PublicKey(Base64 = key)
    for uri in uris.split():
      ta = rpki.x509.X509(DER = urllib2.urlopen(uri).read())
      if ta.getPublicKey() == key:
        return ta
      print "TAL key mismatch for certificate", url
    sys.exit("Could not fetch trust anchor")

  @property
  def serial_filename(self):
    return self.args.serial_filename or os.path.join(self.args.rcynic_tree, "serial")

  def get_serial(self):
    try:
      with open(self.serial_filename, "r") as f:
        return int(f.read().strip())
    except:
      return 0

  def set_serial(self, value):
    with open(self.serial_filename, "w") as f:
      f.write("%s\n" % value)

  def uri_to_filename(self, uri):
    assert uri.startswith("rsync://")
    return os.path.join(self.args.rcynic_tree, uri[len("rsync://"):])

  def add_obj(self, uri, obj):
    fn = self.uri_to_filename(uri)
    dn = os.path.dirname(fn)
    if not os.path.isdir(dn):
      os.makedirs(dn)
    with open(fn, "wb") as f:
      f.write(obj)

  def del_obj(self, uri, hash):
    fn = self.uri_to_filename(uri)
    with open(fn, "rb") as f:
      if hash.lower() != rpki.x509.sha256(f.read()).encode("hex"):
        raise RuntimeError("Hash mismatch for URI %s" % uri)
    os.unlink(fn)
    dn = os.path.dirname(fn)
    while True:
      try:
        os.rmdir(dn)
      except OSError:
        break
      else:
        dn = os.path.dirname(dn)

  def xml_fetch(self, elt):
    url = elt.get("uri")
    hash = elt.get("hash").lower()
    print "Fetching", url
    text = urllib2.urlopen(url).read()
    h = rpki.x509.sha256(text).encode("hex")
    if h != hash:
      sys.exit("Bad hash for %s: expected %s got %s" % (url, hash, h))
    xml = lxml.etree.XML(text)
    rpki.relaxng.rrdp.schema.assertValid(xml)
    return xml

  def snapshot_fetch(self, xml):
    xml = self.xml_fetch(xml)
    print "Unpacking snapshot version %s session %s serial %6s" % (
      xml.get("version"), xml.get("session_id"), xml.get("serial"))
    for elt in xml.iterchildren(tags.publish):
      print " ", elt.get("uri")
      self.add_obj(elt.get("uri"), elt.text.decode("base64"))
    self.set_serial(xml.get("serial"))

  def delta_fetch(self, xml):
    xml = self.xml_fetch(xml)
    old_serial = int(self.get_serial())
    new_serial = int(xml.get("serial"))
    print "Unpacking deltas version %s session %s serial %s" % (
      xml.get("version"), xml.get("session_id"), new_serial)
    if old_serial != new_serial - 1:
      raise RuntimeError("Can't apply deltas: old serial %s new serial %s" % (old_serial, new_serial))
    for i, elt in enumerate(xml.iterchildren(tags.withdraw)):
      uri = elt.get("uri")
      hash = elt.get("hash")
      print "  %3d withdraw URI %s hash %s" % (i, uri, hash)
      self.del_obj(uri, hash)
    for i, elt in enumerate(xml.iterchildren(tags.publish)):
      uri = elt.get("uri")
      hash = elt.get("hash", None)
      print "  %3d publish  URI %s hash %s" % (i, uri, hash)
      if hash is not None:
        self.del_obj(uri, hash)
      self.add_obj(elt.get("uri"), elt.text.decode("base64"))
    self.set_serial(new_serial)

  def write_ta(self):
    der = self.ta.get_DER()
    fn = rpki.x509.sha256(der).encode("hex") + ".cer"
    if not os.path.exists(fn):
      print "Writing", fn
      with open(fn, "wb") as f:
        f.write(der)

if __name__ == "__main__":
  main()