fix(gamestream): accept legacy X.509 v2 Moonlight client certs in mTLS pairing
rustls-webpki 0.103 accepts only X.509 v3 certs and returns `UnsupportedCertVersion` for anything older, aborting the mTLS handshake at the `CertificateVerify` step before it ever checks the signature. The moonlight-embedded client family (moonlight-embedded, aurora-tv, …) still mint self-signed **v2** certs with no keyUsage extension, so pairing fails against punktfunk while it works against Sunshine — whose OpenSSL verify callback never inspects the cert version. (aurora-tv PR #53 worked around this client-side by switching to v3 + keyUsage; this fixes it host-side for stock clients too.) The cert's X.509 version and extensions carry no security weight here: the client cert is self-signed and pinned by SHA-256 (`peer_is_paired`), and the PIN pairing ceremony is the real proof of identity. The property that DOES matter — that the peer holds the private key for the cert it presented — is still enforced: when webpki rejects the cert we re-run exactly that `CertificateVerify` check with a version-agnostic parser (x509-parser SPKI + the same `rsa` verify `pairing::verify256` uses). This is a full cryptographic check, never a bypass: a bad signature still fails, and non-RSA / unsupported schemes fall through to webpki's original error. Matches Sunshine's leniency without loosening the pinned trust model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -139,6 +139,7 @@ impl ClientCertVerifier for AcceptAnyClientCert {
|
|||||||
dss,
|
dss,
|
||||||
&self.provider.signature_verification_algorithms,
|
&self.provider.signature_verification_algorithms,
|
||||||
)
|
)
|
||||||
|
.or_else(|e| accept_legacy_moonlight_cert(message, cert, dss, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_tls13_signature(
|
fn verify_tls13_signature(
|
||||||
@@ -153,6 +154,7 @@ impl ClientCertVerifier for AcceptAnyClientCert {
|
|||||||
dss,
|
dss,
|
||||||
&self.provider.signature_verification_algorithms,
|
&self.provider.signature_verification_algorithms,
|
||||||
)
|
)
|
||||||
|
.or_else(|e| accept_legacy_moonlight_cert(message, cert, dss, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||||
@@ -162,6 +164,107 @@ impl ClientCertVerifier for AcceptAnyClientCert {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fallback client-cert `CertificateVerify` check that tolerates pre-v3 (X.509 v2) certificates,
|
||||||
|
/// invoked only when rustls-webpki's strict path has already rejected the cert.
|
||||||
|
///
|
||||||
|
/// **The compatibility gap.** rustls-webpki (0.103) accepts *only* X.509 v3 certs and returns
|
||||||
|
/// `UnsupportedCertVersion` for anything older — aborting the mTLS handshake before it ever looks at
|
||||||
|
/// the signature. The moonlight-embedded client family (moonlight-embedded, aurora-tv, …) still mint
|
||||||
|
/// self-signed **v2** certs with no `keyUsage` extension, so pairing fails against us while it works
|
||||||
|
/// against Sunshine, whose OpenSSL verify callback never inspects the cert version. The decisive
|
||||||
|
/// factor is the cert *version*, not the missing `keyUsage` — a v3 cert with no extensions passes
|
||||||
|
/// webpki fine.
|
||||||
|
///
|
||||||
|
/// **Why relaxing it does not weaken security.** The cert's X.509 version and extensions carry no
|
||||||
|
/// security weight in GameStream: the client cert is self-signed and later pinned by SHA-256
|
||||||
|
/// (`nvhttp::peer_is_paired`), and the PIN pairing ceremony is the real proof of identity. The one
|
||||||
|
/// property that *does* matter — and that we still enforce here — is that the peer holds the private
|
||||||
|
/// key for the cert it presented (the TLS `CertificateVerify` signature), because the cert itself is
|
||||||
|
/// exchanged in the clear over the plain-HTTP pairing port and is not a secret. So we re-run
|
||||||
|
/// *exactly that* check with a version-agnostic parser: pull the RSA public key from the lenient
|
||||||
|
/// x509-parser and verify the handshake signature ourselves (the same primitive
|
||||||
|
/// `pairing::verify256` uses). This is a full cryptographic verification, never a bypass — a bad
|
||||||
|
/// signature still fails, and any non-RSA / unsupported scheme falls through to webpki's original
|
||||||
|
/// error `webpki_err`. Moonlight/Sunshine client certs are RSA-2048, so this matches Sunshine's
|
||||||
|
/// leniency without loosening the pinned trust model.
|
||||||
|
fn accept_legacy_moonlight_cert(
|
||||||
|
message: &[u8],
|
||||||
|
cert: &CertificateDer,
|
||||||
|
dss: &DigitallySignedStruct,
|
||||||
|
webpki_err: rustls::Error,
|
||||||
|
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||||
|
use rsa::pkcs8::DecodePublicKey;
|
||||||
|
use rsa::signature::Verifier;
|
||||||
|
use rsa::{pkcs1v15, pss, RsaPublicKey};
|
||||||
|
use sha2::{Sha256, Sha384, Sha512};
|
||||||
|
|
||||||
|
let Ok((_, x509)) = x509_parser::parse_x509_certificate(cert.as_ref()) else {
|
||||||
|
return Err(webpki_err);
|
||||||
|
};
|
||||||
|
let Ok(key) = RsaPublicKey::from_public_key_der(x509.public_key().raw) else {
|
||||||
|
return Err(webpki_err); // not an RSA cert — leave webpki's verdict in place
|
||||||
|
};
|
||||||
|
let sig = dss.signature();
|
||||||
|
|
||||||
|
// `VerifyingKey`/`Signature` hash `message` internally with the scheme's digest; each arm moves
|
||||||
|
// `key`, but exactly one arm runs. Covers the RSA PKCS#1v1.5 and RSA-PSS schemes a Moonlight
|
||||||
|
// client offers in `CertificateVerify` (SHA-256/384/512).
|
||||||
|
let ok = match dss.scheme {
|
||||||
|
SignatureScheme::RSA_PKCS1_SHA256 => pkcs1v15::Signature::try_from(sig)
|
||||||
|
.map(|s| {
|
||||||
|
pkcs1v15::VerifyingKey::<Sha256>::new(key)
|
||||||
|
.verify(message, &s)
|
||||||
|
.is_ok()
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
SignatureScheme::RSA_PKCS1_SHA384 => pkcs1v15::Signature::try_from(sig)
|
||||||
|
.map(|s| {
|
||||||
|
pkcs1v15::VerifyingKey::<Sha384>::new(key)
|
||||||
|
.verify(message, &s)
|
||||||
|
.is_ok()
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
SignatureScheme::RSA_PKCS1_SHA512 => pkcs1v15::Signature::try_from(sig)
|
||||||
|
.map(|s| {
|
||||||
|
pkcs1v15::VerifyingKey::<Sha512>::new(key)
|
||||||
|
.verify(message, &s)
|
||||||
|
.is_ok()
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
SignatureScheme::RSA_PSS_SHA256 => pss::Signature::try_from(sig)
|
||||||
|
.map(|s| {
|
||||||
|
pss::VerifyingKey::<Sha256>::new(key)
|
||||||
|
.verify(message, &s)
|
||||||
|
.is_ok()
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
SignatureScheme::RSA_PSS_SHA384 => pss::Signature::try_from(sig)
|
||||||
|
.map(|s| {
|
||||||
|
pss::VerifyingKey::<Sha384>::new(key)
|
||||||
|
.verify(message, &s)
|
||||||
|
.is_ok()
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
SignatureScheme::RSA_PSS_SHA512 => pss::Signature::try_from(sig)
|
||||||
|
.map(|s| {
|
||||||
|
pss::VerifyingKey::<Sha512>::new(key)
|
||||||
|
.verify(message, &s)
|
||||||
|
.is_ok()
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
_ => return Err(webpki_err),
|
||||||
|
};
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
tracing::debug!(
|
||||||
|
"accepted a legacy (pre-v3) Moonlight client cert via version-agnostic RSA verify"
|
||||||
|
);
|
||||||
|
Ok(HandshakeSignatureValid::assertion())
|
||||||
|
} else {
|
||||||
|
Err(webpki_err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a mutual-TLS `ServerConfig` presenting the host cert/key. nvhttp/pairing path: the
|
/// Build a mutual-TLS `ServerConfig` presenting the host cert/key. nvhttp/pairing path: the
|
||||||
/// client cert is **mandatory**.
|
/// client cert is **mandatory**.
|
||||||
pub fn server_config(cert_pem: &str, key_pem: &str) -> Result<Arc<ServerConfig>> {
|
pub fn server_config(cert_pem: &str, key_pem: &str) -> Result<Arc<ServerConfig>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user