有多种方式可以使用 Roughtime 来保持您的时钟同步。这些示例配方使用了 Cloudflare 的 Go 包 ↗,该包基于 Google 的 Go 客户端 ↗。
该协议也在 C++ ↗、Rust ↗ 和 Java ↗ 中得到了实现。
客户端配置由一个格式化为 JSON 对象的命名 Roughtime 服务器列表组成。例如:
{
"servers": [
{
"name": "Cloudflare-Roughtime-2",
"publicKeyType": "ed25519",
"publicKey": "0GD7c3yP8xEc4Zl2zeuN2SlLvDVVocjsPSL8/Rl/7zg=",
"addresses": [
{
"protocol": "udp",
"address": "roughtime.cloudflare.com:2003"
}
]
}
]
}它包含每个服务器的 根公钥 (root public key)。当服务器启动时,它会生成一个 在线 (online) 公钥/私钥对。根私钥用于为在线公钥创建 委托 (delegation),而在线私钥用于对响应进行签名。
委托在 Web 上起着与传统 X.509 证书 ↗ 相同的作用。客户端首先使用根公钥来验证委托,然后使用在线公钥来验证响应。
因为响应是 可审计的 (auditable),该协议使得每个客户端都能够对提供准确时间负责。
配置还编码了服务器使用的签名算法类型(目前仅支持 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 ↗。
使用 Roughtime 的一种更通用的方法是创建桌面警报,在您的时钟发生偏差时警告您。
在 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 {
// 错误处理...
}
}有关完整的运行示例,请查看我们的 GitHub ↗(已在 Ubuntu 18.04 上进行测试)。您可以将此程序作为 cron 作业运行,以定期检查您的时钟是否已同步。
对 Roughtime 使用多个来源非常容易(且强烈推荐):
t0 := time.Now()
res := roughtime.Do(servers, attempts, timeout, nil)第一个参数是一序列的服务器,其余参数与 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 {
// 错误处理...
}变量 chain 是一个结构体,其中包含 results 中第一次成功的查询。它有一个字段 chain.Next 指向下一个成功的查询。Verify() 的输入参数允许您将先前的结果作为起点来验证该链。例如,如果 chain.Verify(nil) 有效,那么 chain.Next.Verify(chain.Roughtime) 也将有效。
可以让 roughtime.Do() 在执行查询时输出有用的信息。为此,请调用 roughtime.SetLogger() 来设置日志记录器。例如:
roughtime.SetLogger(log.New(os.Stdout, "", 0))