1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

rustls で X.509 v1 の証明書を通して Symbol のノードに直接接続する

1
Posted at

ちょっと前に heika の v0.1.0 をリリースした。やったね🎉

heika は、Symbol のノード運営のための、鍵と証明書の Rust のツールキットである。ノードの証明書の発行や更新のほかに、ノードに TLS でつないで、ノードが出している証明書を確かめたり、近くて速いノードを探したりできる。Linux、macOS、Windows のバイナリを Release に置いている。

……はい、というわけで本題。heika を作る中で rustls でノードにつなごうとしたら、証明書が X.509 の v1 だというだけで弾かれた。この記事は、それをどう通したかの話である。

Symbol のノードの TLS

Symbol のノード(catapult)どうしは、次の設定の相互 TLS でつながる。

  • TLS 1.3 だけ
  • 鍵交換は X25519 だけ
  • 証明書の鍵と署名は Ed25519
  • 証明書は、ノードと CA の2枚のチェーン。CA は自己署名で、ノードの証明書は CA の鍵で署名されている

ノードの身元はアカウントの公開鍵で決まる。CA の鍵がメインアカウントの鍵で、ホスト名は関係ない。なので、ブラウザーのような「信頼できるルート CA から、名前の合う証明書がたどれるか」という確かめ方はしない。

そして、この証明書が拡張のない v1 のこともある。catapult は v1 でも受け付けるので、v1 の証明書のままのノードもある。heika も、v3(shoestring と同じ拡張)のほかに v1 の証明書を発行できるようにしている。

rustls が v1 を弾く場所

rustls は証明書を webpki(rustls-webpki)に読ませる。webpki は、証明書の version が v3 でなければ読まない。

rustls-webpki-0.103.15/src/cert.rs
// mozilla::pkix supports v1, v2, v3, and v4, including both the implicit
// (correct) and explicit (incorrect) encoding of v1. We allow only v3.
fn version3(input: &mut untrusted::Reader<'_>) -> Result<(), Error> {

v1 の証明書を渡すと、UnsupportedCertVersion で失敗する。そもそも Symbol のチェーンは WebPKI の確かめ方に合わないので、ServerCertVerifier は自分で書くことになる。ここまでは予想どおりだった。

ハマったのは、webpki が証明書を読む場所がもう1つあることだ。TLS 1.3 のハンドシェイクでは、相手が CertificateVerify で署名を送ってくる。この署名を確かめる rustls の verify_tls13_signature も、中で証明書を webpki に読ませている。

rustls-0.23.45/src/webpki/verify.rs
pub fn verify_tls13_signature(
    msg: &[u8],
    cert: &CertificateDer<'_>,
    dss: &DigitallySignedStruct,
    supported_schemes: &WebPkiSupportedAlgorithms,
) -> Result<HandshakeSignatureValid, Error> {
    // 略
    let cert = webpki::EndEntityCert::try_from(cert).map_err(pki_error)?;

verify_server_cert だけを自分のものにして、verify_tls13_signature は rustls のものに任せると、そこで v1 が弾かれる。

通し方

verify_server_cert と verify_tls13_signature の両方を、webpki を通らないように書く。

証明書は、v1 も読める x509-cert で読む。署名の確かめには、rustls が用意している verify_tls13_signature_with_raw_key を使う。これは証明書ではなく SubjectPublicKeyInfo だけを受け取る関数で、RFC 7250 の Raw Public Key のためのものだ。証明書から SubjectPublicKeyInfo を取り出して渡せば、webpki に証明書を読ませずに済む。

use std::sync::Arc;

use der::{Decode, Encode};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::crypto::{CryptoProvider, verify_tls13_signature_with_raw_key};
use rustls::pki_types::{CertificateDer, ServerName, SubjectPublicKeyInfoDer, UnixTime};
use rustls::{CertificateError, DigitallySignedStruct, Error, SignatureScheme};

#[derive(Debug)]
struct Verifier {
    provider: Arc<CryptoProvider>,
}

impl ServerCertVerifier for Verifier {
    fn verify_server_cert(
        &self,
        end_entity: &CertificateDer<'_>,
        intermediates: &[CertificateDer<'_>],
        _server_name: &ServerName<'_>,
        _ocsp_response: &[u8],
        now: UnixTime,
    ) -> Result<ServerCertVerified, Error> {
        // ノード、CA の2枚を x509-cert で読み、
        // CA が自己署名で、ノードが CA の鍵で署名されていて、どちらも有効期間内かを確かめる
        todo!()
    }

    fn verify_tls12_signature(
        &self,
        _message: &[u8],
        _cert: &CertificateDer<'_>,
        _dss: &DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, Error> {
        Err(Error::General("TLS 1.2 is not supported".into()))
    }

    fn verify_tls13_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, Error> {
        let spki = spki(cert)?;
        verify_tls13_signature_with_raw_key(
            message,
            &SubjectPublicKeyInfoDer::from(&spki[..]),
            dss,
            &self.provider.signature_verification_algorithms,
        )
    }

    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
        vec![SignatureScheme::ED25519]
    }
}

/// 証明書の SubjectPublicKeyInfo の DER。x509-cert は v1 も読める
fn spki(cert: &CertificateDer<'_>) -> Result<Vec<u8>, Error> {
    let cert = x509_cert::Certificate::from_der(cert)
        .map_err(|_| Error::from(CertificateError::BadEncoding))?;
    cert.tbs_certificate()
        .subject_public_key_info()
        .to_der()
        .map_err(|_| CertificateError::BadEncoding.into())
}

設定は、TLS 1.3 と X25519 だけに絞ってから、この Verifier を渡す。

let mut provider = rustls::crypto::ring::default_provider();
// catapult は鍵交換に X25519 しか使わない
provider.kx_groups = vec![rustls::crypto::ring::kx_group::X25519];
let provider = Arc::new(provider);

let config = rustls::ClientConfig::builder_with_provider(provider.clone())
    .with_protocol_versions(&[&rustls::version::TLS13])?
    .dangerous()
    .with_custom_certificate_verifier(Arc::new(Verifier { provider }))
    .with_client_cert_resolver(resolver);

dangerous() という名前のとおり、ここから先は自分で確かめた分しか確かめられない。heika では、catapult がつなぐときと同じ条件を確かめている。

  • 2枚のチェーンであること
  • CA が自己署名であること
  • ノードの証明書の issuer が CA の subject と同じで、CA の鍵で署名されていること
  • どちらの証明書も有効期間内であること

ホスト名は確かめない。catapult も見ないので、SNI には適当な名前を入れている。

こちらが v1 の証明書で名乗るとき

相互 TLS なので、こちらも証明書で名乗る。こちらの証明書は rustls が読まずにそのまま送るので、v1 でも何もしなくてよい。CertifiedKey に DER と秘密鍵を入れて、with_client_cert_resolver に渡すだけである。

テストのために rustls でサーバーを立てるなら、ClientCertVerifier の側にも同じことが要る。verify_tls13_signature を、同じように verify_tls13_signature_with_raw_key で書く。

まとめ

  • webpki は v3 の証明書しか読まない
  • rustls で v1 を通すには、ServerCertVerifier の verify_server_cert と verify_tls13_signature の両方を、webpki を通らないように書く
  • 署名の確かめは、証明書から取り出した SubjectPublicKeyInfo を verify_tls13_signature_with_raw_key に渡せばよい
  • こちらが名乗る証明書は、v1 でもそのまま使える

heika: https://github.com/HinaBlogChannel/heika

heika cert show --connect <ホスト>:<ポート> --client-cert node.full.crt.pem < node.key.pem

で、ノードが出している証明書と、こちらの証明書が受け付けられたかを確かめられる。

リリースしたその日に Twitter のアカウントが凍結されて、heika のことをつぶやけなくなった。ぴえん。なので、ここに書いている。Twitter は凍結されても、Claude Code の契約だけは凍結しないでほしい。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?