mirror of
https://github.com/chocolate-doom/master-server.git
synced 2024-11-22 04:21:45 +00:00
2cc103e783
from start messages. Include millisecond precision in the time fields of the messages for extra accuracy. Log nonce values and demo hashes. Subversion-branch: /master Subversion-revision: 2519
157 lines
5.2 KiB
Python
Executable file
157 lines
5.2 KiB
Python
Executable file
#!/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 a string representing the current time.
|
|
|
|
The time is roughly ISO8601 UTC format, but also includes
|
|
milliseconds for additional accuracy.
|
|
"""
|
|
now = time.time()
|
|
ms = int(now * 1000) % 1000
|
|
datetime_base = time.strftime('%Y-%m-%dT%H:%M:%S', time.gmtime(now))
|
|
return "%s.%03iZ" % (datetime_base, ms)
|
|
|
|
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, nonce):
|
|
"""Generate the plaintext used for a start message."""
|
|
return "\n".join([
|
|
"Message-Type: Start",
|
|
"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."""
|
|
nonce = os.urandom(NONCE_SIZE)
|
|
message = self._generate_start_message(nonce)
|
|
return (nonce, 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
|
|
|
|
# Split plain-text of start message into lines, and verify
|
|
# message type:
|
|
plaintext = plaintext.rstrip("\n")
|
|
plaintext_lines = plaintext.split("\n")
|
|
|
|
if plaintext_lines[0] != "Message-Type: Start":
|
|
return None
|
|
|
|
# Construct the end message:
|
|
message_lines = [ "Message-Type: Signature" ]
|
|
message_lines += plaintext_lines[1:]
|
|
message_lines += [
|
|
"End-Time: %s" % now_string(),
|
|
"Demo-Checksum: %s" % bin_to_hex(demo_hash),
|
|
]
|
|
message = "\n".join(message_lines)
|
|
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":
|
|
nonce, start_message = signer.sign_start_message()
|
|
print "Nonce: %s" % bin_to_hex(nonce)
|
|
print start_message
|
|
elif sys.argv[1] == "end":
|
|
start_message = sys.stdin.read()
|
|
fake_checksum = "3vism1idm4ibmaJ3nF1f"
|
|
print signer.sign_end_message(start_message, fake_checksum)
|
|
|