diff --git a/README.md b/README.md index 2eb9d3d..5068e43 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# signing-spec +# DSSE: Dead Simple Signing Envelope Simple, foolproof standard for signing arbitrary data. @@ -22,7 +22,8 @@ Specifications for: Out of scope (for now at least): -* Key management / PKI +* Key management / PKI / + [exclusive ownership](https://www.bolet.org/~pornin/2005-acns-pornin+stern.pdf) ## Why not...? diff --git a/envelope.md b/envelope.md index 749759c..0aa12ea 100644 --- a/envelope.md +++ b/envelope.md @@ -1,10 +1,10 @@ -# signing-spec Envelope +# DSSE Envelope March 03, 2021 -Version 0.1.0 +Version 1.0.0 -This document describes the recommended data structure for storing signing-spec +This document describes the recommended data structure for storing DSSE signatures, which we call the "JSON Envelope". For the protocol/algorithm, see [Protocol](protocol.md). @@ -29,16 +29,13 @@ the following form, called the "JSON envelope": See [Protocol](protocol.md) for a definition of parameters and functions. -Empty fields may be omitted. [Multiple signatures](#multiple-signatures) are -allowed. - Base64() is [Base64 encoding](https://tools.ietf.org/html/rfc4648), transforming a byte sequence to a unicode string. Either standard or URL-safe encoding is allowed. ### Multiple signatures -An envelope may have more than one signature, which is equivalent to separate +An envelope MAY have more than one signature, which is equivalent to separate envelopes with individual signatures. ```json @@ -55,6 +52,15 @@ envelopes with individual signatures. } ``` +### Parsing rules + +* The following fields are REQUIRED and MUST be set, even if empty: `payload`, + `payloadType`, `signature`, `signature.sig`. +* The following fields are OPTIONAL and MAY be unset: `signature.keyid`. + An unset field MUST be treated the same as set-but-empty. +* Producers, or future versions of the spec, MAY add additional fields. + Consumers MUST ignore unrecognized fields. + ## Other data structures The standard envelope is JSON message with an explicit `payloadType`. diff --git a/implementation/ecdsa.py b/implementation/ecdsa.py index 7ad766f..b394cb5 100644 --- a/implementation/ecdsa.py +++ b/implementation/ecdsa.py @@ -28,6 +28,10 @@ def sign(self, message: bytes) -> bytes: h = SHA256.new(message) return DSS.new(self.secret_key, 'deterministic-rfc6979').sign(h) + def keyid(self) -> str: + """Returns a fingerprint of the public key.""" + return Verifier(self.public_key).keyid() + class Verifier: def __init__(self, public_key): @@ -41,3 +45,10 @@ def verify(self, message: bytes, signature: bytes) -> bool: return True except ValueError: return False + + def keyid(self) -> str: + """Returns a fingerprint of the public key.""" + # Note: This is a hack for demonstration purposes. A proper fingerprint + # should be used. + key = self.public_key.export_key(format='OpenSSH').encode('ascii') + return SHA256.new(key).hexdigest()[:8] diff --git a/implementation/signing_spec.py b/implementation/signing_spec.py index 1fd73c3..0e5fea3 100644 --- a/implementation/signing_spec.py +++ b/implementation/signing_spec.py @@ -6,7 +6,7 @@ The following example requires `pip3 install pycryptodome` and uses ecdsa.py in the same directory as this file. ->>> import binascii, os, sys, textwrap +>>> import os, sys >>> from pprint import pprint >>> sys.path.insert(0, os.path.dirname(__file__)) >>> import ecdsa @@ -26,7 +26,8 @@ >>> pprint(json.loads(signature_json)) {'payload': 'aGVsbG8gd29ybGQ=', 'payloadType': 'http://example.com/HelloWorld', - 'signatures': [{'sig': 'Cc3RkvYsLhlaFVd+d6FPx4ZClhqW4ZT0rnCYAfv6/ckoGdwT7g/blWNpOBuL/tZhRiVFaglOGTU8GEjm4aEaNA=='}]} + 'signatures': [{'keyid': '66301bbf', + 'sig': 'A3JqsQGtVsJ2O2xqrI5IcnXip5GToJ3F+FnZ+O88SjtR6rDAajabZKciJTfUiHqJPcIAriEGAHTVeCUjW2JIZA=='}]} Verification example: @@ -36,20 +37,14 @@ PAE: ->>> def print_hex(b: bytes): -... octets = ' '.join(textwrap.wrap(binascii.hexlify(b).decode('utf-8'), 2)) -... print(*textwrap.wrap(octets, 48), sep='\n') ->>> print_hex(PAE(payloadType, payload)) -02 00 00 00 00 00 00 00 1d 00 00 00 00 00 00 00 -68 74 74 70 3a 2f 2f 65 78 61 6d 70 6c 65 2e 63 -6f 6d 2f 48 65 6c 6c 6f 57 6f 72 6c 64 0b 00 00 -00 00 00 00 00 68 65 6c 6c 6f 20 77 6f 72 6c 64 +>>> PAE(payloadType, payload) +b'DSSEv1 29 http://example.com/HelloWorld 11 hello world' """ import base64, binascii, dataclasses, json, struct # Protocol requires Python 3.8+. -from typing import Iterable, List, Protocol, Tuple +from typing import Iterable, List, Optional, Protocol, Tuple class Signer(Protocol): @@ -57,12 +52,20 @@ def sign(self, message: bytes) -> bytes: """Returns the signature of `message`.""" ... + def keyid(self) -> Optional[str]: + """Returns the ID of this key, or None if not supported.""" + ... + class Verifier(Protocol): def verify(self, message: bytes, signature: bytes) -> bool: """Returns true if `message` was signed by `signature`.""" ... + def keyid(self) -> Optional[str]: + """Returns the ID of this key, or None if not supported.""" + ... + # Collection of verifiers, each of which is associated with a name. VerifierList = Iterable[Tuple[str, Verifier]] @@ -88,23 +91,22 @@ def b64dec(m: str) -> bytes: def PAE(payloadType: str, payload: bytes) -> bytes: - return b''.join([ - struct.pack(' str: + signature = { + 'keyid': signer.keyid(), + 'sig': b64enc(signer.sign(PAE(payloadType, payload))), + } + if not signature['keyid']: + del signature['keyid'] return json.dumps({ - 'payload': - b64enc(payload), - 'payloadType': - payloadType, - 'signatures': [{ - 'sig': b64enc(signer.sign(PAE(payloadType, payload))) - }], + 'payload': b64enc(payload), + 'payloadType': payloadType, + 'signatures': [signature], }) @@ -116,6 +118,10 @@ def Verify(json_signature: str, verifiers: VerifierList) -> VerifiedPayload: recognizedSigners = [] for signature in wrapper['signatures']: for name, verifier in verifiers: + if (signature.get('keyid') is not None and + verifier.keyid() is not None and + signature.get('keyid') != verifier.keyid()): + continue if verifier.verify(pae, b64dec(signature['sig'])): recognizedSigners.append(name) if not recognizedSigners: diff --git a/protocol.md b/protocol.md index fbd2dd6..56e3c1d 100644 --- a/protocol.md +++ b/protocol.md @@ -1,12 +1,12 @@ -# signing-spec Protocol +# DSSE Protocol March 03, 2021 -Version 0.1.0 +Version 1.0.0 -This document describes the protocol/algorithm for creating and verifying -signing-spec signatures, independent of how they are transmitted or stored. For -the recommended data structure, see [Envelope](envelope.md). +This document describes the protocol/algorithm for creating and verifying DSSE +signatures, independent of how they are transmitted or stored. For the +recommended data structure, see [Envelope](envelope.md). ## Signature Definition @@ -24,7 +24,7 @@ SERIALIZED_BODY | bytes | Yes | Yes PAYLOAD_TYPE | string | Yes | Yes KEYID | string | No | No -* SERIALIZED_BODY: Byte sequence to be signed. +* SERIALIZED_BODY: Arbitrary byte sequence to be signed. * PAYLOAD_TYPE: Opaque, case-sensitive string that uniquely and unambiguously identifies how to interpret `payload`. This includes both the encoding @@ -34,10 +34,11 @@ KEYID | string | No | No * [Media Type](https://www.iana.org/assignments/media-types/), a.k.a. MIME type or Content Type * Example: `application/vnd.in-toto+json`. - * IMPORTANT: SHOULD NOT be a generic type that only represents - encoding but not schema. For example, `application/json` is almost - always WRONG. Instead, invent a media type specific for your - application in the `application/vnd` namespace. + * IMPORTANT: This SHOULD be an application-specific type describing + both encoding and schema, NOT a generic type like + `application/json`. The problem with generic types is that two + different applications could use the same encoding (e.g. JSON) but + interpret the payload differently. * SHOULD be lowercase. * [URI](https://tools.ietf.org/html/rfc3986) * Example: `https://example.com/MyMessage/v1-json`. @@ -53,13 +54,15 @@ KEYID | string | No | No Functions: -* PAE() is the - [PASETO Pre-Authentication Encoding](https://github.com/paragonie/paseto/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding), - where parameters `type` and `body` are byte sequences: +* PAE() is the "Pre-Authentication Encoding", where parameters `type` and + `body` are byte sequences: ```none - PAE(type, body) := le64(2) || le64(len(type)) || type || le64(len(body)) || body - le64(n) := 64-bit little-endian encoding of `n`, where 0 <= n < 2^63 + PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body + + = concatenation + SP = ASCII space [0x20] + "DSSEv1" = ASCII [0x44, 0x53, 0x53, 0x45, 0x76, 0x31] + LEN(s) = ASCII decimal encoding of the byte length of s, with no leading zeros ``` * Sign() is an arbitrary digital signature format. Details are agreed upon @@ -102,7 +105,7 @@ either, and verifiers **MUST** accept either. ## Test Vectors -See [reference implementation](reference_implementation.ipynb). Here is an +See [reference implementation](implementation/signing_spec.py). Here is an example. SERIALIZED_BODY: @@ -120,10 +123,7 @@ http://example.com/HelloWorld PAE: ```none -02 00 00 00 00 00 00 00 1d 00 00 00 00 00 00 00 -68 74 74 70 3a 2f 2f 65 78 61 6d 70 6c 65 2e 63 -6f 6d 2f 48 65 6c 6c 6f 57 6f 72 6c 64 0b 00 00 -00 00 00 00 00 68 65 6c 6c 6f 20 77 6f 72 6c 64 +DSSEv1 29 http://example.com/HelloWorld 11 hello world ``` Cryptographic keys: @@ -141,7 +141,7 @@ Result (using the recommended [JSON envelope](envelope.md)): ```json {"payload": "aGVsbG8gd29ybGQ=", "payloadType": "http://example.com/HelloWorld", - "signatures": [{"sig": "y7BK8Mm8Mr4gxk4+G9X3BD1iBc/vVVuJuV4ubmsEK4m/8MhQOOS26ejx+weIjyAx8VjYoZRPpoXSNjHEzdE7nQ=="}]} + "signatures": [{"sig": "A3JqsQGtVsJ2O2xqrI5IcnXip5GToJ3F+FnZ+O88SjtR6rDAajabZKciJTfUiHqJPcIAriEGAHTVeCUjW2JIZA=="}]} ``` [Canonical JSON]: http://wiki.laptop.org/go/Canonical_JSON