Skip to content

Commit cacf247

Browse files
authored
Merge pull request #37 from MarkLodato/dsse
Rename to DSSE, update PAE, mark as v1
2 parents 894a055 + 04e1715 commit cacf247

File tree

5 files changed

+78
-54
lines changed

5 files changed

+78
-54
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# signing-spec
1+
# DSSE: Dead Simple Signing Envelope
22

33
Simple, foolproof standard for signing arbitrary data.
44

@@ -22,7 +22,8 @@ Specifications for:
2222

2323
Out of scope (for now at least):
2424

25-
* Key management / PKI
25+
* Key management / PKI /
26+
[exclusive ownership](https://www.bolet.org/~pornin/2005-acns-pornin+stern.pdf)
2627

2728
## Why not...?
2829

envelope.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# signing-spec Envelope
1+
# DSSE Envelope
22

33
March 03, 2021
44

5-
Version 0.1.0
5+
Version 1.0.0
66

7-
This document describes the recommended data structure for storing signing-spec
7+
This document describes the recommended data structure for storing DSSE
88
signatures, which we call the "JSON Envelope". For the protocol/algorithm, see
99
[Protocol](protocol.md).
1010

@@ -29,16 +29,13 @@ the following form, called the "JSON envelope":
2929

3030
See [Protocol](protocol.md) for a definition of parameters and functions.
3131

32-
Empty fields may be omitted. [Multiple signatures](#multiple-signatures) are
33-
allowed.
34-
3532
Base64() is [Base64 encoding](https://tools.ietf.org/html/rfc4648), transforming
3633
a byte sequence to a unicode string. Either standard or URL-safe encoding is
3734
allowed.
3835

3936
### Multiple signatures
4037

41-
An envelope may have more than one signature, which is equivalent to separate
38+
An envelope MAY have more than one signature, which is equivalent to separate
4239
envelopes with individual signatures.
4340

4441
```json
@@ -55,6 +52,15 @@ envelopes with individual signatures.
5552
}
5653
```
5754

55+
### Parsing rules
56+
57+
* The following fields are REQUIRED and MUST be set, even if empty: `payload`,
58+
`payloadType`, `signature`, `signature.sig`.
59+
* The following fields are OPTIONAL and MAY be unset: `signature.keyid`.
60+
An unset field MUST be treated the same as set-but-empty.
61+
* Producers, or future versions of the spec, MAY add additional fields.
62+
Consumers MUST ignore unrecognized fields.
63+
5864
## Other data structures
5965

6066
The standard envelope is JSON message with an explicit `payloadType`.

implementation/ecdsa.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ def sign(self, message: bytes) -> bytes:
2828
h = SHA256.new(message)
2929
return DSS.new(self.secret_key, 'deterministic-rfc6979').sign(h)
3030

31+
def keyid(self) -> str:
32+
"""Returns a fingerprint of the public key."""
33+
return Verifier(self.public_key).keyid()
34+
3135

3236
class Verifier:
3337
def __init__(self, public_key):
@@ -41,3 +45,10 @@ def verify(self, message: bytes, signature: bytes) -> bool:
4145
return True
4246
except ValueError:
4347
return False
48+
49+
def keyid(self) -> str:
50+
"""Returns a fingerprint of the public key."""
51+
# Note: This is a hack for demonstration purposes. A proper fingerprint
52+
# should be used.
53+
key = self.public_key.export_key(format='OpenSSH').encode('ascii')
54+
return SHA256.new(key).hexdigest()[:8]

implementation/signing_spec.py

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
The following example requires `pip3 install pycryptodome` and uses ecdsa.py in
77
the same directory as this file.
88
9-
>>> import binascii, os, sys, textwrap
9+
>>> import os, sys
1010
>>> from pprint import pprint
1111
>>> sys.path.insert(0, os.path.dirname(__file__))
1212
>>> import ecdsa
@@ -26,7 +26,8 @@
2626
>>> pprint(json.loads(signature_json))
2727
{'payload': 'aGVsbG8gd29ybGQ=',
2828
'payloadType': 'http://example.com/HelloWorld',
29-
'signatures': [{'sig': 'Cc3RkvYsLhlaFVd+d6FPx4ZClhqW4ZT0rnCYAfv6/ckoGdwT7g/blWNpOBuL/tZhRiVFaglOGTU8GEjm4aEaNA=='}]}
29+
'signatures': [{'keyid': '66301bbf',
30+
'sig': 'A3JqsQGtVsJ2O2xqrI5IcnXip5GToJ3F+FnZ+O88SjtR6rDAajabZKciJTfUiHqJPcIAriEGAHTVeCUjW2JIZA=='}]}
3031
3132
Verification example:
3233
@@ -36,33 +37,35 @@
3637
3738
PAE:
3839
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
40+
>>> PAE(payloadType, payload)
41+
b'DSSEv1 29 http://example.com/HelloWorld 11 hello world'
4742
"""
4843

4944
import base64, binascii, dataclasses, json, struct
5045

5146
# Protocol requires Python 3.8+.
52-
from typing import Iterable, List, Protocol, Tuple
47+
from typing import Iterable, List, Optional, Protocol, Tuple
5348

5449

5550
class Signer(Protocol):
5651
def sign(self, message: bytes) -> bytes:
5752
"""Returns the signature of `message`."""
5853
...
5954

55+
def keyid(self) -> Optional[str]:
56+
"""Returns the ID of this key, or None if not supported."""
57+
...
58+
6059

6160
class Verifier(Protocol):
6261
def verify(self, message: bytes, signature: bytes) -> bool:
6362
"""Returns true if `message` was signed by `signature`."""
6463
...
6564

65+
def keyid(self) -> Optional[str]:
66+
"""Returns the ID of this key, or None if not supported."""
67+
...
68+
6669

6770
# Collection of verifiers, each of which is associated with a name.
6871
VerifierList = Iterable[Tuple[str, Verifier]]
@@ -88,23 +91,22 @@ def b64dec(m: str) -> bytes:
8891

8992

9093
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-
])
94+
return b'DSSEv1 %d %b %d %b' % (
95+
len(payloadType), payloadType.encode('utf-8'),
96+
len(payload), payload)
9797

9898

9999
def Sign(payloadType: str, payload: bytes, signer: Signer) -> str:
100+
signature = {
101+
'keyid': signer.keyid(),
102+
'sig': b64enc(signer.sign(PAE(payloadType, payload))),
103+
}
104+
if not signature['keyid']:
105+
del signature['keyid']
100106
return json.dumps({
101-
'payload':
102-
b64enc(payload),
103-
'payloadType':
104-
payloadType,
105-
'signatures': [{
106-
'sig': b64enc(signer.sign(PAE(payloadType, payload)))
107-
}],
107+
'payload': b64enc(payload),
108+
'payloadType': payloadType,
109+
'signatures': [signature],
108110
})
109111

110112

@@ -116,6 +118,10 @@ def Verify(json_signature: str, verifiers: VerifierList) -> VerifiedPayload:
116118
recognizedSigners = []
117119
for signature in wrapper['signatures']:
118120
for name, verifier in verifiers:
121+
if (signature.get('keyid') is not None and
122+
verifier.keyid() is not None and
123+
signature.get('keyid') != verifier.keyid()):
124+
continue
119125
if verifier.verify(pae, b64dec(signature['sig'])):
120126
recognizedSigners.append(name)
121127
if not recognizedSigners:

protocol.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
# signing-spec Protocol
1+
# DSSE Protocol
22

33
March 03, 2021
44

5-
Version 0.1.0
5+
Version 1.0.0
66

7-
This document describes the protocol/algorithm for creating and verifying
8-
signing-spec signatures, independent of how they are transmitted or stored. For
9-
the recommended data structure, see [Envelope](envelope.md).
7+
This document describes the protocol/algorithm for creating and verifying DSSE
8+
signatures, independent of how they are transmitted or stored. For the
9+
recommended data structure, see [Envelope](envelope.md).
1010

1111
## Signature Definition
1212

@@ -24,7 +24,7 @@ SERIALIZED_BODY | bytes | Yes | Yes
2424
PAYLOAD_TYPE | string | Yes | Yes
2525
KEYID | string | No | No
2626

27-
* SERIALIZED_BODY: Byte sequence to be signed.
27+
* SERIALIZED_BODY: Arbitrary byte sequence to be signed.
2828

2929
* PAYLOAD_TYPE: Opaque, case-sensitive string that uniquely and unambiguously
3030
identifies how to interpret `payload`. This includes both the encoding
@@ -34,10 +34,11 @@ KEYID | string | No | No
3434
* [Media Type](https://www.iana.org/assignments/media-types/), a.k.a. MIME
3535
type or Content Type
3636
* Example: `application/vnd.in-toto+json`.
37-
* IMPORTANT: SHOULD NOT be a generic type that only represents
38-
encoding but not schema. For example, `application/json` is almost
39-
always WRONG. Instead, invent a media type specific for your
40-
application in the `application/vnd` namespace.
37+
* IMPORTANT: This SHOULD be an application-specific type describing
38+
both encoding and schema, NOT a generic type like
39+
`application/json`. The problem with generic types is that two
40+
different applications could use the same encoding (e.g. JSON) but
41+
interpret the payload differently.
4142
* SHOULD be lowercase.
4243
* [URI](https://tools.ietf.org/html/rfc3986)
4344
* Example: `https://example.com/MyMessage/v1-json`.
@@ -53,13 +54,15 @@ KEYID | string | No | No
5354

5455
Functions:
5556

56-
* PAE() is the
57-
[PASETO Pre-Authentication Encoding](https://github.com/paragonie/paseto/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding),
58-
where parameters `type` and `body` are byte sequences:
57+
* PAE() is the "Pre-Authentication Encoding", where parameters `type` and
58+
`body` are byte sequences:
5959

6060
```none
61-
PAE(type, body) := le64(2) || le64(len(type)) || type || le64(len(body)) || body
62-
le64(n) := 64-bit little-endian encoding of `n`, where 0 <= n < 2^63
61+
PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
62+
+ = concatenation
63+
SP = ASCII space [0x20]
64+
"DSSEv1" = ASCII [0x44, 0x53, 0x53, 0x45, 0x76, 0x31]
65+
LEN(s) = ASCII decimal encoding of the byte length of s, with no leading zeros
6366
```
6467
6568
* Sign() is an arbitrary digital signature format. Details are agreed upon
@@ -102,7 +105,7 @@ either, and verifiers **MUST** accept either.
102105
103106
## Test Vectors
104107
105-
See [reference implementation](reference_implementation.ipynb). Here is an
108+
See [reference implementation](implementation/signing_spec.py). Here is an
106109
example.
107110
108111
SERIALIZED_BODY:
@@ -120,10 +123,7 @@ http://example.com/HelloWorld
120123
PAE:
121124

122125
```none
123-
02 00 00 00 00 00 00 00 1d 00 00 00 00 00 00 00
124-
68 74 74 70 3a 2f 2f 65 78 61 6d 70 6c 65 2e 63
125-
6f 6d 2f 48 65 6c 6c 6f 57 6f 72 6c 64 0b 00 00
126-
00 00 00 00 00 68 65 6c 6c 6f 20 77 6f 72 6c 64
126+
DSSEv1 29 http://example.com/HelloWorld 11 hello world
127127
```
128128

129129
Cryptographic keys:
@@ -141,7 +141,7 @@ Result (using the recommended [JSON envelope](envelope.md)):
141141
```json
142142
{"payload": "aGVsbG8gd29ybGQ=",
143143
"payloadType": "http://example.com/HelloWorld",
144-
"signatures": [{"sig": "y7BK8Mm8Mr4gxk4+G9X3BD1iBc/vVVuJuV4ubmsEK4m/8MhQOOS26ejx+weIjyAx8VjYoZRPpoXSNjHEzdE7nQ=="}]}
144+
"signatures": [{"sig": "A3JqsQGtVsJ2O2xqrI5IcnXip5GToJ3F+FnZ+O88SjtR6rDAajabZKciJTfUiHqJPcIAriEGAHTVeCUjW2JIZA=="}]}
145145
```
146146

147147
[Canonical JSON]: http://wiki.laptop.org/go/Canonical_JSON

0 commit comments

Comments
 (0)