Dead Simple Signing Envelopes (DSSE)

I had a great talk with Chris Were yesterday, and he talked about the importance of digital signatures. Like it or not, digital signatures…

Dead Simple Signing Envelopes (DSSE)

I had a great talk with Chris Were yesterday, and he talked about the importance of digital signatures. Like it or not, digital signatures provide the core of trust within the Internet. With this, Bob generates a key pair and signs the hash of a message with his private key, and then Alice verifies this signature with his public key. But how do we identify parts of the signature and associated keys? For this, we can use wrappers or envelopes to embed the information.

TUF/In-Toto Metablocks

The Update Framework (TUF) provides a framework to secure the software within a system, while in-toto (“as-a-whole”) can be used to protect the whole of the software support chain. TUF thus delivers updates to a system while in-toto manages a complete software infrastructure. For this, it uses the Metablock format, and where we do not sign the byte values for the signature but sign the JSON data. This is known as canonicalisation - and which is the conversion of data in different forms to a standardised format.

With this we get details of the algorithm use, the key ID, the public key and the signature. In the following, we see that we are using two rsassa-pss-sha256 keys for signing (5f7879 … 9561de62c and 8b50265 … 8c1082abbac9e7e959ec):

{
"signatures": [
{
"keyid": "6e18dfbf96b117b7a36196cfd782ecfeb2d08369fb566539f90ad9ac40f152dd",
"sig": "052b3a75b7d2693ba8c0cab42cebfd6fe9547589426bebf8bac863d3e2e454b3c88b1c1ab2586cf16b4f2070e16027e6b5299f6e15ba64bd12000709202e483899766b0d5ce23f0ddcecf58a4456ea09918bb24c0df33f4b3477e7c1454c6b28efb0ed374d1b150fbbca23cd1a296ae5cd74ec55da0d1b58ccc6c6b9ebbeee41860438910417ded40994672690553c1bc354bce5a92bc164188976857bdce56789f9558aee646730d22f511a20efa210e5cd7e87612f5f7738a7a88c391c67cefc29f345b784b877414e026149649d12fbec5c6d6c3d2c7f4eb3edee0dd356a52d2cddb176dfef62413b92f13b719a0bb6ba4fc7122313e5bae50272bc27f3a5bbd75abfee95289168ddf3b3bd061d3f805dde21386a9e72c2beb02500ee1f93724c3e35cca7f8c23223f2f293a1d6f37b2eaf9fb1ae3913c0d0039b74e4b18ff005c4ec18b929b3c0efe0d1a23bdf81237c2c5fb6e3fc5777ea77f44da7cdfbad913a7754549a0ddd9579ea0c2b9cc81bebaacb7be50ea5d35b65b0fecf63cd"
}
],
"signed": {
"_type": "layout",
"expires": "2023-12-03T10:05:34Z",
"inspect": [
{
"_type": "inspection",
"expected_materials": [
[
"MATCH",
"demo-project.tar.gz",
"WITH",
"PRODUCTS",
"FROM",
"package"
]
],
"expected_products": [
[
"MATCH",
"demo-project/foo.py",
"WITH",
"PRODUCTS",
"FROM",
"update-version"
]
],
"name": "untar",
"run": [
"tar",
"xzf",
"demo-project.tar.gz"
]
}
],
"keys": {
"5f78794eaa621199f21364cd08ca49090930373e8e06645dbb719869561de62c": {
"keyid": "5f78794eaa621199f21364cd08ca49090930373e8e06645dbb719869561de62c",
"keyid_hash_algorithms": [
"sha256",
"sha512"
],
"keytype": "rsa",
"keyval": {
"private": "",
"public": "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA3hxpbQ/Ns7uYIxYRE1Sn\nRkdzzVpb5QWPJ25QrObXYlZNG/kaIEvUQYhyXxbRu0c/RtVAVuOeTlij1U8q1KZa\novgwXb8G2BXZwIJISjzwu8f0QmNBm+lY4GllWhZz6eKo6bQk/43YYP5cpinVDEZY\nxwmdqO6sFpsdxeo3A6eK0erV0Fiy/wz609X1qZr3fyWQyKrq9oeG/WuEndHS7Uuv\nEYt2CYZ5RhjTXG5Gw49hZV4P4RnCn0hKXFEWj0qeUBE4FB6RcfeyC5bnV9GLSHz4\n3ZwGymzp2FhRLIeVfGPd0rgzw1SkFgRPeH/CD+hycyuY+75gZhPxnsrMNANP175z\n0FXdvfFqSLx7sYOU/VLjmvXJbsmeIRFP3v4PurWSU6T3DeSH3aMvMgDFOCrb3uWx\njnKb2Les1YmkoNKUCJYbk1bXoE31fBTW2BLylWCwJ8355kOr/PEHh7gMJUa0EGw5\noUUDgtqUKqjhjSS/aLq2PTN9A1yHy5fHKUFyTSrQjsqFAgMBAAE=\n-----END PUBLIC KEY-----"
},
"scheme": "rsassa-pss-sha256"
},
"8b50265fd2ba917d5d444ef0bac3fa96a6cfae940d458c1082abbac9e7e959ec": {
"keyid": "8b50265fd2ba917d5d444ef0bac3fa96a6cfae940d458c1082abbac9e7e959ec",
"keyid_hash_algorithms": [
"sha256",
"sha512"
],
"keytype": "rsa",
"keyval": {
"private": "",
"public": "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvfGOZnz7ADHqQ348KLqd\nYd1M0/ZM/BjAvKHx0ql86i2gkULYJSfPK9jjuaTVzQ9ITQZVlbHS9ThEkIQO7/FI\nop1NG8224QVSjf8vD32dH4SyPHrRpkdUgu9EE0DqLiaQXduhQn31BvwMFlIASQvS\nDHKVTk+VO8T1fRmrAQx/3AFAK43ForYCT19b48TwAgL3K3f4C6YesJNmfSDoPYo7\nZjrQ+DdcC0cxGS3AV2NMIqWc6gKoYT6fAd0MTe5g1qrR10lnNC7+dYFuxt9SkoG0\nT4+Gbt5jpczgM/0xOjGQyVXjOJxpX35xzD9a75kocRYQx6gQFgX0kV5HopoaEKgC\n/rYVBZsdj/Wm6qn7vhETNLAveVB2EtSH+P905anrzUDQ6/GlXrPI+tZ9MandklaU\newA7MEaIAWuuTr9DU7igRaIa8MsPT8C5t2MlBa999qsJ8irrWFa3svhrxKb2CDl4\n9F4RD2LE4QElcR+nMkIQb4wXpzsQJoga6d1We8MiIIbNAgMBAAE=\n-----END PUBLIC KEY-----"
},
"scheme": "rsassa-pss-sha256"
}
},
"readme": "",
"steps": [
{
"_type": "step",
"expected_command": [
"git",
"clone",
"https://github.com/in-toto/demo-project.git"
],
"expected_materials": [],
"expected_products": [
[
"CREATE",
"demo-project/foo.py"
],
[
"DISALLOW",
"*"
]
],
"name": "clone",
"pubkeys": [
"5f78794eaa621199f21364cd08ca49090930373e8e06645dbb719869561de62c"
],
"threshold": 1
},
{
"_type": "step",
"expected_command": [],
"expected_materials": [
[
"MATCH",
"demo-project/*",
"WITH",
"PRODUCTS",
"FROM",
"clone"
],
[
"DISALLOW",
"*"
]
],
"expected_products": [
[
"ALLOW",
"demo-project/foo.py"
],
[
"DISALLOW",
"*"
]
],
"name": "update-version",
"pubkeys": [
"5f78794eaa621199f21364cd08ca49090930373e8e06645dbb719869561de62c"
],
"threshold": 1
},
{
"_type": "step",
"expected_command": [
"tar",
"--exclude",
".git",
"-zcvf",
"demo-project.tar.gz",
"demo-project"
],
"expected_materials": [
[
"MATCH",
"demo-project/*",
"WITH",
"PRODUCTS",
"FROM",
"update-version"
],
[
"DISALLOW",
"*"
]
],
"expected_products": [
[
"CREATE",
"demo-project.tar.gz"
],
[
"DISALLOW",
"*"
]
],
"name": "package",
"pubkeys": [
"8b50265fd2ba917d5d444ef0bac3fa96a6cfae940d458c1082abbac9e7e959ec"
],
"threshold": 1
}
]
}
}

