Advertisement
Guest User

Untitled

a guest
Sep 21st, 2017
476
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 60.96 KB | None | 0 0
  1. # Copyright (C) 2008, 2009, 2010 Red Hat, Inc.  All rights reserved.
  2. #
  3. # This copyrighted material is made available to anyone wishing to use, modify,
  4. # copy, or redistribute it subject to the terms and conditions of the GNU
  5. # General Public License v.2.  This program is distributed in the hope that it
  6. # will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
  7. # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  8. # See the GNU General Public License for more details.  You should have
  9. # received a copy of the GNU General Public License along with this program; if
  10. # not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
  11. # Floor, Boston, MA 02110-1301, USA.  Any Red Hat trademarks that are
  12. # incorporated in the source code or documentation are not subject to the GNU
  13. # General Public License and may only be used or replicated with the express
  14. # permission of Red Hat, Inc.
  15. #
  16. # Red Hat Author: Miloslav Trmac <mitr@redhat.com>
  17.  
  18. import Queue
  19. import binascii
  20. import cStringIO
  21. import crypt
  22. import logging
  23. import os
  24. import shutil
  25. import signal
  26. import socket
  27. import string
  28. import struct
  29. import subprocess
  30. import sys
  31. import tempfile
  32. import time
  33.  
  34. import gpgme
  35. import nss.error
  36. import nss.nss
  37. import pexpect
  38.  
  39. import double_tls
  40. import errors
  41. import server_common
  42. import settings
  43. import utils
  44.  
  45. # When trying to connect to the bridge, don't repeat the connections way too
  46. # often.  Try MAX_FAST_RECONNECTIONS attempts FAST_RECONNECTION_SECONDS apart,
  47. # then wait SLOW_RECONNECTION_SECONDS.  Then try again
  48. MAX_FAST_RECONNECTIONS = 5
  49. FAST_RECONNECTION_SECONDS = 5
  50. SLOW_RECONNECTION_SECONDS = 60
  51.  
  52. # Infrastructure
  53.  
  54. Key = server_common.Key
  55. KeyAccess = server_common.KeyAccess
  56. User = server_common.User
  57.  
  58. class ServerConfiguration(server_common.GPGConfiguration,
  59.                           server_common.ServerBaseConfiguration):
  60.  
  61.     def _add_defaults(self, defaults):
  62.         super(ServerConfiguration, self)._add_defaults(defaults)
  63.         defaults.update({'bridge-port': 44333,
  64.                          'gnupg-key-type': 'DSA',
  65.                          'gnupg-key-length': 1024,
  66.                          'gnupg-subkey-type': 'ELG-E',
  67.                          'gnupg-subkey-length': 2048,
  68.                          'gnupg-key-usage': 'sign',
  69.                          'max-file-payload-size': 1024 * 1024 * 1024,
  70.                          'max-memory-payload-size': 1024 * 1024,
  71.                          'max-rpms-payloads-size': 10 * 1024 * 1024 * 1024,
  72.                          'passphrase-length': 64,
  73.                          'server-cert-nickname': 'sigul-server-cert',
  74.                          'signing-timeout': 60})
  75.  
  76.     def _add_sections(self, sections):
  77.         super(ServerConfiguration, self)._add_sections(sections)
  78.         sections.update(('gnupg','server'))
  79.  
  80.     def _read_configuration(self, parser):
  81.         super(ServerConfiguration, self)._read_configuration(parser)
  82.         self.gnupg_key_type = parser.get('gnupg', 'gnupg-key-type')
  83.         self.gnupg_key_length = parser.getint('gnupg', 'gnupg-key-length')
  84.         self.gnupg_subkey_type = parser.get('gnupg', 'gnupg-subkey-type')
  85.         if self.gnupg_subkey_type == '':
  86.             self.gnupg_subkey_type = None
  87.         else:
  88.             self.gnupg_subkey_length = parser.getint('gnupg',
  89.                                                      'gnupg-subkey-length')
  90.         self.gnupg_key_usage = parser.get('gnupg', 'gnupg-key-usage')
  91.         self.passphrase_length = parser.getint('gnupg', 'passphrase-length')
  92.         self.bridge_hostname = parser.get('server', 'bridge-hostname')
  93.         self.bridge_port = parser.getint('server', 'bridge-port')
  94.         self.max_file_payload_size = parser.getint('server',
  95.                                                    'max-file-payload-size')
  96.         self.max_memory_payload_size = parser.getint('server',
  97.                                                      'max-memory-payload-size')
  98.         self.max_rpms_payloads_size = parser.getint('server',
  99.                                                     'max-rpms-payloads-size')
  100.         self.server_cert_nickname = parser.get('server', 'server-cert-nickname')
  101.         self.signing_timeout = parser.getint('server', 'signing-timeout')
  102.  
  103. class RequestHandled(Exception):
  104.     '''Used to terminate further processing of the request.'''
  105.     pass
  106.  
  107. class InvalidRequestError(Exception):
  108.     pass
  109.  
  110. class RequestHandler(object):
  111.     '''Information about a request type and its handler.'''
  112.  
  113.     PAYLOAD_NONE = 0
  114.     PAYLOAD_MEMORY = 1
  115.     PAYLOAD_FILE = 2
  116.  
  117.     def __init__(self, handler, payload_storage=PAYLOAD_NONE,
  118.                  payload_auth_optional=False):
  119.         self.handler = handler
  120.         self.payload_storage = payload_storage
  121.         self.payload_auth_optional = payload_auth_optional
  122.  
  123. # op value => (handler, expected payload type)
  124. # Each handler raises RequestHandled, InvalidRequestError.  op value None means
  125. # the default handler
  126. request_handlers = {}
  127.  
  128. def request_handler(**kwargs):
  129.     '''Register this function as a request handler, using kwargs.
  130.  
  131.    Function name must be cmd_request_name_with_dash_replaced_by_underscore,
  132.    e.g. cmd_list_users for 'list-users'.  This decorator must be used with
  133.    (possibly zero) parameters.
  134.  
  135.    '''
  136.     def real_decorator(fn):
  137.         assert fn.__name__.startswith('cmd_')
  138.         request_handlers[fn.__name__[len('cmd_'):].replace('_', '-')] = \
  139.             RequestHandler(fn, **kwargs)
  140.         return fn
  141.     return real_decorator
  142.  
  143. class ServerProxy(object):
  144.     '''A proxy for double_tls.DoubleTLSClient that stores read outer data.'''
  145.  
  146.     def __init__(self, server):
  147.         self.__server = server
  148.         self.__stored = ''
  149.  
  150.     def stored_outer_read(self, bytes):
  151.         data = self.__server.outer_read(bytes)
  152.         self.__stored += data
  153.         return data
  154.  
  155.     def stored_data(self):
  156.         '''Return the currently stored data.
  157.  
  158.        Each piece of data is returned only once in repeated calls to this
  159.        method.
  160.  
  161.        '''
  162.         res = self.__stored
  163.         self.__stored = ''
  164.         return res
  165.  
  166. class ServersConnection(object):
  167.     '''A connection to the bridge/client.'''
  168.  
  169.     def __init__(self, config):
  170.         self.config = config
  171.         self.__client = double_tls.DoubleTLSClient(config,
  172.                                                    config.bridge_hostname,
  173.                                                    config.bridge_port,
  174.                                                    config.server_cert_nickname)
  175.         self.payload_path = None
  176.         self.payload_file = None
  177.         utils.nss_init(config) # May raise utils.NSSInitError
  178.  
  179.     def outer_field(self, key, required=False):
  180.         '''Return an outer field value, or None if not present.
  181.  
  182.        Raise InvalidRequestError if field is not present and required == True.
  183.  
  184.        '''
  185.         v = self.__outer_fields.get(key)
  186.         if required and v is None:
  187.             raise InvalidRequestError('Required outer field %s missing' % key)
  188.         return v
  189.  
  190.     def safe_outer_field(self, key, **kwargs):
  191.         '''Return an outer field value, or None if not present.
  192.  
  193.        Raise InvalidRequestError if field is not a safe string.
  194.  
  195.        '''
  196.         v = self.outer_field(key, **kwargs)
  197.         if v is not None and not utils.string_is_safe(v):
  198.             raise InvalidRequestError('Field %s has unsafe value' % repr(key))
  199.         return v
  200.  
  201.     def outer_field_bool(self, key):
  202.         '''Return outer field value as a bool or None if not present.
  203.  
  204.        Raise InvalidRequestError.
  205.  
  206.        '''
  207.         v = self.__outer_fields.get(key)
  208.         if v is not None:
  209.             try:
  210.                 v = utils.u32_unpack(v)
  211.             except struct.error:
  212.                 raise InvalidRequestError('Integer field has incorrect length')
  213.             try:
  214.                 v = { 0: False, 1: True }[v]
  215.             except KeyError:
  216.                 raise InvalidRequestError('Boolean field has invalid value')
  217.         return v
  218.  
  219.     def inner_field(self, key, required=False):
  220.         '''Return an inner field value, or None if not present.
  221.  
  222.        Raise InvalidRequestError if fiels is not present and required == True.
  223.  
  224.        '''
  225.         v = self.__inner_fields.get(key)
  226.         if required and v is None:
  227.             raise InvalidRequestError('Required inner field %s missing.' % key)
  228.         return v
  229.  
  230.     def read_request(self):
  231.         '''Read a request.
  232.  
  233.        Return request handler.  Raise RequestHandled, InvalidRequestError,
  234.        double_tls.InnerCertificateNotFound.
  235.  
  236.        '''
  237.         proxy = ServerProxy(self.__client)
  238.         buf = proxy.stored_outer_read(utils.u32_size)
  239.         logging.debug('Started processing a request')
  240.         client_version = utils.u32_unpack(buf)
  241.         if client_version != utils.protocol_version:
  242.             logging.warning('Unknown protocol version %d in request',
  243.                             client_version)
  244.             self.__client.inner_close()
  245.             self.__client.outer_write(utils.u32_pack(errors.UNKNOWN_VERSION))
  246.             raise RequestHandled()
  247.         try:
  248.             self.__outer_fields = utils.read_fields(proxy.stored_outer_read)
  249.         except utils.InvalidFieldsError, e:
  250.             raise InvalidRequestError(str(e))
  251.         logging.info('Request: %s', utils.readable_fields(self.__outer_fields))
  252.         header_data = proxy.stored_data()
  253.         buf = self.__client.outer_read(utils.u32_size)
  254.         payload_size = utils.u32_unpack(buf)
  255.  
  256.         request_op = self.safe_outer_field('op', required=True)
  257.         if request_op not in request_handlers:
  258.             request_op = None
  259.         handler = request_handlers[request_op]
  260.  
  261.         reader = utils.SHA512Reader(self.__client.outer_read)
  262.         if handler.payload_storage == RequestHandler.PAYLOAD_NONE:
  263.             if payload_size != 0:
  264.                 raise InvalidRequestError('Unexpected payload')
  265.         elif handler.payload_storage == RequestHandler.PAYLOAD_MEMORY:
  266.             if payload_size > self.config.max_memory_payload_size:
  267.                 raise InvalidRequestError('Payload too large')
  268.             f = cStringIO.StringIO()
  269.             utils.copy_data(f.write, reader.read, payload_size)
  270.             self.__payload = f.getvalue()
  271.         else:
  272.             assert handler.payload_storage == RequestHandler.PAYLOAD_FILE
  273.             if payload_size > self.config.max_file_payload_size:
  274.                 raise InvalidRequestError('Payload too large')
  275.             (fd, self.payload_path) = tempfile.mkstemp(text=False)
  276.             f = os.fdopen(fd, 'w+b')
  277.             try:
  278.                 utils.copy_data(f.write, reader.read, payload_size)
  279.             finally:
  280.                 f.close()
  281.             self.payload_file = open(self.payload_path, 'rb')
  282.         self.payload_sha512_digest = reader.sha512()
  283.  
  284.         # FIXME? authenticate using the client's certificate as well?
  285.         # May raise double_tls.InnerCertificateNotFound.
  286.         self.__client.inner_open_server(self.config.server_cert_nickname)
  287.         try:
  288.             try:
  289.                 self.__inner_fields = utils.read_fields(self.__client.
  290.                                                         inner_read)
  291.             except utils.InvalidFieldsError, e:
  292.                 raise InvalidRequestError(str(e))
  293.         finally:
  294.             self.__client.inner_close()
  295.         # print repr(self.__inner_fields)
  296.         if (self.inner_field('header-auth-sha512', required=True) !=
  297.             nss.nss.sha512_digest(header_data)):
  298.             raise InvalidRequestError('Header authentication failed')
  299.         payload_auth = self.inner_field('payload-auth-sha512')
  300.         if payload_auth is None:
  301.             if not handler.payload_auth_optional:
  302.                 raise InvalidRequestError('Authentication hash missing')
  303.         else:
  304.             if payload_auth != self.payload_sha512_digest:
  305.                 raise InvalidRequestError('Payload authentication failed')
  306.         self.payload_authenticated = payload_auth is not None
  307.  
  308.         mech = nss.nss.CKM_SHA512_HMAC
  309.         slot = nss.nss.get_best_slot(mech)
  310.         buf = self.inner_field('header-auth-key', required=True)
  311.         if len(buf) < 64:
  312.             raise InvalidRequestError('Header authentication key too small')
  313.         # "Unwrap" because the key was encrypted for transmission using TLS
  314.         nss_key = nss.nss.import_sym_key(slot, mech, nss.nss.PK11_OriginUnwrap,
  315.                                          nss.nss.CKA_SIGN, nss.nss.SecItem(buf))
  316.         self.__reply_header_writer = \
  317.             utils.SHA512HMACWriter(self.__client.outer_write, nss_key)
  318.         buf = self.inner_field('payload-auth-key', required=True)
  319.         if len(buf) < 64:
  320.             raise InvalidRequestError('Payload authentication key too small')
  321.         nss_key = nss.nss.import_sym_key(slot, mech, nss.nss.PK11_OriginUnwrap,
  322.                                          nss.nss.CKA_SIGN, nss.nss.SecItem(buf))
  323.         self.__reply_payload_writer = \
  324.             utils.SHA512HMACWriter(self.__client.outer_write, nss_key)
  325.         return handler
  326.  
  327.     def send_reply_header(self, error_code, fields):
  328.         '''Send a reply header to the client.'''
  329.         self.__reply_header_writer.write(utils.u32_pack(error_code))
  330.         self.__reply_header_writer.write(utils.format_fields(fields))
  331.         self.__reply_header_writer.write_64B_hmac()
  332.  
  333.     def __send_payload_size(self, payload_size):
  334.         '''Prepare for sending payload of payload_size to the client.
  335.  
  336.        Valid both for the primary payload and for subreply payloads.
  337.  
  338.        '''
  339.         self.__client.outer_write(utils.u32_pack(payload_size))
  340.  
  341.     def __send_payload_from_file(self, writer, fd):
  342.         '''Send contents of fd to the client as payload, using writer.
  343.  
  344.        Valid both for the primary payload and for subreply payloads.
  345.  
  346.        '''
  347.         fd.seek(0)
  348.         file_size = os.fstat(fd.fileno()).st_size
  349.         self.__send_payload_size(file_size)
  350.  
  351.         sent = 0
  352.         while True:
  353.             data = fd.read(4096)
  354.             if len(data) == 0:
  355.                 break
  356.             writer.write(data)
  357.             sent += len(data)
  358.         if sent != file_size:
  359.             raise IOError('File size did not match size returned by fstat()')
  360.         writer.write_64B_hmac()
  361.  
  362.     def send_reply_payload(self, payload):
  363.         '''Send payload to the client.'''
  364.         self.__send_payload_size(len(payload))
  365.         self.__reply_payload_writer.write(payload)
  366.         self.__reply_payload_writer.write_64B_hmac()
  367.  
  368.     def send_reply_payload_from_file(self, fd):
  369.         '''Send contents of fd to the client as payload.'''
  370.         self.__send_payload_from_file(self.__reply_payload_writer, fd)
  371.  
  372.     def send_reply_ok_only(self):
  373.         '''Send an erorrs.OK reply with no fields or payload.'''
  374.         self.send_reply_header(errors.OK, {})
  375.         self.send_reply_payload('')
  376.  
  377.     def send_error(self, error_code, message=None, log_it=True):
  378.         '''Send an erorr response with code and message.
  379.  
  380.        Raise RequestHandled at the end.
  381.  
  382.        '''
  383.         if message is not None:
  384.             f = {'message': message}
  385.             if log_it:
  386.                 logging.info('Request error: %s, %s',
  387.                              errors.message(error_code), message)
  388.         else:
  389.             f = {}
  390.             if log_it:
  391.                 logging.info('Request error: %s', errors.message(error_code))
  392.         self.send_reply_header(error_code, f)
  393.         self.send_reply_payload('')
  394.         raise RequestHandled()
  395.  
  396.     def read_subheader(self, nss_key):
  397.         '''Read fields in a subrequest header authenticated using nss_key.
  398.  
  399.        Return the header.
  400.  
  401.        '''
  402.         reader = utils.SHA512HMACReader(self.__client.outer_read, nss_key)
  403.         try:
  404.             fields = utils.read_fields(reader.read)
  405.         except utils.InvalidFieldsError, e:
  406.             raise InvalidRequestError('Invalid response format: %s' % str(e))
  407.         if not reader.verify_64B_hmac_authenticator():
  408.             raise InvalidRequestError('Subrequest header authentication failed')
  409.         return fields
  410.  
  411.     def read_subpayload_to_file(self, nss_key, max_size, tmp_dir):
  412.         '''Read a subpayload authenticated using nss_key.
  413.  
  414.        Return (path, file, payload digest, payload authenticated).  Limit file
  415.        size to max_size.  Create the temporary file in tmp_dir.
  416.  
  417.        '''
  418.         buf = self.__client.outer_read(utils.u32_size)
  419.         payload_size = utils.u32_unpack(buf)
  420.         if payload_size > self.config.max_file_payload_size:
  421.             raise InvalidRequestError('Payload too large')
  422.         if payload_size > max_size:
  423.             raise InvalidRequestError('Total payload size too large')
  424.  
  425.         reader = utils.SHA512HashAndHMACReader(self.__client.outer_read,
  426.                                                nss_key)
  427.         (fd, payload_path) = tempfile.mkstemp(text=False, dir=tmp_dir)
  428.         f = os.fdopen(fd, 'w+b')
  429.         try:
  430.             utils.copy_data(f.write, reader.read, payload_size)
  431.         finally:
  432.             f.close()
  433.         payload_file = open(payload_path, 'rb')
  434.         payload_sha512_digest = reader.sha512()
  435.         auth = self.__client.outer_read(64)
  436.         return (payload_path, payload_file, payload_sha512_digest,
  437.                 auth == reader.hmac())
  438.  
  439.     def send_subheader(self, fields, nss_key):
  440.         '''Send fields in a subreply header authenticated using nss_key.'''
  441.         writer = utils.SHA512HMACWriter(self.__client.outer_write, nss_key)
  442.         writer.write(utils.format_fields(fields))
  443.         writer.write_64B_hmac()
  444.  
  445.     def send_empty_subpayload(self, nss_key):
  446.         '''Send an empty subreply payload authenticated using nss_key.'''
  447.         self.__send_payload_size(0)
  448.         writer = utils.SHA512HMACWriter(self.__client.outer_write, nss_key)
  449.         writer.write_64B_hmac()
  450.  
  451.     def send_subpayload_from_file(self, fd, nss_key):
  452.         '''Send a subreply payload from fd authenticated using nss_key.'''
  453.         writer = utils.SHA512HMACWriter(self.__client.outer_write, nss_key)
  454.         self.__send_payload_from_file(writer, fd)
  455.  
  456.     def close(self):
  457.         '''Destroy non-garbage-collected state.
  458.  
  459.        Raise double_tls.ChildConnectionRefusedError,
  460.        double_tls.ChildUnrecoverableError.
  461.  
  462.        '''
  463.         if self.payload_file is not None:
  464.             self.payload_file.close()
  465.         if self.payload_path is not None:
  466.             os.remove(self.payload_path)
  467.         # May raise double_tls.ChildConnectionRefusedError,
  468.         # double_tls.ChildUnrecoverableError.
  469.         self.__client.outer_close()
  470.  
  471.     def auth_fail(self, reason):
  472.         '''Report an authentication failure.
  473.  
  474.        Raise RequestHandled.
  475.  
  476.        '''
  477.         logging.warning('Request authentication failed: %s', reason)
  478.         self.send_error(errors.AUTHENTICATION_FAILED, log_it=False)
  479.  
  480.     def authenticate_admin(self, db):
  481.         '''Check the request is a valid administration request.
  482.  
  483.        Raise RequestHandled (on permission denied), InvalidRequestError.
  484.        '''
  485.  
  486.  
  487.         user = self.safe_outer_field('user')
  488.         if user is None:
  489.             self.auth_fail('user field missing')
  490.         password = self.inner_field('password')
  491.         if password is None:
  492.             self.auth_fail('password field missing')
  493.         user = db.query(User).filter_by(name=user).first()
  494.         if user is not None and user.sha512_password is not None:
  495.             crypted_pw = str(user.sha512_password)
  496.         else:
  497.             # Perform the encryption anyway to make timing attacks more
  498.             # difficult.
  499.             crypted_pw = 'x'
  500.         if crypt.crypt(password, crypted_pw) != crypted_pw:
  501.             self.auth_fail('password does not match')
  502.         if not user.admin:
  503.             self.auth_fail('user is not a server administrator')
  504.         # OK
  505.  
  506.     def __authenticate_admin_or_user(self, db):
  507.         '''Check the request is a valid key access request.
  508.  
  509.        Allow server administrators to authenticate without having a key
  510.        passphrase.  Return (user, key, access), with access None if a server
  511.        administrator was authenticated.  Raise RequestHandled (on permission
  512.        denied), InvalidRequestError.
  513.  
  514.        '''
  515.         user = self.safe_outer_field('user')
  516.         if user is None:
  517.             self.auth_fail('user field missing')
  518.         key = self.safe_outer_field('key')
  519.         if key is None:
  520.             self.auth_fail('key field missing')
  521.         password = self.inner_field('password')
  522.         user_passphrase = self.inner_field('passphrase')
  523.         if password is None and user_passphrase is None:
  524.             self.auth_fail('both password and passphrase fields missing')
  525.         user = db.query(User).filter_by(name=user).first()
  526.         key = db.query(Key).filter_by(name=key).first()
  527.         access = None
  528.         if password is not None:
  529.             if user is not None and user.sha512_password is not None:
  530.                 crypted_pw = str(user.sha512_password)
  531.             else:
  532.                 # Perform the encryption anyway to make timing attacks more
  533.                 # difficult.
  534.                 crypted_pw = 'x'
  535.             if crypt.crypt(password, crypted_pw) != crypted_pw:
  536.                 self.auth_fail('password does not match')
  537.             assert user is not None
  538.             if not user.admin or key is None:
  539.                 self.auth_fail('user is not a server administrator')
  540.         else:
  541.             assert user_passphrase is not None
  542.             encrypted_passphrase = None
  543.             if user is not None and key is not None:
  544.                 access = (db.query(KeyAccess).filter_by(user=user, key=key).
  545.                           first())
  546.                 if access is not None:
  547.                     encrypted_passphrase = access.encrypted_passphrase
  548.             if encrypted_passphrase is None:
  549.                 # Perform a decryption attempt anyway to make timing attacks
  550.                 # more difficult.  gpg will probably choke on the attempt
  551.                 # quickly enough, too bad.
  552.                 encrypted_passphrase = 'x'
  553.             try:
  554.                 server_common.gpg_decrypt(self.config, encrypted_passphrase,
  555.                                           user_passphrase)
  556.             except gpgme.GpgmeError:
  557.                 self.auth_fail('passphrase does not match')
  558.             assert user is not None and key is not None and access is not None
  559.         return (user, key, access) # OK
  560.  
  561.     def authenticate_admin_or_user(self, db):
  562.         '''Check the request is a valid key access request.
  563.  
  564.        Allow server administrators to authenticate without having a key
  565.        passphrase.  Return (user, key).  Raise RequestHandled (on permission
  566.        denied), InvalidRequestError.
  567.  
  568.        '''
  569.         (user, key, _) = self.__authenticate_admin_or_user(db)
  570.         return (user, key)
  571.  
  572.     def authenticate_admin_or_key_admin(self, db):
  573.         '''Check the request is a valid key administration request.
  574.  
  575.        Allow server administrators to authenticate without having a key
  576.        passphrase.  Return (user, key).  Raise RequestHandled (on permission
  577.        denied), InvalidRequestError.
  578.  
  579.        '''
  580.         (user, key, access) = self.__authenticate_admin_or_user(db)
  581.         if access is not None and not access.key_admin:
  582.             self.auth_fail('user is not a key administrator')
  583.         return (user, key)
  584.  
  585.     def authenticate_user(self, db):
  586.         '''Check the request is a valid key access request.
  587.  
  588.        Return a (access, key passphrase).  Raise RequestHandled (on permission
  589.        denied), InvalidRequestError.
  590.  
  591.        '''
  592.         user = self.safe_outer_field('user')
  593.         if user is None:
  594.             self.auth_fail('user field missing')
  595.         key = self.safe_outer_field('key')
  596.         if key is None:
  597.             self.auth_fail('key field missing')
  598.         user_passphrase = self.inner_field('passphrase')
  599.         if user_passphrase is None:
  600.             self.auth_fail('passphrase field missing')
  601.         user = db.query(User).filter_by(name=user).first()
  602.         key = db.query(Key).filter_by(name=key).first()
  603.         encrypted_passphrase = None
  604.         access = None
  605.         if user is not None and key is not None:
  606.             access = db.query(KeyAccess).filter_by(user=user, key=key).first()
  607.             if access is not None:
  608.                 encrypted_passphrase = access.encrypted_passphrase
  609.         if encrypted_passphrase is None:
  610.             # Perform a decryption attempt anyway to make timing attacks more
  611.             # difficult.  gpg will probably choke on the attempt quickly
  612.             # enough, too bad.
  613.             encrypted_passphrase = 'x'
  614.         try:
  615.             key_passphrase = server_common.gpg_decrypt(self.config,
  616.                                                        encrypted_passphrase,
  617.                                                        user_passphrase)
  618.         except gpgme.GpgmeError:
  619.             self.auth_fail('passphrase does not match')
  620.         assert user is not None and key is not None and access is not None
  621.         return (access, key_passphrase)
  622.  
  623.     def authenticate_key_admin(self, db):
  624.         '''Check the request is a valid key administration request.
  625.  
  626.        Return a KeyAccess.  Raise RequestHandled (on permission denied),
  627.        InvalidRequestError.
  628.  
  629.        '''
  630.         (access, key_passphrase) = self.authenticate_user(db)
  631.         if not access.key_admin:
  632.             self.auth_fail('user is not a key administrator')
  633.         return (access, key_passphrase)
  634.  
  635. def key_by_name(db, conn):
  636.     '''Return a key specified by conn.safe_outer_field('key').
  637.  
  638.    Raise InvalidRequestError.
  639.  
  640.    '''
  641.     name = conn.safe_outer_field('key', required=True)
  642.     key = db.query(Key).filter_by(name=name).first()
  643.     if key is None:
  644.         conn.send_error(errors.KEY_NOT_FOUND)
  645.     return key
  646.  
  647. def user_by_name(db, conn):
  648.     '''Return an user specified by conn.safe_outer_field('name').
  649.  
  650.    Raise InvalidRequestError.
  651.  
  652.    '''
  653.     name = conn.safe_outer_field('name', required=True)
  654.     user = db.query(User).filter_by(name=name).first()
  655.     if user is None:
  656.         conn.send_error(errors.USER_NOT_FOUND)
  657.     return user
  658.  
  659. def key_access_by_names(db, conn):
  660.     '''Return a key access specified by conn.safe_outer_field('name'),
  661.    conn.safe_outer_field('key').
  662.  
  663.    Raise InvalidRequestError.
  664.  
  665.    '''
  666.     # Load user and key to provide full diagnostics
  667.     user = user_by_name(db, conn)
  668.     key = key_by_name(db, conn)
  669.     access = db.query(KeyAccess).filter_by(user=user, key=key).first()
  670.     if access is None:
  671.         conn.send_error(errors.KEY_USER_NOT_FOUND)
  672.     return access
  673.  
  674. _passphrase_characters = string.ascii_letters + string.digits
  675. def random_passphrase(conn):
  676.     '''Return a random passphrase.'''
  677.     random = nss.nss.generate_random(conn.config.passphrase_length)
  678.     return ''.join(_passphrase_characters[ord(c) % len(_passphrase_characters)]
  679.                    for c in random)
  680.  
  681. class RPMFileError(Exception):
  682.     pass
  683.  
  684. class RPMFile(object):
  685.     '''A single RPM, to be signed.'''
  686.  
  687.     def __init__(self, path, sha512_digest, request_id=None):
  688.         '''Initialize.
  689.  
  690.        sha512_digest is a SHA-512 digest of path, in binary form.
  691.        self.status is set to None, to be updated by other operations with this
  692.        RPM.
  693.  
  694.        '''
  695.         self.path = path
  696.         self.__sha512_digest = sha512_digest
  697.         self.request_id = request_id
  698.         self.status = None
  699.  
  700.     def verify(self):
  701.         '''Verify validity of the file.
  702.  
  703.        Raise RPMFileError (setting self.status).
  704.  
  705.        '''
  706.         # Use an external process to verify the file, to prevent the attacker
  707.         # from taking control of a process with an open network socket and
  708.         # key_passphrase if a security bug in librpm* is exploitable.
  709.         res = subprocess.call(('rpm', '--quiet', '--nosignature', '-K',
  710.                                self.path),
  711.                               # PIPE is used only to avoid inheriting our file
  712.                               # descriptors
  713.                               stdin=subprocess.PIPE, stdout=subprocess.PIPE,
  714.                               stderr=subprocess.STDOUT, close_fds=True)
  715.         if res != 0:
  716.             self.status = errors.CORRUPT_RPM
  717.             raise RPMFileError('Corrupt RPM')
  718.  
  719.     def read_header(self, fd):
  720.         '''Read file header from fd, which corresponds to self.path.
  721.  
  722.        Set self.rpm_id to a string identifying the RPM.  Raise RPMFileError
  723.        (setting self.status).
  724.  
  725.        '''
  726.         # Don't import rpm at the top of the file!  The rpm Python module calls
  727.         # NSS_NoDB_Init() during its initialization, which breaks our attempts
  728.         # to initialize nss with our certificate database.
  729.         import rpm
  730.  
  731.         ts = rpm.ts()
  732.         ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
  733.         try:
  734.             self.__header = ts.hdrFromFdno(fd.fileno())
  735.         except rpm.error, e:
  736.             self.status = errors.CORRUPT_RPM
  737.             raise RPMFileError('Error reading RPM header: %s' % str(e))
  738.  
  739.         rpm_id = (self.__header[rpm.RPMTAG_NAME],
  740.                   self.__header[rpm.RPMTAG_EPOCH],
  741.                   self.__header[rpm.RPMTAG_VERSION],
  742.                   self.__header[rpm.RPMTAG_RELEASE],
  743.                   self.__header[rpm.RPMTAG_ARCH],
  744.                   binascii.b2a_hex(self.__sha512_digest))
  745.         self.rpm_id = repr(rpm_id)
  746.  
  747.     def authenticate(self, get_field, payload_authenticated):
  748.         '''Verify the file corresponds to the request fields.
  749.  
  750.        Use get_field to read a request field (may return None if missing).
  751.        Raise RPMFileError on missing authentication (setting self.status),
  752.        InvalidRequestError on invalid authentication.
  753.  
  754.        '''
  755.         # Don't import rpm at the top of the file!  The rpm Python module calls
  756.         # NSS_NoDB_Init() during its initialization, which breaks our attempts
  757.         # to initialize nss with our certificate database.
  758.         import rpm
  759.  
  760.         for (field, tag) in (('rpm-name', rpm.RPMTAG_NAME),
  761.                              ('rpm-epoch', rpm.RPMTAG_EPOCH),
  762.                              ('rpm-version', rpm.RPMTAG_VERSION),
  763.                              ('rpm-release', rpm.RPMTAG_RELEASE),
  764.                              ('rpm-arch', rpm.RPMTAG_ARCH)):
  765.             field_value = get_field(field)
  766.             if field_value is None:
  767.                 continue
  768.             if not utils.string_is_safe(field_value):
  769.                 raise InvalidRequestError('Field %s has unsafe value' %
  770.                                           repr(field))
  771.             if (tag == rpm.RPMTAG_ARCH and
  772.                 self.__header[rpm.RPMTAG_SOURCEPACKAGE] == 1):
  773.                 rpm_value = 'src'
  774.             else:
  775.                 rpm_value = self.__header[tag]
  776.                 if rpm_value is None:
  777.                     rpm_value = ''
  778.             if field_value != str(rpm_value):
  779.                 raise InvalidRequestError('RPM mismatch')
  780.  
  781.         field_value = get_field('rpm-sigmd5')
  782.         if field_value is not None:
  783.             rpm_value = self.__header[rpm.RPMTAG_SIGMD5]
  784.             if rpm_value is None or field_value != rpm_value:
  785.                 raise InvalidRequestError('RPM sigmd5 mismatch')
  786.         elif not payload_authenticated:
  787.             self.status = errors.UNAUTHENTICATED_RPM
  788.             raise RPMFileError('RPM not authenticated')
  789.  
  790. class SigningContext(object):
  791.     '''A tool for running rpm --addsign.'''
  792.  
  793.     def __init__(self, conn, key, key_passphrase):
  794.         self.__key = key
  795.         self.__key_passphrase = key_passphrase
  796.         self.__argv = ['--define', '_signature gpg',
  797.                        '--define', '_gpg_name %s' % key.fingerprint]
  798.         field_value = conn.outer_field_bool('v3-signature')
  799.         if field_value is not None and field_value:
  800.             # Add --force-v3-sigs to the value in redhat-rpm-config-9.0.3-3.fc10
  801.             self.__argv += ['--define',
  802.                             '__gpg_sign_cmd %{__gpg} gpg --force-v3-sigs '
  803.                             '--batch --no-verbose --no-armor --passphrase-fd 3 '
  804.                             '--no-secmem-warning -u "%{_gpg_name}" -sbo '
  805.                             '%{__signature_filename} %{__plaintext_filename}']
  806.         self.__env = dict(os.environ) # Shallow copy, uses our $GNUPGHOME
  807.         self.__env['LC_ALL'] = 'C'
  808.  
  809.     def sign_rpm(self, config, rpm):
  810.         '''Sign rpm, using config.
  811.  
  812.        Raise RPMFileError on error.
  813.  
  814.        '''
  815.         try:
  816.             child = pexpect.spawn('rpm', self.__argv + ['--addsign', rpm.path],
  817.                                   env=self.__env,
  818.                                   timeout=config.signing_timeout)
  819.             child.expect('Enter pass phrase: ')
  820.             child.sendline(self.__key_passphrase)
  821.             answer = child.expect(['Pass phrase is good\.',
  822.                                    'Pass phrase check failed'])
  823.             child.expect(pexpect.EOF)
  824.             child.close()
  825.         except pexpect.ExceptionPexpect, e:
  826.             msg = str(e).splitlines()[0] # We don't want all of the pexpect dump
  827.             rpm.status = errors.UNKNOWN_ERROR
  828.             raise RPMFileError('Error signing %s: %s' % (rpm.rpm_id, msg))
  829.         if (not os.WIFEXITED(child.status) or
  830.             os.WEXITSTATUS(child.status) != 0 or answer != 0):
  831.             rpm.status = errors.UNKNOWN_ERROR
  832.             raise RPMFileError('Error signing %s: status %d, output %s'
  833.                                % (rpm.rpm_id, child.status, child.before))
  834.         logging.info('Signed RPM %s with key %s', rpm.rpm_id, self.__key.name)
  835.  
  836. # Request handlers
  837.  
  838. @request_handler()
  839. def cmd_list_users(db, conn):
  840.     conn.authenticate_admin(db)
  841.     # Order by name to hide database structure
  842.     users = db.query(User).order_by(User.name).all()
  843.     conn.send_reply_header(errors.OK, {'num-users': len(users)})
  844.     payload = ''
  845.     for user in users:
  846.         payload += user.name + '\x00'
  847.     conn.send_reply_payload(payload)
  848.  
  849. @request_handler()
  850. def cmd_new_user(db, conn):
  851.     conn.authenticate_admin(db)
  852.     name = conn.safe_outer_field('name', required=True)
  853.     # FIXME: is this check atomic?
  854.     if db.query(User).filter_by(name=name).first() is not None:
  855.         conn.send_error(errors.ALREADY_EXISTS)
  856.     new_password = conn.inner_field('new-password')
  857.     admin = conn.outer_field_bool('admin')
  858.     if admin is None:
  859.         admin = False
  860.     user = User(name, clear_password=new_password, admin=admin)
  861.     db.add(user)
  862.     db.commit()
  863.     conn.send_reply_ok_only()
  864.  
  865. @request_handler()
  866. def cmd_delete_user(db, conn):
  867.     conn.authenticate_admin(db)
  868.     user = user_by_name(db, conn)
  869.     if len(user.key_accesses) > 0:
  870.         conn.send_error(errors.USER_HAS_KEY_ACCESSES)
  871.     db.delete(user)
  872.     db.commit()
  873.     conn.send_reply_ok_only()
  874.  
  875. @request_handler()
  876. def cmd_user_info(db, conn):
  877.     conn.authenticate_admin(db)
  878.     user = user_by_name(db, conn)
  879.     conn.send_reply_header(errors.OK, {'admin': user.admin})
  880.     conn.send_reply_payload('')
  881.  
  882. @request_handler()
  883. def cmd_modify_user(db, conn):
  884.     conn.authenticate_admin(db)
  885.     user = user_by_name(db, conn)
  886.     admin = conn.outer_field_bool('admin')
  887.     if admin is not None:
  888.         user.admin = admin
  889.     new_name = conn.safe_outer_field('new-name')
  890.     if new_name is not None:
  891.         # FIXME: is this check atomic?
  892.         if db.query(User).filter_by(name=new_name).first() is not None:
  893.             conn.send_error(errors.ALREADY_EXISTS)
  894.         user.name = new_name
  895.     new_password = conn.inner_field('new-password')
  896.     if new_password is not None:
  897.         user.clear_password = new_password
  898.     db.commit()
  899.     conn.send_reply_ok_only()
  900.  
  901. @request_handler()
  902. def cmd_key_user_info(db, conn):
  903.     conn.authenticate_admin(db)
  904.     access = key_access_by_names(db, conn)
  905.     conn.send_reply_header(errors.OK, {'key-admin': access.key_admin})
  906.     conn.send_reply_payload('')
  907.  
  908. @request_handler()
  909. def cmd_modify_key_user(db, conn):
  910.     conn.authenticate_admin(db)
  911.     access = key_access_by_names(db, conn)
  912.     key_admin = conn.outer_field_bool('key-admin')
  913.     if key_admin is not None:
  914.         access.key_admin = key_admin
  915.     db.commit()
  916.     conn.send_reply_ok_only()
  917.  
  918. @request_handler()
  919. def cmd_list_keys(db, conn):
  920.     conn.authenticate_admin(db)
  921.     # Order by name to hide database structure
  922.     keys = db.query(Key).order_by(Key.name).all()
  923.     conn.send_reply_header(errors.OK, {'num-keys': len(keys)})
  924.     payload = ''
  925.     for user in keys:
  926.         payload += user.name + '\x00'
  927.     conn.send_reply_payload(payload)
  928.  
  929. @request_handler()
  930. def cmd_new_key(db, conn):
  931.     conn.authenticate_admin(db)
  932.     key_name = conn.safe_outer_field('key', required=True)
  933.     # FIXME: is this check atomic?
  934.     if db.query(Key).filter_by(name=key_name).first() is not None:
  935.         conn.send_error(errors.ALREADY_EXISTS)
  936.     admin_name = conn.safe_outer_field('initial-key-admin')
  937.     if admin_name is None:
  938.         admin_name = conn.safe_outer_field('user', required=True)
  939.     admin = db.query(User).filter_by(name=admin_name).first()
  940.     if admin is None:
  941.         conn.send_error(errors.USER_NOT_FOUND)
  942.     key_attrs = ('Key-Type: %s\n' % conn.config.gnupg_key_type +
  943.                  'Key-Length: %d\n' % conn.config.gnupg_key_length +
  944.                  'Key-Usage: %s\n' % conn.config.gnupg_key_usage)
  945.     if conn.config.gnupg_subkey_type is not None:
  946.         key_attrs += ('Subkey-Type: %s\n' % conn.config.gnupg_subkey_type +
  947.                       'Subkey-Length: %d\n' % conn.config.gnupg_subkey_length)
  948.     key_passphrase = random_passphrase(conn)
  949.     key_attrs += 'Passphrase: %s\n' % key_passphrase
  950.     name = conn.safe_outer_field('name-real')
  951.     if name is None:
  952.         name = key_name
  953.     key_attrs += 'Name-Real: %s\n' % name
  954.     name = conn.safe_outer_field('name-comment')
  955.     if name is not None:
  956.         key_attrs += 'Name-Comment: %s\n' % name
  957.     name = conn.safe_outer_field('name-email')
  958.     if name is not None:
  959.         key_attrs += 'Name-Email: %s\n' % name
  960.     expire = conn.safe_outer_field('expire-date')
  961.     if expire is not None:
  962.         if not utils.yyyy_mm_dd_is_valid(expire):
  963.             raise InvalidRequestError('Invalid expiration date')
  964.         key_attrs += 'Expire-Date: %s\n' % expire
  965.     user_passphrase = conn.inner_field('passphrase', required=True)
  966.  
  967.     env = dict(os.environ) # Shallow copy, uses our $GNUPGHOME
  968.     env['LC_ALL'] = 'C'
  969.     sub = subprocess.Popen((settings.gnupg_bin, '--gen-key', '--batch',
  970.                             '--quiet', '--status-fd', '1'),
  971.                            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
  972.                            stderr=subprocess.PIPE, close_fds=True, env=env)
  973.     (out, err) = sub.communicate(key_attrs)
  974.     for line in err.split('\n'):
  975.         if (line != '' and
  976.             not line.startswith('gpg: WARNING: unsafe permissions on homedir')
  977.             and not line.startswith('Not enough random bytes available.')
  978.             and not line.startswith('the OS a chance to collect more entropy!')
  979.             and not (line.startswith('gpg: key ') and
  980.                      line.endswith('marked as ultimately trusted'))):
  981.                 logging.error('Unrecognized GPG stderr: %s', repr(line))
  982.                 conn.send_error(errors.UNKNOWN_ERROR)
  983.     fingerprint = None
  984.     for line in out.split('\n'):
  985.         if (line == '' or line == '[GNUPG:] GOOD_PASSPHRASE' or
  986.             line.startswith('[GNUPG:] PROGRESS')):
  987.             continue
  988.         if not line.startswith('[GNUPG:] KEY_CREATED'):
  989.             logging.error('Unrecognized GPG stdout: %s', repr(line))
  990.             conn.send_error(errors.UNKNOWN_ERROR)
  991.         fingerprint = line.split(' ')[-1]
  992.     if fingerprint is None:
  993.         logging.error('Can not find fingerprint of a new key in gpg output')
  994.         conn.send_error(errors.UNKNOWN_ERROR)
  995.  
  996.     try:
  997.         key = Key(key_name, fingerprint)
  998.         db.add(key)
  999.         access = KeyAccess(key, admin, key_admin=True)
  1000.         access.set_passphrase(conn.config, key_passphrase=key_passphrase,
  1001.                               user_passphrase=user_passphrase)
  1002.         db.add(access)
  1003.         db.commit()
  1004.     except:
  1005.         server_common.gpg_delete_key(conn.config, fingerprint)
  1006.         raise
  1007.     payload = server_common.gpg_public_key(conn.config, fingerprint)
  1008.     conn.send_reply_header(errors.OK, {})
  1009.     conn.send_reply_payload(payload)
  1010.  
  1011. @request_handler(payload_storage=RequestHandler.PAYLOAD_FILE)
  1012. def cmd_import_key(db, conn):
  1013.     conn.authenticate_admin(db)
  1014.     key_name = conn.safe_outer_field('key', required=True)
  1015.     # FIXME: is this check atomic?
  1016.     if db.query(Key).filter_by(name=key_name).first() is not None:
  1017.         conn.send_error(errors.ALREADY_EXISTS)
  1018.     admin_name = conn.safe_outer_field('initial-key-admin')
  1019.     if admin_name is None:
  1020.         admin_name = conn.safe_outer_field('user', required=True)
  1021.     admin = db.query(User).filter_by(name=admin_name).first()
  1022.     if admin is None:
  1023.         conn.send_error(errors.USER_NOT_FOUND)
  1024.     new_key_passphrase = random_passphrase(conn)
  1025.     import_key_passphrase = conn.inner_field('passphrase', required=True)
  1026.     user_passphrase = conn.inner_field('new-passphrase', required=True)
  1027.  
  1028.     try:
  1029.         fingerprint = server_common.gpg_import_key(conn.config, conn.payload_file)
  1030.     except server_common.GPGError, e:
  1031.         conn.send_error(errors.INVALID_IMPORT, message=str(e))
  1032.  
  1033.     try:
  1034.         try:
  1035.             server_common.gpg_change_password(conn.config, fingerprint,
  1036.                                               import_key_passphrase,
  1037.                                               new_key_passphrase)
  1038.         except server_common.GPGError, e:
  1039.             conn.send_error(errors.IMPORT_PASSPHRASE_ERROR)
  1040.  
  1041.         key = Key(key_name, fingerprint)
  1042.         db.add(key)
  1043.         access = KeyAccess(key, admin, key_admin=True)
  1044.         access.set_passphrase(conn.config, key_passphrase=new_key_passphrase,
  1045.                               user_passphrase=user_passphrase)
  1046.         db.add(access)
  1047.         db.commit()
  1048.     except:
  1049.         server_common.gpg_delete_key(conn.config, fingerprint)
  1050.         raise
  1051.     conn.send_reply_ok_only()
  1052.  
  1053. @request_handler()
  1054. def cmd_delete_key(db, conn):
  1055.     conn.authenticate_admin(db)
  1056.     key = key_by_name(db, conn)
  1057.     server_common.gpg_delete_key(conn.config, key.fingerprint)
  1058.     for a in key.key_accesses:
  1059.         db.delete(a)
  1060.     db.delete(key)
  1061.     db.commit()
  1062.     conn.send_reply_ok_only()
  1063.  
  1064. @request_handler()
  1065. def cmd_modify_key(db, conn):
  1066.     conn.authenticate_admin(db)
  1067.     key = key_by_name(db, conn)
  1068.     new_name = conn.safe_outer_field('new-name')
  1069.     if new_name is not None:
  1070.         # FIXME: is this check atomic?
  1071.         if db.query(Key).filter_by(name=new_name).first() is not None:
  1072.             conn.send_error(errors.ALREADY_EXISTS)
  1073.         key.name = new_name
  1074.     db.commit()
  1075.     conn.send_reply_ok_only()
  1076.  
  1077. @request_handler()
  1078. def cmd_list_key_users(db, conn):
  1079.     (user, key) = conn.authenticate_admin_or_key_admin(db)
  1080.     # Order by name to hide database structure
  1081.     names = sorted(access.user.name for access in key.key_accesses)
  1082.     conn.send_reply_header(errors.OK, {'num-users': len(names)})
  1083.     payload = ''
  1084.     for name in names:
  1085.         payload += name + '\x00'
  1086.     conn.send_reply_payload(payload)
  1087.  
  1088. @request_handler()
  1089. def cmd_grant_key_access(db, conn):
  1090.     (access, key_passphrase) = conn.authenticate_key_admin(db)
  1091.     user = user_by_name(db, conn)
  1092.     new_passphrase = conn.inner_field('new-passphrase', required=True)
  1093.     # FIXME: is this check atomic?
  1094.     if (db.query(KeyAccess).filter_by(user=user, key=access.key).first() is not
  1095.         None):
  1096.         conn.send_error(errors.ALREADY_EXISTS)
  1097.     a2 = KeyAccess(access.key, user, key_admin=False)
  1098.     a2.set_passphrase(conn.config, key_passphrase=key_passphrase,
  1099.                       user_passphrase=new_passphrase)
  1100.     db.add(a2)
  1101.     db.commit()
  1102.     conn.send_reply_ok_only()
  1103.  
  1104. @request_handler()
  1105. def cmd_revoke_key_access(db, conn):
  1106.     (_, key) = conn.authenticate_admin_or_key_admin(db)
  1107.     user = user_by_name(db, conn)
  1108.     access = db.query(KeyAccess).filter_by(user=user, key=key).first()
  1109.     if access is None:
  1110.         conn.send_error(errors.KEY_USER_NOT_FOUND)
  1111.     if len(key.key_accesses) == 1:
  1112.         conn.send_error(errors.ONLY_ONE_KEY_USER)
  1113.     db.delete(access)
  1114.     db.commit()
  1115.     conn.send_reply_ok_only()
  1116.  
  1117. @request_handler()
  1118. def cmd_get_public_key(db, conn):
  1119.     (_, key) = conn.authenticate_admin_or_user(db)
  1120.     payload = server_common.gpg_public_key(conn.config, str(key.fingerprint))
  1121.     conn.send_reply_header(errors.OK, {})
  1122.     conn.send_reply_payload(payload)
  1123.  
  1124. @request_handler()
  1125. def cmd_change_passphrase(db, conn):
  1126.     (access, key_passphrase) = conn.authenticate_user(db)
  1127.     new_passphrase = conn.inner_field('new-passphrase', required=True)
  1128.     access.set_passphrase(conn.config, key_passphrase=key_passphrase,
  1129.                           user_passphrase=new_passphrase)
  1130.     db.commit()
  1131.     conn.send_reply_ok_only()
  1132.  
  1133. @request_handler(payload_storage=RequestHandler.PAYLOAD_FILE)
  1134. def cmd_sign_text(db, conn):
  1135.     (access, key_passphrase) = conn.authenticate_user(db)
  1136.     signed_file = tempfile.TemporaryFile()
  1137.     try:
  1138.         server_common.gpg_clearsign(conn.config, signed_file, conn.payload_file,
  1139.                                     access.key.fingerprint, key_passphrase)
  1140.         logging.info('Signed text %s with key %s',
  1141.                      binascii.b2a_hex(conn.payload_sha512_digest),
  1142.                      access.key.name)
  1143.         conn.send_reply_header(errors.OK, {})
  1144.         conn.send_reply_payload_from_file(signed_file)
  1145.     finally:
  1146.         signed_file.close()
  1147.  
  1148. @request_handler(payload_storage=RequestHandler.PAYLOAD_FILE)
  1149. def cmd_sign_data(db, conn):
  1150.     (access, key_passphrase) = conn.authenticate_user(db)
  1151.     signature_file = tempfile.TemporaryFile()
  1152.     try:
  1153.         server_common.gpg_detached_signature(conn.config, signature_file,
  1154.                                              conn.payload_file,
  1155.                                              access.key.fingerprint,
  1156.                                              key_passphrase)
  1157.         logging.info('Signed data %s with key %s',
  1158.                      binascii.b2a_hex(conn.payload_sha512_digest),
  1159.                      access.key.name)
  1160.         conn.send_reply_header(errors.OK, {})
  1161.         conn.send_reply_payload_from_file(signature_file)
  1162.     finally:
  1163.         signature_file.close()
  1164.  
  1165. @request_handler(payload_storage=RequestHandler.PAYLOAD_FILE,
  1166.                  payload_auth_optional=True)
  1167. def cmd_sign_rpm(db, conn):
  1168.     (access, key_passphrase) = conn.authenticate_user(db)
  1169.  
  1170.     rpm = RPMFile(conn.payload_path, conn.payload_sha512_digest)
  1171.     try:
  1172.         rpm.verify()
  1173.         rpm.read_header(conn.payload_file)
  1174.         rpm.authenticate(conn.outer_field, conn.payload_authenticated)
  1175.     except RPMFileError:
  1176.         conn.send_error(rpm.status)
  1177.  
  1178.     ctx = SigningContext(conn, access.key, key_passphrase)
  1179.     try:
  1180.         ctx.sign_rpm(conn.config, rpm)
  1181.     except RPMFileError, e:
  1182.         logging.error(str(e))
  1183.         conn.send_error(rpm.status)
  1184.  
  1185.     # Reopen to get the new file even if rpm doesn't overwrite the file in place
  1186.     f = open(rpm.path, 'rb')
  1187.     try:
  1188.         conn.send_reply_header(errors.OK, {})
  1189.         conn.send_reply_payload_from_file(f)
  1190.     finally:
  1191.         f.close()
  1192.  
  1193. class SignRPMsRequestThread(utils.WorkerThread):
  1194.     '''A thread that handles sign-rpm requests.
  1195.  
  1196.    The requests are put into dest_queue as RPMFile objects, with None marking
  1197.    end of the requests.
  1198.  
  1199.    '''
  1200.  
  1201.     def __init__(self, conn, dest_queue, header_nss_key, payload_nss_key,
  1202.                  tmp_dir):
  1203.         super(SignRPMsRequestThread, self). \
  1204.             __init__('sign-rpms:requests', 'request thread',
  1205.                      output_queues=((dest_queue, None),))
  1206.         self.__conn = conn
  1207.         self.__dest = dest_queue
  1208.         self.__header_nss_key = header_nss_key
  1209.         self.__payload_nss_key = payload_nss_key
  1210.         self.__tmp_dir = tmp_dir
  1211.  
  1212.     def _real_run(self):
  1213.         total_size = 0
  1214.         server_idx = 0
  1215.         while True:
  1216.             (rpm, size) = self.__read_one_request \
  1217.                 (server_idx,
  1218.                  self.__conn.config.max_rpms_payloads_size - total_size)
  1219.             if rpm is None:
  1220.                 break
  1221.             server_idx += 1
  1222.             total_size += size
  1223.             self.__dest.put(rpm)
  1224.  
  1225.     def __read_one_request(self, server_idx, remaining_size):
  1226.         '''Read one request from self.__conn.
  1227.  
  1228.        Return (RPMFile, file size), (None, None) on EOF.  Raise
  1229.        InvalidRequestError, others.  Only allow remaining_size bytes for the
  1230.        payload.
  1231.  
  1232.        '''
  1233.         try:
  1234.             nss_key = utils.derived_key(self.__header_nss_key, server_idx)
  1235.             fields = self.__conn.read_subheader(nss_key)
  1236.         except EOFError:
  1237.             return (None, None)
  1238.         s = utils.readable_fields(fields)
  1239.         logging.debug('%s: Started handling %s', self.name, s)
  1240.         logging.info('Subrequest: %s', s)
  1241.         if 'id' not in fields:
  1242.             raise InvalidRequestError('Required subheader field id missing.')
  1243.  
  1244.         nss_key = utils.derived_key(self.__payload_nss_key, server_idx)
  1245.         (path, payload_file, sha512_digest, authenticated) \
  1246.             = self.__conn.read_subpayload_to_file(nss_key, remaining_size,
  1247.                                                   self.__tmp_dir)
  1248.         try:
  1249.             # Count whole blocks to avoid millions of 1-byte files filling the
  1250.             # hard drive due to internal fragmentation.
  1251.             size = utils.file_size_in_blocks(payload_file)
  1252.  
  1253.             rpm = RPMFile(path, sha512_digest, request_id=fields['id'])
  1254.             try:
  1255.                 rpm.verify()
  1256.                 rpm.read_header(payload_file)
  1257.             except RPMFileError:
  1258.                 return (rpm, size)
  1259.         finally:
  1260.             payload_file.close()
  1261.  
  1262.         try:
  1263.             rpm.authenticate(fields.get, authenticated)
  1264.         except RPMFileError:
  1265.             return (rpm, size)
  1266.  
  1267.         return (rpm, size)
  1268.  
  1269. class SignRPMsSignerThread(utils.WorkerThread):
  1270.     '''A thread that actually performs the signing.
  1271.  
  1272.    The requests in dst_queue and src_queue are RPMFile objects, with None
  1273.    marking end of the requests.
  1274.  
  1275.    '''
  1276.  
  1277.     def __init__(self, config, dst_queue, src_queue, ctx):
  1278.         super(SignRPMsSignerThread, self).__init__ \
  1279.             ('sign-rpms:signing', 'signer thread',
  1280.              input_queues=((src_queue, None),),
  1281.              output_queues=((dst_queue, None),))
  1282.         self.__config = config
  1283.         self.__dst = dst_queue
  1284.         self.__src = src_queue
  1285.         self.__ctx = ctx
  1286.  
  1287.     def _real_run(self):
  1288.         while True:
  1289.             rpm = self.__src.get()
  1290.             if rpm is None:
  1291.                 break
  1292.             try:
  1293.                 try:
  1294.                     # FIXME: sign more at a time
  1295.                     self.__handle_one_rpm(rpm)
  1296.                 except:
  1297.                     if rpm.status is None:
  1298.                         rpm.status = errors.UNKNOWN_ERROR
  1299.                     raise
  1300.             finally:
  1301.                 self.__dst.put(rpm)
  1302.  
  1303.     def __handle_one_rpm(self, rpm):
  1304.         '''Handle an incoming request.'''
  1305.         logging.debug('%s: Started handling %s', self.name, rpm.rpm_id)
  1306.         if rpm.status is not None:
  1307.             return
  1308.  
  1309.         try:
  1310.             self.__ctx.sign_rpm(self.__config, rpm)
  1311.         except RPMFileError, e:
  1312.             logging.error(str(e))
  1313.  
  1314. class SignRPMsReplyThread(utils.WorkerThread):
  1315.     '''A thread that sends subrequest replies.
  1316.  
  1317.    The requests in src_queue are RPMFile objects, with None marking end of the
  1318.    requests.
  1319.  
  1320.    '''
  1321.  
  1322.     def __init__(self, conn, src_queue, header_nss_key, payload_nss_key):
  1323.         super(SignRPMsReplyThread, self). \
  1324.             __init__('sign-rpms:replies', 'reply thread',
  1325.                      input_queues=((src_queue, None),))
  1326.         self.__conn = conn
  1327.         self.__src = src_queue
  1328.         self.__header_nss_key = header_nss_key
  1329.         self.__payload_nss_key = payload_nss_key
  1330.  
  1331.     def _real_run(self):
  1332.         '''Read all results and send subreplies.'''
  1333.         server_idx = 0
  1334.         while True:
  1335.             rpm = self.__src.get()
  1336.             if rpm is None:
  1337.                 break
  1338.             self.__handle_one_rpm(rpm, server_idx)
  1339.             server_idx += 1
  1340.  
  1341.     def __handle_one_rpm(self, rpm, server_idx):
  1342.         '''Send information based on rpm.'''
  1343.         logging.debug('%s: Started handling %s', self.name, rpm.rpm_id)
  1344.         f = {'id': rpm.request_id}
  1345.         if rpm.status is not None:
  1346.             f['status'] = rpm.status
  1347.             logging.info('Subrequest %d error: %s', server_idx,
  1348.                          errors.message(rpm.status))
  1349.         else:
  1350.             f['status'] = errors.OK
  1351.         nss_key = utils.derived_key(self.__header_nss_key, server_idx)
  1352.         self.__conn.send_subheader(f, nss_key)
  1353.  
  1354.         nss_key = utils.derived_key(self.__payload_nss_key, server_idx)
  1355.         if rpm.status is None:
  1356.             f = open(rpm.path, 'rb')
  1357.             try:
  1358.                 self.__conn.send_subpayload_from_file(f, nss_key)
  1359.             finally:
  1360.                 f.close()
  1361.         else:
  1362.             self.__conn.send_empty_subpayload(nss_key)
  1363.  
  1364.  
  1365. @request_handler()
  1366. def cmd_sign_rpms(db, conn):
  1367.     (access, key_passphrase) = conn.authenticate_user(db)
  1368.     mech = nss.nss.CKM_GENERIC_SECRET_KEY_GEN
  1369.     slot = nss.nss.get_best_slot(mech)
  1370.     buf = conn.inner_field('subrequest-header-auth-key', required=True)
  1371.     if len(buf) < 64:
  1372.         raise InvalidRequestError('Subrequest header authentication key too '
  1373.                                   'small')
  1374.     # "Unwrap" because the key was encrypted for transmission using TLS
  1375.     subrequest_header_nss_key = nss.nss.import_sym_key \
  1376.         (slot, mech, nss.nss.PK11_OriginUnwrap, nss.nss.CKA_DERIVE,
  1377.          nss.nss.SecItem(buf))
  1378.     buf = conn.inner_field('subrequest-payload-auth-key', required=True)
  1379.     if len(buf) < 64:
  1380.         raise InvalidRequestError('Subrequest payload authentication key too '
  1381.                                   'small')
  1382.     subrequest_payload_nss_key = nss.nss.import_sym_key \
  1383.         (slot, mech, nss.nss.PK11_OriginUnwrap, nss.nss.CKA_DERIVE,
  1384.          nss.nss.SecItem(buf))
  1385.     buf = conn.inner_field('subreply-header-auth-key', required=True)
  1386.     if len(buf) < 64:
  1387.         raise InvalidRequestError('Subreply header authentication key too '
  1388.                                   'small')
  1389.     subreply_header_nss_key = nss.nss.import_sym_key \
  1390.         (slot, mech, nss.nss.PK11_OriginUnwrap, nss.nss.CKA_DERIVE,
  1391.          nss.nss.SecItem(buf))
  1392.     buf = conn.inner_field('subreply-payload-auth-key', required=True)
  1393.     if len(buf) < 64:
  1394.         raise InvalidRequestError('Subreply payload authentication key too '
  1395.                                   'small')
  1396.     subreply_payload_nss_key = nss.nss.import_sym_key \
  1397.         (slot, mech, nss.nss.PK11_OriginUnwrap, nss.nss.CKA_DERIVE,
  1398.          nss.nss.SecItem(buf))
  1399.     signing_ctx = SigningContext(conn, access.key, key_passphrase)
  1400.     conn.send_reply_ok_only()
  1401.  
  1402.     tmp_dir = tempfile.mkdtemp()
  1403.     exception = None
  1404.     try:
  1405.         q1 = Queue.Queue(100)
  1406.         q2 = Queue.Queue(100)
  1407.         threads = []
  1408.         threads.append(SignRPMsRequestThread(conn, q1,
  1409.                                              subrequest_header_nss_key,
  1410.                                              subrequest_payload_nss_key,
  1411.                                              tmp_dir))
  1412.         threads.append(SignRPMsSignerThread(conn.config, q2, q1, signing_ctx))
  1413.         threads.append(SignRPMsReplyThread(conn, q2, subreply_header_nss_key,
  1414.                                            subreply_payload_nss_key))
  1415.  
  1416.         (_, exception) = utils.run_worker_threads(threads,
  1417.                                                   (InvalidRequestError,))
  1418.     finally:
  1419.         shutil.rmtree(tmp_dir)
  1420.     if exception is not None:
  1421.         raise exception[0], exception[1], exception[2]
  1422.  
  1423. def unknown_request_handler(unused_db, conn):
  1424.     conn.send_reply_header(errors.UNKNOWN_OP, {})
  1425.     conn.send_reply_payload('')
  1426. # Allow some payload in order to return errors.UNKNOWN_OP rather than fail with
  1427. # "payload too large"
  1428. request_handlers[None] = RequestHandler(unknown_request_handler,
  1429.                                         RequestHandler.PAYLOAD_MEMORY)
  1430.  
  1431.  
  1432. _CHILD_OK = 0                   # Handled a request
  1433. _CHILD_CONNECTION_REFUSED = 1   # Connection to the bridge was refused
  1434. _CHILD_BUG = 2                  # A bug in the child
  1435. # Undefined values are treated as _CHILD_BUG:
  1436.  
  1437. def request_handling_child(config):
  1438.     '''Handle a single request, runinng in a child process.
  1439.  
  1440.    Return one of the _CHILD_* exit codes.
  1441.  
  1442.    '''
  1443.     try:
  1444.         utils.set_regid(config)
  1445.         utils.set_reuid(config)
  1446.         utils.update_HOME_for_uid(config)
  1447.     except:
  1448.         # The failing function has already logged the exception
  1449.         return _CHILD_BUG
  1450.  
  1451.     child_exception = None
  1452.     try:
  1453.         db = server_common.db_open(config)
  1454.         conn = ServersConnection(config)
  1455.         try:
  1456.             logging.debug('Waiting for a request')
  1457.             handler = conn.read_request()
  1458.             handler.handler(db, conn)
  1459.         finally:
  1460.             try:
  1461.                 conn.close()
  1462.             except (double_tls.ChildConnectionRefusedError,
  1463.                     double_tls.ChildUnrecoverableError), e:
  1464.                 child_exception = e
  1465.     except RequestHandled:
  1466.         pass
  1467.     except InvalidRequestError, e:
  1468.         logging.warning('Invalid request: %s', str(e))
  1469.     except (IOError, socket.error), e:
  1470.         logging.info('I/O error: %s', repr(e))
  1471.     except nss.error.NSPRError, e:
  1472.         if e.errno == nss.error.PR_CONNECT_RESET_ERROR:
  1473.             logging.debug('NSPR error: Connection reset')
  1474.         else:
  1475.             logging.warning('NSPR error: %s', str(e))
  1476.     except EOFError, e:
  1477.         if isinstance(child_exception, double_tls.ChildConnectionRefusedError):
  1478.             logging.info('Connection to the bridge refused')
  1479.             return _CHILD_CONNECTION_REFUSED
  1480.         elif isinstance(child_exception, double_tls.ChildUnrecoverableError):
  1481.             logging.debug('Unrecoverable error in child')
  1482.             return _CHILD_BUG
  1483.         else:
  1484.             logging.info('Unexpected EOF')
  1485.     except (KeyboardInterrupt, SystemExit):
  1486.         pass # Don't consider this an unexpected exception
  1487.     except (utils.NSSInitError, double_tls.InnerCertificateNotFound), e:
  1488.         logging.error(str(e))
  1489.         return _CHILD_BUG
  1490.     except:
  1491.         logging.error('Unexpected exception', exc_info=True)
  1492.         return _CHILD_BUG
  1493.     logging.debug('Request handling finished')
  1494.     return _CHILD_OK
  1495.  
  1496. def main():
  1497.     options = utils.get_daemon_options('A signing server',
  1498.                                        '~/.sigul/server.conf')
  1499.     logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
  1500.                         level=utils.logging_level_from_options(options),
  1501.                         filename=os.path.join(options.log_dir,
  1502.                                               'sigul_server.log'))
  1503.     try:
  1504.         config = ServerConfiguration(options.config_file)
  1505.     except utils.ConfigurationError, e:
  1506.         sys.exit(str(e))
  1507.  
  1508.     server_common.gpg_modify_environ(config)
  1509.  
  1510.     if options.daemonize:
  1511.         utils.daemonize()
  1512.  
  1513.     signal.signal(signal.SIGTERM, utils.sigterm_handler)
  1514.     utils.create_pid_file(options, 'sigul_server')
  1515.     try:
  1516.         try:
  1517.             fast_reconnections_done = 0
  1518.             while True:
  1519.                 child_pid = os.fork()
  1520.                 if child_pid == 0:
  1521.                     try:
  1522.                         status = request_handling_child(config)
  1523.                         logging.shutdown()
  1524.                         os._exit(status)
  1525.                     finally:
  1526.                         try:
  1527.                             logging.shutdown()
  1528.                         finally:
  1529.                             os._exit(_CHILD_BUG)
  1530.                 (_, status) = os.waitpid(child_pid, 0)
  1531.                 if os.WIFEXITED(status) and os.WEXITSTATUS(status) == _CHILD_OK:
  1532.                     fast_reconnections_done = 0
  1533.                 elif (os.WIFEXITED(status) and
  1534.                       os.WEXITSTATUS(status) == _CHILD_CONNECTION_REFUSED):
  1535.                     if fast_reconnections_done < MAX_FAST_RECONNECTIONS:
  1536.                         time.sleep(FAST_RECONNECTION_SECONDS)
  1537.                         fast_reconnections_done += 1
  1538.                     else:
  1539.                         time.sleep(SLOW_RECONNECTION_SECONDS)
  1540.                         fast_reconnections_done = 0
  1541.                 else: # _CHILD_BUG, unknown status code or WIFSIGNALED
  1542.                     logging.error('Child died with status %d', status)
  1543.                     break
  1544.         except (KeyboardInterrupt, SystemExit):
  1545.             pass # Silence is golden
  1546.     finally:
  1547.         utils.delete_pid_file(options, 'sigul_server')
  1548.  
  1549. if __name__ == '__main__':
  1550.     main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement