|
| 1 | +r"""Reference implementation of signing-spec. |
| 2 | +
|
| 3 | +Copyright 2021 Google LLC. |
| 4 | +SPDX-License-Identifier: Apache-2.0 |
| 5 | +
|
| 6 | +The following example requires `pip3 install pycryptodome` and uses ecdsa.py in |
| 7 | +the same directory as this file. |
| 8 | +
|
| 9 | +>>> import binascii, os, sys, textwrap |
| 10 | +>>> from pprint import pprint |
| 11 | +>>> sys.path.insert(0, os.path.dirname(__file__)) |
| 12 | +>>> import ecdsa |
| 13 | +
|
| 14 | +>>> signer = ecdsa.Signer.construct( |
| 15 | +... curve='P-256', |
| 16 | +... d=97358161215184420915383655311931858321456579547487070936769975997791359926199, |
| 17 | +... point_x=46950820868899156662930047687818585632848591499744589407958293238635476079160, |
| 18 | +... point_y=5640078356564379163099075877009565129882514886557779369047442380624545832820) |
| 19 | +>>> verifier = ecdsa.Verifier(signer.public_key) |
| 20 | +>>> payloadType = 'http://example.com/HelloWorld' |
| 21 | +>>> payload = b'hello world' |
| 22 | +
|
| 23 | +Signing example: |
| 24 | +
|
| 25 | +>>> signature_json = Sign(payloadType, payload, signer) |
| 26 | +>>> pprint(json.loads(signature_json)) |
| 27 | +{'payload': 'aGVsbG8gd29ybGQ=', |
| 28 | + 'payloadType': 'http://example.com/HelloWorld', |
| 29 | + 'signatures': [{'sig': 'Cc3RkvYsLhlaFVd+d6FPx4ZClhqW4ZT0rnCYAfv6/ckoGdwT7g/blWNpOBuL/tZhRiVFaglOGTU8GEjm4aEaNA=='}]} |
| 30 | +
|
| 31 | +Verification example: |
| 32 | +
|
| 33 | +>>> result = Verify(signature_json, [('mykey', verifier)]) |
| 34 | +>>> pprint(result) |
| 35 | +VerifiedPayload(payloadType='http://example.com/HelloWorld', payload=b'hello world', recognizedSigners=['mykey']) |
| 36 | +
|
| 37 | +PAE: |
| 38 | +
|
| 39 | +>>> def print_hex(b: bytes): |
| 40 | +... octets = ' '.join(textwrap.wrap(binascii.hexlify(b).decode('utf-8'), 2)) |
| 41 | +... print(*textwrap.wrap(octets, 48), sep='\n') |
| 42 | +>>> print_hex(PAE(payloadType, payload)) |
| 43 | +02 00 00 00 00 00 00 00 1d 00 00 00 00 00 00 00 |
| 44 | +68 74 74 70 3a 2f 2f 65 78 61 6d 70 6c 65 2e 63 |
| 45 | +6f 6d 2f 48 65 6c 6c 6f 57 6f 72 6c 64 0b 00 00 |
| 46 | +00 00 00 00 00 68 65 6c 6c 6f 20 77 6f 72 6c 64 |
| 47 | +""" |
| 48 | + |
| 49 | +import base64, binascii, dataclasses, json, struct |
| 50 | + |
| 51 | +# Protocol requires Python 3.8+. |
| 52 | +from typing import Iterable, List, Protocol, Tuple |
| 53 | + |
| 54 | + |
| 55 | +class Signer(Protocol): |
| 56 | + def sign(self, message: bytes) -> bytes: |
| 57 | + """Returns the signature of `message`.""" |
| 58 | + ... |
| 59 | + |
| 60 | + |
| 61 | +class Verifier(Protocol): |
| 62 | + def verify(self, message: bytes, signature: bytes) -> bool: |
| 63 | + """Returns true if `message` was signed by `signature`.""" |
| 64 | + ... |
| 65 | + |
| 66 | + |
| 67 | +# Collection of verifiers, each of which is associated with a name. |
| 68 | +VerifierList = Iterable[Tuple[str, Verifier]] |
| 69 | + |
| 70 | + |
| 71 | +@dataclasses.dataclass |
| 72 | +class VerifiedPayload: |
| 73 | + payloadType: str |
| 74 | + payload: bytes |
| 75 | + recognizedSigners: List[str] # List of names of signers |
| 76 | + |
| 77 | + |
| 78 | +def b64enc(m: bytes) -> str: |
| 79 | + return base64.standard_b64encode(m).decode('utf-8') |
| 80 | + |
| 81 | + |
| 82 | +def b64dec(m: str) -> bytes: |
| 83 | + m = m.encode('utf-8') |
| 84 | + try: |
| 85 | + return base64.b64decode(m, validate=True) |
| 86 | + except binascii.Error: |
| 87 | + return base64.b64decode(m, altchars='-_', validate=True) |
| 88 | + |
| 89 | + |
| 90 | +def PAE(payloadType: str, payload: bytes) -> bytes: |
| 91 | + return b''.join([ |
| 92 | + struct.pack('<Q', 2), |
| 93 | + struct.pack('<Q', len(payloadType)), |
| 94 | + payloadType.encode('utf-8'), |
| 95 | + struct.pack('<Q', len(payload)), payload |
| 96 | + ]) |
| 97 | + |
| 98 | + |
| 99 | +def Sign(payloadType: str, payload: bytes, signer: Signer) -> str: |
| 100 | + return json.dumps({ |
| 101 | + 'payload': |
| 102 | + b64enc(payload), |
| 103 | + 'payloadType': |
| 104 | + payloadType, |
| 105 | + 'signatures': [{ |
| 106 | + 'sig': b64enc(signer.sign(PAE(payloadType, payload))) |
| 107 | + }], |
| 108 | + }) |
| 109 | + |
| 110 | + |
| 111 | +def Verify(json_signature: str, verifiers: VerifierList) -> VerifiedPayload: |
| 112 | + wrapper = json.loads(json_signature) |
| 113 | + payloadType = wrapper['payloadType'] |
| 114 | + payload = b64dec(wrapper['payload']) |
| 115 | + pae = PAE(payloadType, payload) |
| 116 | + recognizedSigners = [] |
| 117 | + for signature in wrapper['signatures']: |
| 118 | + for name, verifier in verifiers: |
| 119 | + if verifier.verify(pae, b64dec(signature['sig'])): |
| 120 | + recognizedSigners.append(name) |
| 121 | + if not recognizedSigners: |
| 122 | + raise ValueError('No valid signature found') |
| 123 | + return VerifiedPayload(payloadType, payload, recognizedSigners) |
| 124 | + |
| 125 | + |
| 126 | +if __name__ == '__main__': |
| 127 | + import doctest |
| 128 | + doctest.testmod() |
0 commit comments