From b4f07d37ee5006629fedf9d1ef1636771ab80d73 Mon Sep 17 00:00:00 2001 From: Asra Ali Date: Wed, 27 Apr 2022 09:27:34 -0500 Subject: [PATCH 1/3] Add support for x509 certificates in DSSE Signed-off-by: Asra Ali --- envelope.md | 13 +++++++++---- envelope.proto | 4 ++++ implementation/signing_spec.py | 19 ++++++++++++++++--- protocol.md | 15 +++++++++++---- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/envelope.md b/envelope.md index 0aa12ea..88d0342 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](), 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..8804c1a 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 corresponding to the public key. + // OPTIONAL. + string cert = 3; } diff --git a/implementation/signing_spec.py b/implementation/signing_spec.py index 0e5fea3..9c61e4a 100644 --- a/implementation/signing_spec.py +++ b/implementation/signing_spec.py @@ -56,6 +56,9 @@ def keyid(self) -> Optional[str]: """Returns the ID of this key, or None if not supported.""" ... + def certificate(self) -> Optional[str]: + """Returns the certificate of the key, or None if not supported.""" + class Verifier(Protocol): def verify(self, message: bytes, signature: bytes) -> bool: @@ -66,10 +69,16 @@ def keyid(self) -> Optional[str]: """Returns the ID of this key, or None if not supported.""" ... +class RootPool(Protocol): + def verify(self, certificate: bytes) -> bool: + """Returns true if `certificate` chains back to the Root pool.""" # Collection of verifiers, each of which is associated with a name. VerifierList = Iterable[Tuple[str, Verifier]] +# A root certificate pool. +Root = RootPool + @dataclasses.dataclass class VerifiedPayload: @@ -100,9 +109,12 @@ 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, @@ -110,7 +122,7 @@ def Sign(payloadType: str, payload: bytes, signer: Signer) -> str: }) -def Verify(json_signature: str, verifiers: VerifierList) -> VerifiedPayload: +def Verify(json_signature: str, verifiers: VerifierList, root: RootPool) -> VerifiedPayload: wrapper = json.loads(json_signature) payloadType = wrapper['payloadType'] payload = b64dec(wrapper['payload']) @@ -122,8 +134,9 @@ def Verify(json_signature: str, verifiers: VerifierList) -> VerifiedPayload: verifier.keyid() is not None and signature.get('keyid') != verifier.keyid()): continue - if verifier.verify(pae, b64dec(signature['sig'])): - recognizedSigners.append(name) + if (verifier.verify(pae, b64dec(signature['sig'])) and + root.verify(signature['cert'])): + recognizedSigners.append(name) if not recognizedSigners: raise ValueError('No valid signature found') return VerifiedPayload(payloadType, payload, recognizedSigners) diff --git a/protocol.md b/protocol.md index ec0d017..9149bea 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,11 @@ 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 for the key + used to sign the message. As with Sign(), details 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 +83,7 @@ Functions: Out of band: - Agree on a PAYLOAD_TYPE and cryptographic details, optionally including - KEYID. + KEYID and trusted root certificates. To sign: @@ -90,12 +96,13 @@ To sign: 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. +- Optionally, verify the signing key's CERTIFICATE links back to a trusted root. - Reject if PAYLOAD_TYPE is not a supported type. - Parse SERIALIZED_BODY according to PAYLOAD_TYPE. Reject if the parsing fails. From 18f72d45db21559ae57ecb0179c3e177b769dbb2 Mon Sep 17 00:00:00 2001 From: Asra Ali Date: Wed, 27 Apr 2022 10:02:30 -0500 Subject: [PATCH 2/3] update certificate wording to include chain Signed-off-by: Asra Ali --- envelope.md | 2 +- envelope.proto | 2 +- implementation/signing_spec.py | 2 +- protocol.md | 11 ++++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/envelope.md b/envelope.md index 88d0342..af8ed78 100644 --- a/envelope.md +++ b/envelope.md @@ -23,7 +23,7 @@ the following form, called the "JSON envelope": "signatures": [{ "keyid": "", "sig": "", - "cert": "" + "cert": "" }] } ``` diff --git a/envelope.proto b/envelope.proto index 8804c1a..1aa1fd7 100644 --- a/envelope.proto +++ b/envelope.proto @@ -33,7 +33,7 @@ message Signature { // OPTIONAL. string keyid = 2; - // *Unauthenticated* PEM encoded X.509 certificate corresponding to the public key. + // *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 9c61e4a..435c05c 100644 --- a/implementation/signing_spec.py +++ b/implementation/signing_spec.py @@ -57,7 +57,7 @@ def keyid(self) -> Optional[str]: ... def certificate(self) -> Optional[str]: - """Returns the certificate of the key, or None if not supported.""" + """Returns the cert chain of the key, or None if not supported.""" class Verifier(Protocol): diff --git a/protocol.md b/protocol.md index 9149bea..a37c0ae 100644 --- a/protocol.md +++ b/protocol.md @@ -53,10 +53,11 @@ CERTIFICATE | 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 for the key - used to sign the message. As with Sign(), details are agreed upon - out-of-band by the signer and verifier. This ensures the necessary information - to verify the signature remains alongside the metadata. +* 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: @@ -102,7 +103,7 @@ To verify: - Optionally, filter acceptable public keys by KEYID. - Verify SIGNATURE against PAE(UTF8(PAYLOAD_TYPE), SERIALIZED_BODY). Reject if the verification fails. -- Optionally, verify the signing key's CERTIFICATE links back to a trusted root. +- Optionally, verify the signing key's CERTIFICATE chains back to a trusted root. - Reject if PAYLOAD_TYPE is not a supported type. - Parse SERIALIZED_BODY according to PAYLOAD_TYPE. Reject if the parsing fails. From 10854373c9de34487634020bb33de83ca334f0f1 Mon Sep 17 00:00:00 2001 From: Asra Ali Date: Tue, 3 May 2022 11:51:27 -0500 Subject: [PATCH 3/3] update Signed-off-by: Asra Ali --- envelope.md | 2 +- implementation/signing_spec.py | 33 ++++++++++++++++----------------- protocol.md | 19 +++++++++++-------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/envelope.md b/envelope.md index af8ed78..48923c3 100644 --- a/envelope.md +++ b/envelope.md @@ -34,7 +34,7 @@ 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](), transforming a DER (binary) encoded X.509 certificate to a base64 encoding with a one-line header and footer. +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 diff --git a/implementation/signing_spec.py b/implementation/signing_spec.py index 435c05c..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 @@ -57,11 +61,13 @@ def keyid(self) -> Optional[str]: ... def certificate(self) -> Optional[str]: - """Returns the cert chain of the key, or None if not supported.""" - + """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`.""" ... @@ -69,16 +75,10 @@ def keyid(self) -> Optional[str]: """Returns the ID of this key, or None if not supported.""" ... -class RootPool(Protocol): - def verify(self, certificate: bytes) -> bool: - """Returns true if `certificate` chains back to the Root pool.""" # Collection of verifiers, each of which is associated with a name. VerifierList = Iterable[Tuple[str, Verifier]] -# A root certificate pool. -Root = RootPool - @dataclasses.dataclass class VerifiedPayload: @@ -101,8 +101,8 @@ 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: @@ -122,7 +122,7 @@ def Sign(payloadType: str, payload: bytes, signer: Signer) -> str: }) -def Verify(json_signature: str, verifiers: VerifierList, root: RootPool) -> VerifiedPayload: +def Verify(json_signature: str, verifiers: VerifierList) -> VerifiedPayload: wrapper = json.loads(json_signature) payloadType = wrapper['payloadType'] payload = b64dec(wrapper['payload']) @@ -132,11 +132,10 @@ def Verify(json_signature: str, verifiers: VerifierList, root: RootPool) -> Veri 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'])) and - root.verify(signature['cert'])): - recognizedSigners.append(name) + if verifier.verify(pae, b64dec(signature['sig']), signature.get('cert')): + recognizedSigners.append(name) if not recognizedSigners: raise ValueError('No valid signature found') return VerifiedPayload(payloadType, payload, recognizedSigners) diff --git a/protocol.md b/protocol.md index a37c0ae..ca77086 100644 --- a/protocol.md +++ b/protocol.md @@ -84,7 +84,7 @@ Functions: Out of band: - Agree on a PAYLOAD_TYPE and cryptographic details, optionally including - KEYID and trusted root certificates. + KEYID and trusted root certificates and constraints. To sign: @@ -92,8 +92,8 @@ 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: @@ -101,9 +101,10 @@ To verify: 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. -- Optionally, verify the signing key's CERTIFICATE chains back to a trusted root. +- 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. @@ -127,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`.