Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for x509 certificates in DSSE #50

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions envelope.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ the following form, called the "JSON envelope":
"payloadType": "<PAYLOAD_TYPE>",
"signatures": [{
"keyid": "<KEYID>",
"sig": "<Base64(SIGNATURE)>"
"sig": "<Base64(SIGNATURE)>",
"cert": "<PEM(CERTIFICATE_CHAIN)>"
}]
}
```
Expand All @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits:

  • This is a certificate chain, so it is a series of PEM-encoded certificates, concatenated with newlines?
  • wrap at 80 columns for consistency with rest of file


### Multiple signatures

An envelope MAY have more than one signature, which is equivalent to separate
Expand All @@ -44,10 +47,12 @@ envelopes with individual signatures.
"payloadType": "<PAYLOAD_TYPE>",
"signatures": [{
"keyid": "<KEYID_1>",
"sig": "<SIG_1>"
"sig": "<SIG_1>",
"cert": "<CERT_1>"
}, {
"keyid": "<KEYID_2>",
"sig": "<SIG_2>"
"sig": "<SIG_2>",
"cert": "<CERT_2>"
}]
}
```
Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions envelope.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
24 changes: 18 additions & 6 deletions implementation/signing_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`."""
...

Expand Down Expand Up @@ -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,
Expand All @@ -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')
Expand Down
31 changes: 21 additions & 10 deletions protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should we also allow ASN.1 encoding in the protocol? I can see why we'd want to specify PEM in the JSON envelope because that would need base64 encoding otherwise, but a different envelope format (say CBOR or protobuf) might want a binary format encoding of the certificate.

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
Expand All @@ -77,25 +84,27 @@ Functions:
Out of band:

- Agree on a PAYLOAD_TYPE and cryptographic details, optionally including
KEYID.
KEYID and trusted root certificates and constraints.

To sign:

- Serialize the message according to PAYLOAD_TYPE. Call the result
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).

asraa marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: remove "constraints"

same on line 132 below

CERTIFICATE is specified, it MUST be verified against a trusted root
certificate. Reject if the verification fails.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"trusted root certificate and path validation" or similar

same on line 134 below

- Reject if PAYLOAD_TYPE is not a supported type.
- Parse SERIALIZED_BODY according to PAYLOAD_TYPE. Reject if the parsing
fails.
asraa marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ACCEPTED_KEYS is no longer correct. What should this be instead?

- Break if the cardinality of ACCEPTED_KEYS is greater or equal to `t`.
- Reject if the cardinality of ACCEPTED_KEYS is less than `t`.
Expand Down