diff --git a/envelope.md b/envelope.md index 0aa12ea..48923c3 100644 --- a/envelope.md +++ b/envelope.md @@ -22,7 +22,8 @@ the following form, called the "JSON envelope": "payloadType": "", "signatures": [{ "keyid": "", - "sig": "" + "sig": "", + "cert": "" }] } ``` @@ -33,6 +34,8 @@ 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. +PEM() is a [PEM encoding](https://datatracker.ietf.org/doc/html/rfc1421), transforming a DER (binary) encoded X.509 certificate to a base64 encoding with a one-line header and footer. + ### Multiple signatures An envelope MAY have more than one signature, which is equivalent to separate @@ -44,10 +47,12 @@ envelopes with individual signatures. "payloadType": "", "signatures": [{ "keyid": "", - "sig": "" + "sig": "", + "cert": "" }, { "keyid": "", - "sig": "" + "sig": "", + "cert": "" }] } ``` @@ -56,7 +61,7 @@ envelopes with individual signatures. * 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`. +* The following fields are OPTIONAL and MAY be unset: `signature.keyid`, `signature.cert` 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. diff --git a/envelope.proto b/envelope.proto index c16db11..1aa1fd7 100644 --- a/envelope.proto +++ b/envelope.proto @@ -32,4 +32,8 @@ message Signature { // *Unauthenticated* hint identifying which public key was used. // OPTIONAL. string keyid = 2; + + // *Unauthenticated* PEM encoded X.509 certificate chain corresponding to the public key. + // OPTIONAL. + string cert = 3; } diff --git a/implementation/signing_spec.py b/implementation/signing_spec.py index 0e5fea3..335e954 100644 --- a/implementation/signing_spec.py +++ b/implementation/signing_spec.py @@ -41,7 +41,11 @@ b'DSSEv1 29 http://example.com/HelloWorld 11 hello world' """ -import base64, binascii, dataclasses, json, struct +import base64 +import binascii +import dataclasses +import json +import struct # Protocol requires Python 3.8+. from typing import Iterable, List, Optional, Protocol, Tuple @@ -56,9 +60,14 @@ def keyid(self) -> Optional[str]: """Returns the ID of this key, or None if not supported.""" ... + def certificate(self) -> Optional[str]: + """Returns the cert chain of the key in PEM format, or None if not supported.""" +# If a Verifier does not accept certificates, it MUST ignore `cert`, +# If it does, it MUST verify `cert` against a known root pool and decided constraints +# before verifying that `signature` was signed by `cert`. class Verifier(Protocol): - def verify(self, message: bytes, signature: bytes) -> bool: + def verify(self, message: bytes, signature: bytes, cert: Optional[str]) -> bool: """Returns true if `message` was signed by `signature`.""" ... @@ -92,17 +101,20 @@ def b64dec(m: str) -> bytes: def PAE(payloadType: str, payload: bytes) -> bytes: return b'DSSEv1 %d %b %d %b' % ( - len(payloadType), payloadType.encode('utf-8'), - len(payload), payload) + 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))), + 'cert': signer.cert(), } if not signature['keyid']: del signature['keyid'] + if not signature['cert']: + del signature['cert'] return json.dumps({ 'payload': b64enc(payload), 'payloadType': payloadType, @@ -120,9 +132,9 @@ def Verify(json_signature: str, verifiers: VerifierList) -> VerifiedPayload: for name, verifier in verifiers: if (signature.get('keyid') is not None and verifier.keyid() is not None and - signature.get('keyid') != verifier.keyid()): + signature.get('keyid') != verifier.keyid()): continue - if verifier.verify(pae, b64dec(signature['sig'])): + if verifier.verify(pae, b64dec(signature['sig']), signature.get('cert')): recognizedSigners.append(name) if not recognizedSigners: raise ValueError('No valid signature found') diff --git a/protocol.md b/protocol.md index ec0d017..ca77086 100644 --- a/protocol.md +++ b/protocol.md @@ -23,6 +23,7 @@ Name | Type | Required | Authenticated SERIALIZED_BODY | bytes | Yes | Yes PAYLOAD_TYPE | string | Yes | Yes KEYID | string | No | No +CERTIFICATE | string | No | No * SERIALIZED_BODY: Arbitrary byte sequence to be signed. @@ -52,6 +53,12 @@ KEYID | string | No | No decisions; it may only be used to narrow the selection of possible keys to try. +* CERTIFICATE: Optional, unauthenticated PEM encoded X.509 certificate chain for + the key used to sign the message. As with Sign(), details on the trusted root + certificates are agreed upon out-of-band by the signer and verifier. This + ensures the necessary information to verify the signature remains alongside + the metadata. + Functions: * PAE() is the "Pre-Authentication Encoding", where parameters `type` and @@ -77,7 +84,7 @@ Functions: Out of band: - Agree on a PAYLOAD_TYPE and cryptographic details, optionally including - KEYID. + KEYID and trusted root certificates and constraints. To sign: @@ -85,17 +92,19 @@ To sign: SERIALIZED_BODY. - Sign PAE(UTF8(PAYLOAD_TYPE), SERIALIZED_BODY). Call the result SIGNATURE. - Optionally, compute a KEYID. -- Encode and transmit SERIALIZED_BODY, PAYLOAD_TYPE, SIGNATURE, and KEYID, - preferably using the recommended [JSON envelope](envelope.md). +- Encode and transmit SERIALIZED_BODY, PAYLOAD_TYPE, SIGNATURE, CERTIFICATE, + and KEYID, preferably using the recommended [JSON envelope](envelope.md). To verify: -- Receive and decode SERIALIZED_BODY, PAYLOAD_TYPE, SIGNATURE, and KEYID, such - as from the recommended [JSON envelope](envelope.md). Reject if decoding - fails. +- Receive and decode SERIALIZED_BODY, PAYLOAD_TYPE, SIGNATURE, KEYID, and + CERTIFICATE such as from the recommended [JSON envelope](envelope.md). + Reject if decoding fails. - Optionally, filter acceptable public keys by KEYID. -- Verify SIGNATURE against PAE(UTF8(PAYLOAD_TYPE), SERIALIZED_BODY). Reject if - the verification fails. +- Verify SIGNATURE against PAE(UTF8(PAYLOAD_TYPE), SERIALIZED_BODY) using + the predefined roots of trust and constraints optionally CERTIFICATE. If + CERTIFICATE is specified, it MUST be verified against a trusted root + certificate. Reject if the verification fails. - Reject if PAYLOAD_TYPE is not a supported type. - Parse SERIALIZED_BODY according to PAYLOAD_TYPE. Reject if the parsing fails. @@ -119,8 +128,10 @@ To verify a `(t, n)`-ENVELOPE: Reject if decoding fails. - For each (SIGNATURE, KEYID) in SIGNATURES, - Optionally, filter acceptable public keys by KEYID. - - Verify SIGNATURE against PAE(UTF8(PAYLOAD_TYPE), SERIALIZED_BODY). Skip - over if the verification fails. + - Verify SIGNATURE against PAE(UTF8(PAYLOAD_TYPE), SERIALIZED_BODY) using + the predefined roots of trust and constraints optionally CERTIFICATE. If + CERTIFICATE is specified, it MUST be verified against a trusted root + certificate. Reject if the verification fails. - Add the accepted public key to the set ACCEPTED_KEYS. - Break if the cardinality of ACCEPTED_KEYS is greater or equal to `t`. - Reject if the cardinality of ACCEPTED_KEYS is less than `t`.