-
Notifications
You must be signed in to change notification settings - Fork 18
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
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this should be part of the interface. Instead:
|
||
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,17 +109,20 @@ 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, | ||
'signatures': [signature], | ||
}) | ||
|
||
|
||
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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
asraa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I worry that this small amount of detail will lead to insecure implementations, in particular just verifying that it came from a trusted root but not verifying the actual chain properties (e.g. common name). Previously, "cryptographic details" implied roots of trust because we said nothing about the public key. Now root certs are called out explicitly, but without sufficient detail. I feel like we need to either say less (so that it's obvious that there is missing detail) or more (so that it's clear how to implement correctly.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmmm I had to think about this. I would definitely like to call this out explicitly because leaving it out would lead to people potentially not verifying the chain and using the public key inside to verify the sig. I agree that this does not call out details on path validation: I could link https://www.rfc-editor.org/rfc/rfc5280.html#section-6, and rephrase to say There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure RFC5280 is sufficient. That RFC is super complex, and it sounds like TLS doesn't even use it directly. It is more strict and requires a SEQUENCE of certificates that leads directly from the root CA to the server's certificate (or the other way around?) and it does something with the subject name to match against the domain name. I feel like we need to call that out explicitly, or else it will get dropped. Alternatively, is there some implementation we can point to to make things more concrete? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are also caveats around Common Name vs Subject Alternative Name. I think the former used to be the one to verify, but now it's the latter in TLS There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I just had to deal with this myself, and ended up using https://github.com/wbond/certvalidator which does complete path validation back to trusted roots |
||
|
||
To sign: | ||
|
||
|
@@ -90,12 +96,13 @@ To sign: | |
|
||
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. | ||
- 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. | ||
asraa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to fix the link to PEM encoding.