-
Notifications
You must be signed in to change notification settings - Fork 779
Modify TLS parsing rules for Durango #2458
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
Conversation
namedCurve := elliptic.P256() | ||
x, y := elliptic.Unmarshal(namedCurve, der) |
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.
TODO: Should we use a different parsing function? This aligns with the x509 package, but is deprecated.
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.
The canonical entry point is x509.ParsePKIXPublicKey but you are correctly reimplementing that. I think this might be a rare enough case in which elliptic.Unmarshal is appropriate. Its behavior is very well specified, anyway, so you don't need to worry about it changing.
if err := ValidateCertificate(cert); err != nil { | ||
return err | ||
} | ||
|
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.
This is now done in certificate parsing (pre-durango) or not at all (post-durango).
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.
ValidateCertificate
is still called in connToIDAndCert
post-durango, no? Or are you referring to something else?
func connToIDAndCert(conn *tls.Conn, invalidCerts prometheus.Counter, durangoTime time.Time) (ids.NodeID, net.Conn, *staking.Certificate, error) {
if err := conn.Handshake(); err != nil {
return ids.EmptyNodeID, nil, nil, err
}
state := conn.ConnectionState()
if len(state.PeerCertificates) == 0 {
return ids.EmptyNodeID, nil, nil, errNoCert
}
tlsCert := state.PeerCertificates[0]
// Invariant: ParseCertificate is used rather than CertificateFromX509 to
// ensure that signature verification can assume the certificate was
// parseable according the staking package's parser.
//
// TODO: Remove pre-Durango parsing after v1.11.x has activated.
var (
peerCert *staking.Certificate
err error
)
if time.Now().Before(durangoTime) {
peerCert, err = staking.ParseCertificate(tlsCert.Raw)
} else {
peerCert, err = staking.ParseCertificatePermissive(tlsCert.Raw)
}
if err != nil {
invalidCerts.Inc()
return ids.EmptyNodeID, nil, nil, err
}
// We validate the certificate here to attempt to make the validity of the
// peer certificate as clear as possible. Specifically, a node running a
// prior version using an invalid certificate should not be able to report
// healthy.
if err := staking.ValidateCertificate(peerCert); err != nil {
invalidCerts.Inc()
return ids.EmptyNodeID, nil, nil, err
}
nodeID := ids.NodeIDFromCert(peerCert)
return nodeID, conn, peerCert, nil
}
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.
I think it also still happens here:
Lines 112 to 114 in 7623ffd
if err := staking.ValidateCertificate(stakingCert); err != nil { | |
return nil, fmt.Errorf("invalid staking certificate: %w", err) | |
} |
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.
Good catch. We expect any certificates returned from a Parse
function to have been validated by that parse function... So I removed the call from connToIDAndCert
.
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.
As for the call in node
- I'm not exactly sure how we want to handle that in the future yet... We'll probably just re-parse it or something.
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.
I think Parse sounds right
staking/parse.go
Outdated
} | ||
|
||
// Ref: https://github.com/golang/go/blob/go1.19.12/src/crypto/x509/parser.go#L215-L306 | ||
func parsePublicKey(keyData *publicKeyInfo) (any, x509.SignatureAlgorithm, error) { |
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.
After Durango activates, we'll remove the SignatureAlgorithm
from the cert - and this function.
|
||
input := cryptobyte.String(bytes) | ||
// Consume the length and tag bytes. | ||
if !input.ReadASN1(&input, cryptobyte_asn1.SEQUENCE) { |
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.
Technically - there is no reason to enforce the tag
s here. Should we switch to using ReadAnyASN1
? It might be nice to keep the tags just to make it more clear what we are reading/skipping. Although it would be faster to just ignore the tag.
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.
Behind the scenes, this just calls ReadAnyASN1
and compares the tag afterwards. If speed is a factor in the decision, I don't think that switch would actually make much of an impact: https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.16.0:cryptobyte/asn1.go;l=594-605
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.
As long as it doesn't change in the future, it doesn't matter really. Aesthetically, I think I like leaving the tags in, as it makes the parser more precise about what it is it's extracting, even if it doesn't check the contents of what it's ignoring.
@@ -276,6 +276,12 @@ func NewNetwork( | |||
IPSigner: peer.NewIPSigner(config.MyIPPort, config.TLSKey), | |||
} | |||
|
|||
// Invariant: We delay the activation of durango during the TLS handshake to |
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.
Nice catch
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.
Changes under staking/ LGTM.
|
||
input := cryptobyte.String(bytes) | ||
// Consume the length and tag bytes. | ||
if !input.ReadASN1(&input, cryptobyte_asn1.SEQUENCE) { |
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.
As long as it doesn't change in the future, it doesn't matter really. Aesthetically, I think I like leaving the tags in, as it makes the parser more precise about what it is it's extracting, even if it doesn't check the contents of what it's ignoring.
} | ||
return pub, x509.SHA256WithRSA, nil | ||
case oid.Equal(oidPublicKeyECDSA): | ||
namedCurve := elliptic.P256() |
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.
I went back and forth on whether to recommend checking the AI parameters to make sure they say P-256, but ultimately there is no benefit in the "certificate is just a complex encoding for keys" model, so this LGTM.
namedCurve := elliptic.P256() | ||
x, y := elliptic.Unmarshal(namedCurve, der) |
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.
The canonical entry point is x509.ParsePKIXPublicKey but you are correctly reimplementing that. I think this might be a rare enough case in which elliptic.Unmarshal is appropriate. Its behavior is very well specified, anyway, so you don't need to worry about it changing.
} | ||
if pub.E <= 0 { | ||
return nil, 0, ErrRSAPublicExponentNotPositive | ||
} |
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.
I would restrict N and E further. I can't promise crypto/rsa will always accept E=1, or 256-bit keys, for example
Ideally, if your in-use certificates allow, you'd restrict N's bitsize to two or three options, and E to two or three values. If you need to be more flexible than that, I would check at least 3 <= E <= MaxInt32, and 2048 <= N.BitLen <= 8192.
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.
Oh, and also I'd check that both N and E are odd. (You can do that with N.Bit(0)
)
N needs to be a product of two large primes, and E needs to be coprime with phi(N) which is even. Libraries can and do reject even values because some math tricks don't work on them.
This PR has become stale because it has been open for 30 days with no activity. Adding the |
Why this should be merged
This PR removes the reliance on the
crypto/x509
package for parsing TLS certs used outside of TLS connections.The result is faster (as less checks are performed) and more obviously correct (as we no longer need to perform additional verification outside of parsing).
How this works
x509.ParseCertificate
and removes unnecessary checks.How this was tested