時計を同期するために Roughtime を使う方法はいくつかあります。このレシピでは、Google の Go クライアント ↗ を基にした Cloudflare の Go パッケージ ↗ を使います。
プロトコルは C++ ↗、Rust ↗、Java ↗ でも実装されています。
クライアント設定は、名前付き Roughtime サーバーの一覧で、JSON オブジェクトとして記述します。例:
{
"servers": [
{
"name": "Cloudflare-Roughtime-2",
"publicKeyType": "ed25519",
"publicKey": "0GD7c3yP8xEc4Zl2zeuN2SlLvDVVocjsPSL8/Rl/7zg=",
"addresses": [
{
"protocol": "udp",
"address": "roughtime.cloudflare.com:2003"
}
]
}
]
}各サーバーの ルート公開鍵 が含まれます。サーバー起動時に オンライン の公開鍵 / 秘密鍵ペアを生成します。ルート秘密鍵はオンライン公開鍵の 委任 を作るために使い、オンライン秘密鍵は応答の署名に使います。
委任は、Web 上の従来の X.509 証明書 ↗ と同じ役割です。クライアントはまずルート公開鍵で委任を検証し、次にオンライン公開鍵で応答を検証します。
応答は 監査可能 です。そのため、プロトコルは各クライアントに、正確な時刻を提供する説明責任を持たせます。
設定には、サーバーが使う署名アルゴリズムの種類も含まれます(現在サポートしているのは Ed25519 ↗ のみです)。最後に、サービスへ到達できるアドレスの一覧と、到達に使うトランスポートプロトコル(現在サポートしているのは UDP のみ)が含まれます。
最初の例として、単一の Roughtime サーバーで TLS クライアントまたはサーバーを同期する方法がわかりやすいです。自前の時計と Roughtime サーバーの時計の差を計算します。
最初の手順は、設定ファイルの読み込みです(github.com/cloudflare/roughtime をインポートしてください)。
servers, skipped, err := roughtime.LoadConfig("roughtime.config")この例では、変数 servers は入力ファイルから解析した有効なサーバー設定の一覧です。変数 skipped はスキップしたサーバー数です。たとえば、署名アルゴリズムやトランスポートプロトコルが未対応の場合にスキップされます。
次に、システム時刻を取得し、一覧の最初のサーバーを問い合わせます。
t0 := time.Now()
rt, err := roughtime.Get(&servers[0], attempts, timeout, nil)これでサーバーへリクエストを送り、応答を検証します。変数 rt の型は *roughtime.Roughtime で、問い合わせ結果を表します。入力は次のとおりです。
- サーバーの設定。
- サーバーへ接続する試行回数。
- 各接続試行の待機時間。
- 省略可能な
*roughtime.Roughtime。前回の問い合わせ結果です。
最後のパラメーターを渡すと、リクエストの nonce 生成に使います(詳細は後述します)。
crypto/tls パッケージでは、証明書やセッションチケットなどの検証に使う現在時刻の コールバックを指定 ↗ できます。このコールバックは次のように計算できます。
t1, radius := rt.Now()
delta := t1.Sub(t0.Now())
now := func() time.Time {
return time.Now().Add(delta)
}変数 t1 はサーバーが報告した時刻、radius はサーバーの不確かさ半径です。
完全な動作例は GitHub ↗ を参照してください。
より一般的な使い方として、時計がずれているときに警告するデスクトップ通知を作れます。
Ubuntu GNU/Linux では、次のような処理ができます。
skew := time.Duration(math.Abs(float64(delta)))
if skew > 10*time.Second {
summary := "Check your clock!"
body := fmt.Sprintf("%s says it's off by %v.", servers[0].Name, skew)
cmd := exec.Command("notify-send", "-i", "clock", summary, body)
if err := cmd.Run(); err != nil {
// error handling ...
}
}完全な動作例は GitHub ↗ を参照してください(Ubuntu 18.04 でテスト済み)。このプログラムを cron ジョブとして実行し、時計が同期しているかを定期的に確認します。
Roughtime で複数のソースを使うのは簡単です(強く推奨します)。
t0 := time.Now()
res := roughtime.Do(servers, attempts, timeout, nil)第 1 パラメーターはサーバーの列、残りのパラメーターは roughtime.Get() と同じです。列 servers の各サーバーを順に問い合わせます。出力 res は servers と同じ長さのスライスです。
各要素は、そのサーバーへの問い合わせ結果です。成功した場合、結果にはサーバーの時刻が含まれます。失敗した場合、発生したエラーが含まれます。自前の時計と有効な応答の差の中央値を計算するには:
thresh := 10 * time.Second
delta, err := roughtime.MedianDeltaWithRadiusThresh(res, t0, thresh)不確かさ半径が 10 秒を超える応答は除外します。有効な応答がなければエラーが返ります。
関数 roughtime.Do() は有効な応答をつなぎます。各 nonce は、直前に成功した問い合わせのサーバー応答から生成します。ブログ ↗ で詳しく説明しているとおり、問い合わせをこのように連結すると、問い合わせがこの順で行われたことの暗号的証明になります。結果がこの性質を持つことを検証するには、次のようにします。
chain := roughtime.NewChain(results)
ok, err := chain.Verify(nil)
if err != nil || !ok {
// error handling ...
}変数 chain は、results 内の最初の成功した問い合わせを含む構造です。フィールド chain.Next が、次の成功した問い合わせを指します。Verify() の入力パラメーターで、チェーン検証の起点として前回の結果を使えます。たとえば chain.Verify(nil) が有効なら、chain.Next.Verify(chain.Roughtime) も有効です。
roughtime.Do() は、問い合わせ実行中に有用な情報を出力できます。そのためには roughtime.SetLogger() でロガーを設定します。例:
roughtime.SetLogger(log.New(os.Stdout, "", 0))