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

webtransport: deterministically generate certificates, to survive host reboots #1824

Closed
Tracked by #1827
marten-seemann opened this issue Oct 12, 2022 · 6 comments · Fixed by #1833
Closed
Tracked by #1827
Assignees

Comments

@marten-seemann
Copy link
Contributor

Copied from marten-seemann/go-libp2p-webtransport#2. I can't move that issue since it's not in the libp2p org.


We can generate a (cryptographically secure) (pseudo-)random value from the host's private key as an input to our certificate generation mechanism.
Furthermore, we can set the validity period of the certificate such that it is independent from the boot time of the node.

This would allow us to generate certificates with deterministic (but still unpredictable) hashes, which means that our multiaddr (which includes the certificate hash) would survive reboots of the node, even if all state (except for the private key) is lost.

@marten-seemann
Copy link
Contributor Author

We can generate a (cryptographically secure) (pseudo-)random value from the host's private key as an input to our certificate generation mechanism.

Adding a bit more context here: We already derive a key from the host key for quic-go's stateless reset key in

keyReader := hkdf.New(sha256.New, keyBytes, nil, []byte(statelessResetKeyInfo))
qconfig.StatelessResetKey = make([]byte, 32)
if _, err := io.ReadFull(keyReader, qconfig.StatelessResetKey); err != nil {

We should probably use the same construction here (with a different salt / info)

@MarcoPolo
Copy link
Contributor

MarcoPolo commented Oct 14, 2022

https://pkg.go.dev/crypto/ecdsa

Signatures generated by this package are not deterministic, but entropy is mixed with the private key and the message, achieving the same level of security in case of randomness source failure.

I wish I saw that sooner.

We're using ECDSA secp256 for the cert key and signature since it's the interopable default.

The exact list of allowed public key algorithms used in the Subject Public Key Info field (and, as a consequence, in the TLS CertificateVerify message) is implementation-defined; however, it MUST include ECDSA with the secp256r1 (NIST P-256) named group ([RFC3279], Section 2.3.5; [RFC8422]) to provide an interoperable default. It MUST NOT contain RSA keys ([RFC3279], Section 2.3.1).

(from https://www.w3.org/TR/webtransport/)

We could consider other key algorithms, but it depends on browser support at that point.

@marten-seemann
Copy link
Contributor Author

marten-seemann commented Oct 15, 2022

Good catch! That's annoying, and I wish there was a way to turn this off.

Looking at the implementation, there might be a way to hack around this though. The randomization works by reading a single byte from the supplied io.Reader with 50% probability: https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/crypto/internal/randutil/randutil.go;drc=9ddb8ea73724d717a9bbf44be7d585ba5587504f;l=25

That means for every deterministic io.Reader, there are exactly 2 certificates that can possibly be generated. We could generate both of them, and then deterministically choose one of them (e.g. by choosing the one with the smaller cert hash).

Of course, this is hacky, and it relies on ecdsa.MaybeReadByte to not change in future Go versions. In principle, the Go team could implement this function such that the number of possible outcomes gets so large that we can't brute-force our way through it any more.

@MarcoPolo
Copy link
Contributor

Another hack is to wrap it with a reader that detects if the first read is a single byte and doesn’t advance the underlying reader.

@marten-seemann
Copy link
Contributor Author

That’s better! No need to loop and do expensive crypto.

@MarcoPolo
Copy link
Contributor

we can even keep two streams of seeded pseudo-randomness and then use one stream for the single byte reads and the other for the rest of the reads. Just in case the single byte read is ever important.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

2 participants