DSSE

The Metablock format in in-toto and TUF need canonicalisation, and but DSSE has no need for this and can thus apply itself to byte streams [here]. It also simplifies the formatting of the signature and supports an envelope that supports multiple signatures for the same signed data. DSSE also supports a PAE (Pre-Auth-Encoding) and which provides a safe way to serialization and deserialization data that is untrusted.

In the following example of DSSE, we have a message of “Hello”, and which is “SGVsbG8=” in Base64 encoding. The payload type will define the format of the data, such as for a JSON form.

Message: Hello 123
Type: http://example.com/testing

Private key: EccKey(curve='NIST P-256',
point_x=33796162813166805588710121804061061246441408202115825092749411076575922636412,
point_y=445423445026610045218754189758102393455802184758516107499727218923652216278,
d=115109344795764689841124184802822027938123581634343585745872628005620037228718)

Public key: EccKey(curve='NIST P-256',
point_x=33796162813166805588710121804061061246441408202115825092749411076575922636412,
point_y=445423445026610045218754189758102393455802184758516107499727218923652216278)

Key ID: 2a7141d6


== Signing ==
{'payload': 'SGVsbG8gMTIz',
'payloadType': 'http://example.com/testing',
'signatures': [{'keyid': '2a7141d6',
'sig': 'i16opxQrAEnXJ7x9kTWYd2LtZwEoRQqhYEvxoexvj2/tKXX6DZE8cbQ3eEGb8ZtuWcMk2LjDLyhpQ2BSGcwQnA=='}]}


