Add GPG signature generation, as the first step towards secure demo

support.

Subversion-branch: /master
Subversion-revision: 2516
This commit is contained in:
Simon Howard 2012-08-04 01:25:20 +00:00
parent da23130736
commit 6d2987c286
4 changed files with 258 additions and 8 deletions

View file

@ -27,10 +27,11 @@ import simplejson
from select import select
from time import time, strftime
from master_config import *
import secure_demo
# Maximum length of a query response.
MAX_RESPONSE_LEN = 1000
MAX_RESPONSE_LEN = 1500
# Normal packet types.
@ -45,6 +46,10 @@ NET_MASTER_PACKET_TYPE_QUERY = 2
NET_MASTER_PACKET_TYPE_QUERY_RESPONSE = 3
NET_MASTER_PACKET_TYPE_GET_METADATA = 4
NET_MASTER_PACKET_TYPE_GET_METADATA_RESPONSE = 5
NET_MASTER_PACKET_TYPE_SIGN_START = 6
NET_MASTER_PACKET_TYPE_SIGN_START_RESPONSE = 7
NET_MASTER_PACKET_TYPE_SIGN_END = 8
NET_MASTER_PACKET_TYPE_SIGN_END_RESPONSE = 9
def bind_socket_to(sock, config):
""" Bind the specified socket to the address/port configuration from
@ -129,6 +134,11 @@ class MasterServer:
self.sock = self.open_socket(server_address)
self.query_sock = self.open_socket(query_address)
if secure_demo.available and SIGNING_KEY:
self.signer = secure_demo.SecureSigner(SIGNING_KEY)
else:
self.signer = None
def send_query(self, server):
""" Send a query to the specified server. """
@ -297,6 +307,41 @@ class MasterServer:
NET_MASTER_PACKET_TYPE_GET_METADATA_RESPONSE,
packet)
def sign_start_message(self, addr):
""" Generate a signed start message and return to the client. """
self.log_output(addr, "Start demo")
if self.signer is None:
return
# Generate start message and send it back.
signature = self.signer.sign_start_message()
self.send_message(addr, NET_MASTER_PACKET_TYPE_SIGN_START_RESPONSE,
signature)
def sign_end_message(self, data, addr):
""" Generate a signed end message and return to the client. """
self.log_output(addr, "End demo")
if self.signer is None:
return
# Parse the data. The first part is a 160-bit SHA1 hash, and the
# rest of the data is the start message.
demo_hash = data[0:20]
start_message = data[20:]
# Parse the start message and verify the signature, then use it
# to generate an end message along with the hash of the demo.
signature = self.signer.sign_end_message(start_message, demo_hash)
if signature is None:
self.log_output(addr, "Failed to verify start message!")
else:
self.send_message(addr, NET_MASTER_PACKET_TYPE_SIGN_END_RESPONSE,
signature)
def process_packet(self, data, addr):
""" Process a packet received from a server. """
@ -308,6 +353,10 @@ class MasterServer:
self.process_query(addr)
elif packet_type == NET_MASTER_PACKET_TYPE_GET_METADATA:
self.process_metadata_request(addr)
elif packet_type == NET_MASTER_PACKET_TYPE_SIGN_START:
self.sign_start_message(addr)
elif packet_type == NET_MASTER_PACKET_TYPE_SIGN_END:
self.sign_end_message(data[2:], addr)
def rx_packet(self):
""" Invoked when a packet is received. """

View file

@ -32,6 +32,10 @@ NET_MASTER_PACKET_TYPE_QUERY = 2
NET_MASTER_PACKET_TYPE_QUERY_RESPONSE = 3
NET_MASTER_PACKET_TYPE_GET_METADATA = 4
NET_MASTER_PACKET_TYPE_GET_METADATA_RESPONSE = 5
NET_MASTER_PACKET_TYPE_SIGN_START = 6
NET_MASTER_PACKET_TYPE_SIGN_START_RESPONSE = 7
NET_MASTER_PACKET_TYPE_SIGN_END = 8
NET_MASTER_PACKET_TYPE_SIGN_END_RESPONSE = 9
UDP_PORT = 2342
@ -147,7 +151,8 @@ def get_metadata(addr_str):
print "Waiting for response..."
response = get_response(sock, addr, NET_MASTER_PACKET_TYPE_GET_METADATA_RESPONSE)
response = get_response(sock, addr,
NET_MASTER_PACKET_TYPE_GET_METADATA_RESPONSE)
servers = decode_string_list(response)
@ -161,15 +166,65 @@ def get_metadata(addr_str):
print "\t\tVersion: %s" % metadata["version"]
print "\t\tMax. players: %i" % metadata["max_players"]
if len(sys.argv) > 2 and sys.argv[1] == "query":
query_master(sys.argv[2])
elif len(sys.argv) > 2 and sys.argv[1] == "add":
add_to_master(sys.argv[2])
elif len(sys.argv) > 2 and sys.argv[1] == "get-metadata":
get_metadata(sys.argv[2])
def sign_start(addr_str):
""" Request a signed start message from the master. """
addr = (socket.gethostbyname(addr_str), UDP_PORT)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Send request
print "Sending signed start request to master at %s" % str(addr_str)
send_message(sock, addr, NET_MASTER_PACKET_TYPE_SIGN_START)
# Receive response
print "Waiting for response..."
response = get_response(sock, addr,
NET_MASTER_PACKET_TYPE_SIGN_START_RESPONSE)
print response
def sign_end(addr_str):
""" Request a signed end message from the server. """
addr = (socket.gethostbyname(addr_str), UDP_PORT)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
print "Paste the start message, then type ^D"
start_message = sys.stdin.read()
fake_sha1 = "3ijAI83u8A(*j98jf3jf"
print "Sending signed end request to master at %s" % str(addr_str)
send_message(sock, addr, NET_MASTER_PACKET_TYPE_SIGN_END,
payload=(fake_sha1 + start_message))
print "Waiting for response..."
response = get_response(sock, addr,
NET_MASTER_PACKET_TYPE_SIGN_END_RESPONSE)
print response
commands = [
("query", query_master),
("add", add_to_master),
("get-metadata", get_metadata),
("sign-start", sign_start),
("sign-end", sign_end),
]
for name, callback in commands:
if len(sys.argv) > 2 and name == sys.argv[1]:
callback(sys.argv[2])
break
else:
print "Usage:"
print "chocolate-master-test.py query <address>"
print "chocolate-master-test.py add <address>"
print "chocolate-master-test.py get-metadata <address>"
print "chocolate-master-test.py sign-start <address>"
print "chocolate-master-test.py sign-end <address>"

View file

@ -41,3 +41,8 @@ SERVER_ADDRESS = (None, 2342)
QUERY_ADDRESS = None
# ID of the GPG key to use to sign secure demo messages.
# Use the email address of the key or the hex key ID.
SIGNING_KEY = None

141
secure_demo.py Executable file
View file

@ -0,0 +1,141 @@
#!/usr/bin/env python
#
# Copyright(C) 2012 Simon Howard
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
# 02111-1307, USA.
#
#
# This is a library used by the master for the signed demos system. It
# uses GPG to create signed messages that are returned by the master
# back to the clients.
#
from io import BytesIO
import os
import sys
import time
NONCE_SIZE = 16 # bytes
try:
import gpgme
available = True
except ImportError:
available = False
def now_string():
"""Generate an ISO8601 string for the current time."""
now = time.time()
return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(now))
def bin_to_hex(data):
"""Convert a string of binary data into a hex representation."""
return "".join(map(lambda x: "%02x" % ord(x), data))
class SecureSigner(object):
def __init__(self, key):
"""Initialize a new SecureSigner. Must be passed a key identifier
string specifying the GPG key to use. """
self.context = gpgme.Context()
self.key = self.context.get_key(key)
self.context.signers = [ self.key ]
def _generate_start_message(self):
"""Generate the plaintext used for a start message."""
nonce = os.urandom(NONCE_SIZE)
return "\n".join([
"Start-Time: %s" % now_string(),
"Nonce: %s" % bin_to_hex(nonce),
])
def _sign_plaintext_message(self, message):
"""Sign a plaintext message."""
signature = BytesIO()
self.context.sign(BytesIO(message), signature, gpgme.SIG_MODE_CLEAR)
return signature.getvalue()
def sign_start_message(self):
"""Generate a new signed start message with a random nonce value."""
message = self._generate_start_message()
return self._sign_plaintext_message(message)
def _verify_signature(self, result):
"""Check the results of a verify operation."""
if len(result) != 1:
return False
# Check the signature is valid:
signature = result[0]
if (signature.summary & gpgme.SIGSUM_VALID) == 0:
return False
# Check the signature matches the right key:
for subkey in self.key.subkeys:
if subkey.fpr == signature.fpr:
break
else:
return False
return True
def _verify_start_message(self, signed_message):
"""Check that a signed message is correctly signed, returning
the plaintext if it is valid, or None if it is invalid."""
# Parse the plain text signed message:
try:
plaintext = BytesIO()
result = self.context.verify(BytesIO(signed_message),
None, plaintext)
if self._verify_signature(result):
return plaintext.getvalue()
except gpgme.GpgmeError:
pass
# Failure of some kind occurred: message failed to parse, or
# did not pass verification, etc.
return None
def sign_end_message(self, start_message, demo_hash):
"""Verify a start message and sign an end message that verifies
a complete demo."""
plaintext = self._verify_start_message(start_message)
if plaintext is None:
return None
# We assume the plaintext message ends with a newline.
if plaintext[-1] != "\n":
plaintext = plaintext + "\n"
# Add extra fields to the plaintext, to create the end message.
message = plaintext + "\n".join([
"End-Time: %s" % now_string(),
"Demo-Checksum: %s" % bin_to_hex(demo_hash),
])
return self._sign_plaintext_message(message)
if __name__ == "__main__":
if len(sys.argv) < 3:
print "Usage: %s <start|end> <key>" % sys.argv[0]
sys.exit(1)
signer = SecureSigner(sys.argv[2])
if sys.argv[1] == "start":
print signer.sign_start_message()
elif sys.argv[1] == "end":
start_message = sys.stdin.read()
fake_checksum = "3vism1idm4ibmaJ3nF1f"
print signer.sign_end_message(start_message, fake_checksum)