mirror of
https://github.com/chocolate-doom/master-server.git
synced 2024-11-24 13:21:47 +00:00
Add GPG signature generation, as the first step towards secure demo
support. Subversion-branch: /master Subversion-revision: 2516
This commit is contained in:
parent
da23130736
commit
6d2987c286
4 changed files with 258 additions and 8 deletions
|
@ -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. """
|
||||
|
|
|
@ -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>"
|
||||
|
||||
|
|
|
@ -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
141
secure_demo.py
Executable 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)
|
||||
|
Loading…
Reference in a new issue