== Verification ==
VerifiedPayload(payloadType='http://example.com/testing',
payload=b'Hello 123', recognizedSigners=['mykey'])


DSSEv1
b'DSSEv1 26 http://example.com/testing 9 Hello 123'

We can see that the envelope used in the signature is [here]:

{'payload': 'SGVsbG8gMTIz',
'payloadType': 'http://example.com/testing',
'signatures': [{'keyid': '2a7141d6',
'sig': 'i16opxQrAEnXJ7x9kTWYd2LtZwEoRQqhYEvxoexvj2/tKXX6DZE8cbQ3eEGb8ZtuWcMk2LjDLyhpQ2BSGcwQnA=='}]}

For this, “Hello 123” in Base64 encoding is “SGVsbG8gMTIz”. We then have a signature of “i16opxQrAE … wQnA==” and which has been signed with the key ID of “2a7141d6”. The key that we have generated as an ID of “2a7141d6”, and where we have a private key of d, and a public key of (point_x, point_y) [here]:

Private key: EccKey(curve='NIST P-256', 
point_x=33796162813166805588710121804061061246441408202115825092749411076575922636412,
point_y=445423445026610045218754189758102393455802184758516107499727218923652216278,
d=115109344795764689841124184802822027938123581634343585745872628005620037228718)

Public key: EccKey(curve='NIST P-256',
point_x=33796162813166805588710121804061061246441408202115825092749411076575922636412,
point_y=445423445026610045218754189758102393455802184758516107499727218923652216278)

Key ID: 2a7141d6

The PAE format in this case is [here]:

DSSEv1
b'DSSEv1 26 http://example.com/testing 9 Hello 123'

and where we read 26 characters for the payload type and then nine characters for the message. The format is:

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

The code for this is taken from [here][here]:


import base64, binascii, dataclasses, json, struct
import os, sys
from pprint import pprint
import ecdsa_new

# Protocol requires Python 3.8+.
from typing import Iterable, List, Optional, Protocol, Tuple


class Signer(Protocol):
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]]


@dataclasses.dataclass
class VerifiedPayload:
payloadType: str
payload: bytes
recognizedSigners: List[str] # List of names of signers


def b64enc(m: bytes) -> str:
return base64.standard_b64encode(m).decode('utf-8')


def b64dec(m: str) -> bytes:
m = m.encode('utf-8')
try:
return base64.b64decode(m, validate=True)
except binascii.Error:
return base64.b64decode(m, altchars='-_', validate=True)


def PAE(payloadType: str, payload: bytes) -> bytes:
return b'DSSEv1 %d %b %d %b' % (
len(payloadType), payloadType.encode('utf-8'),
len(payload), payload)


def Sign(payloadType: str, payload: bytes, signer: Signer) -> 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': [signature],
})

def Verify(json_signature: str, verifiers: VerifierList) -> VerifiedPayload:
wrapper = json.loads(json_signature)
payloadType = wrapper['payloadType']
payload = b64dec(wrapper['payload'])
pae = PAE(payloadType, payload)
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:
raise ValueError('No valid signature found')
return VerifiedPayload(payloadType, payload, recognizedSigners)


payload = b'hello world'
payloadType = 'http://example.com/HelloWorld'
if (len(sys.argv)>1):
payload=str(sys.argv[1]).encode()
if (len(sys.argv)>2):
payloadType=str(sys.argv[2])

print(f"Message: {payload.decode()}")
print(f"Type: {payloadType}")
signer = ecdsa_new.Signer.generate(curve='P-256')
verifier = ecdsa_new.Verifier(signer.public_key)



print(f"\nPrivate key: {signer.secret_key}")
print(f"Public key: {signer.public_key}")
print(f"Key ID: {signer.keyid()}")

print("\n\n== Signing ==")
signature_json = Sign(payloadType, payload, signer)
pprint(json.loads(signature_json))
print("\n\n== Verification ==")
result = Verify(signature_json, [('mykey', verifier)])
pprint(result)
print("\n\nDSSEv1")
pprint(PAE(payloadType, payload))

Conclusions

DSSE is a nice and simple way to define signatures. The key advantage of DSSE over in-toto is that it can be used with any message encoding, and not just JSON. The payload type of the message is also covered by the authenication, and will not use canonicalisation.