├── .github └── workflows │ ├── build_and_test.yml │ ├── cifuzz.yml │ └── semgrep.yml ├── FUZZING.md ├── LICENSE ├── README.md ├── client └── client.go ├── cmd ├── getroughtime │ └── main.go ├── keygen │ └── keygen.go └── testserver │ └── main.go ├── config └── config.go ├── ecosystem.json ├── ecosystem.json.go ├── ecosystem.md ├── go.mod ├── go.sum ├── internal └── ecosystem_json_go_builder │ ├── main.go │ └── main_test.go ├── protocol ├── error.go ├── internal │ ├── cmd │ │ └── gen_test_vectors.go │ └── testing │ │ └── testing.go ├── protocol.go ├── protocol_test.go ├── testdata │ ├── roughtime_google_001.json │ ├── roughtime_google_010.json │ ├── roughtime_google_100.json │ ├── roughtime_ietf_draft08_001.json │ ├── roughtime_ietf_draft08_010.json │ ├── roughtime_ietf_draft08_100.json │ ├── roughtime_ietf_draft11_001.json │ ├── roughtime_ietf_draft11_010.json │ └── roughtime_ietf_draft11_100.json └── version.go ├── recipes ├── alerter.go ├── testdata │ └── ca.pem └── tls.go └── roughtime.go /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - synchronize 11 | - ready_for_review 12 | branches: 13 | - master 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | platform: [ubuntu-latest, macos-latest] 19 | name: Build 20 | runs-on: ${{ matrix.platform }} 21 | steps: 22 | - name: Set up Go 1.x 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: ^1.21 26 | - name: Check out code 27 | uses: actions/checkout@v4 28 | - name: Build 29 | run: go build -v ./... 30 | test: 31 | strategy: 32 | matrix: 33 | platform: [ubuntu-latest, macos-latest] 34 | name: Test 35 | runs-on: ${{ matrix.platform }} 36 | needs: 37 | - build 38 | steps: 39 | - name: Set up Go 1.x 40 | uses: actions/setup-go@v5 41 | with: 42 | go-version: ^1.21 43 | - name: Check out code 44 | uses: actions/checkout@v4 45 | - name: Test 46 | run: go test -v ./... 47 | race: 48 | strategy: 49 | matrix: 50 | platform: [ubuntu-latest, macos-latest] 51 | name: Race condition test 52 | runs-on: ${{ matrix.platform }} 53 | needs: 54 | - build 55 | - test 56 | steps: 57 | - name: Set up Go 1.x 58 | uses: actions/setup-go@v5 59 | with: 60 | go-version: ^1.21 61 | - name: Check out code 62 | uses: actions/checkout@v4 63 | - name: Test race conditions 64 | run: go test -v -race ./... 65 | coverage: 66 | strategy: 67 | matrix: 68 | platform: [ubuntu-latest, macos-latest] 69 | name: Test coverage 70 | runs-on: ${{ matrix.platform }} 71 | needs: 72 | - build 73 | - test 74 | steps: 75 | - name: Set up Go 1.x 76 | uses: actions/setup-go@v5 77 | with: 78 | go-version: ^1.21 79 | - name: Check out code 80 | uses: actions/checkout@v4 81 | - name: Test coverage 82 | run: go test -v -cover -covermode=atomic -coverpkg=./... -coverprofile=coverage.txt ./... 83 | # TODO: upload coverage.txt to codecov.io or similar 84 | lint: 85 | name: Golangci Lint 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: Set up Go 1.x 89 | uses: actions/setup-go@v5 90 | with: 91 | go-version: ^1.21 92 | - name: Check out code 93 | uses: actions/checkout@v4 94 | - name: Run golangci-lint 95 | uses: golangci/golangci-lint-action@v6.1.1 96 | with: 97 | version: v1.62.0 98 | install-mode: goinstall 99 | -------------------------------------------------------------------------------- /.github/workflows/cifuzz.yml: -------------------------------------------------------------------------------- 1 | name: CIFuzz 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | permissions: {} 10 | jobs: 11 | Fuzzing: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | security-events: write 15 | steps: 16 | - name: Build Fuzzers 17 | id: build 18 | uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master 19 | with: 20 | oss-fuzz-project-name: 'roughtime' 21 | language: go 22 | - name: Run Fuzzers 23 | uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master 24 | with: 25 | oss-fuzz-project-name: 'roughtime' 26 | language: go 27 | fuzz-seconds: 300 28 | output-sarif: true 29 | - name: Upload Crash 30 | uses: actions/upload-artifact@v4 31 | if: failure() && steps.build.outcome == 'success' 32 | with: 33 | name: artifacts 34 | path: ./out/artifacts 35 | - name: Upload Sarif 36 | if: always() && steps.build.outcome == 'success' 37 | uses: github/codeql-action/upload-sarif@v3 38 | with: 39 | # Path to SARIF file relative to the root of the repository 40 | sarif_file: cifuzz-sarif/results.sarif 41 | checkout_path: cifuzz-sarif 42 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | - master 8 | schedule: 9 | - cron: '0 0 * * *' 10 | name: Semgrep config 11 | jobs: 12 | semgrep: 13 | name: semgrep/ci 14 | runs-on: ubuntu-latest 15 | env: 16 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 17 | SEMGREP_URL: https://cloudflare.semgrep.dev 18 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 19 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 20 | container: 21 | image: semgrep/semgrep 22 | steps: 23 | - uses: actions/checkout@v4 24 | - run: semgrep ci 25 | -------------------------------------------------------------------------------- /FUZZING.md: -------------------------------------------------------------------------------- 1 | # Running go-fuzz tests 2 | 3 | ```sh 4 | go test -fuzz=FuzzParseRequest ./protocol 5 | go test -fuzz=FuzzVerifyReply ./protocol 6 | ``` 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Roughtime 2 | 3 | A fork of [Google's Roughtime 4 | protocol](https://roughtime.googlesource.com/roughtime/) and utilities for 5 | building Roughtime clients. This repository also implements the [IETF 6 | version](https://datatracker.ietf.org/doc/draft-ietf-ntp-roughtime/). 7 | 8 | For more information about Roughtime and tips for writing your own 9 | client or server, visit the [developer 10 | documentation](https://developers.cloudflare.com/time-services/roughtime/). 11 | 12 | ## Note on status 13 | 14 | This repository currently supports draft-ietf-ntp-roughtime-11 and 15 | draft-ietf-ntp-roughtime-08. Backwards compatibility with this version is not 16 | guaranteed; users should expect breaking changes as the IETF process continues. 17 | Likewise, the API should be regarded as unstable. 18 | 19 | If you want to use this code and the protocol please join the NTP WG 20 | [mailing list](https://www.ietf.org/mailman/listinfo/ntp) so that you are 21 | aware of the evolution of the protocol and issues that others discover. 22 | 23 | ** DO NOT USE IN PRODUCTION SOFTWARE ** 24 | 25 | ## Ecosystem guidelines 26 | 27 | We welcome pull requests for adding your Roughtime service to our list. Your PR 28 | should do the following: 29 | 30 | * Add your server's configuration to `ecosystem.json`. The list of servers 31 | will be alphabetized by the `"name"` field. 32 | 33 | * Add some information about your service to `ecosystem.md`. (This is also 34 | kept in alphabetical order.) This should include details about how your 35 | service is provisioned: 36 | 37 | 1. how you synchronize your server's clock; 38 | 2. if your code is open source, a link to the code; 39 | 3. where in the world your server is located; and 40 | 4. whether you will guarantee up time, and if so, how you will do so. 41 | 5. what version you run 42 | 43 | * Generate the `ecosystem.json.go` from the `ecosystem.json`. Use the 44 | `go generate` command for this. 45 | 46 | A couple things to keep in mind: 47 | 48 | * To be healthy, the Roughtime ecosystem **needs a diverse set of time 49 | sources.** The list already contains servers that are synced with Google's 50 | NTP servers; as such, servers that expose new sources will be preferred. (An 51 | atomic clock would be cool!) 52 | 53 | * We reserve the right to prune this list at any time. (For example, if a 54 | server is unreliable, or its root secret key has been compromised.) 55 | 56 | * As new versions come out we may prune servers that do not update. 57 | 58 | Finally, a disclaimer: the ecosystem is growing, and ours might not be the 59 | definitive list of who is serving Roughtime at any given time. 60 | 61 | ## Contributing 62 | 63 | We welcome your bug fixes, issues, and improvements to either the 64 | protocol or this code. Note that substantive changes to the protocol 65 | need to be discussed on the NTP WG mailing list. 66 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Roughtime Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Modifications copyright 2018 Cloudflare, Inc. 16 | // 17 | // The code has been simplified in order to suit our requirements. 18 | 19 | // This package defines some functionalities useful for building Roughtime 20 | // clients. It's based on Google's original Go implementation of Roughtime. For 21 | // more information, visit https://roughtime.googlesource.com/roughtime. 22 | package client 23 | 24 | import ( 25 | "crypto/rand" 26 | "encoding/json" 27 | "errors" 28 | "fmt" 29 | "io" 30 | "log" 31 | "net" 32 | "os" 33 | "slices" 34 | "time" 35 | 36 | "github.com/cloudflare/roughtime/config" 37 | "github.com/cloudflare/roughtime/protocol" 38 | ) 39 | 40 | const ( 41 | DefaultQueryAttempts = 3 42 | DefaultQueryTimeout = time.Second 43 | ) 44 | 45 | var ( 46 | // Used for logging the output of Do and DoFromFile. 47 | logger *log.Logger 48 | ) 49 | 50 | // SetLogger sets the logger used to log the output of Do and DoFromFile. If 51 | // l == nil, then no output will be logged. 52 | func SetLogger(l *log.Logger) { 53 | if l == nil { 54 | logger = log.New(io.Discard, "", 0) 55 | } else { 56 | logger = l 57 | } 58 | } 59 | 60 | func init() { 61 | // Don't log output of Do and DoFromFile by default. 62 | SetLogger(nil) 63 | } 64 | 65 | // getVersionPreferenceForServer resolves the client's version preference based 66 | // on the server's configuration. 67 | func getVersionPreferenceForServer(server *config.Server) ([]protocol.Version, error) { 68 | switch server.Version { 69 | case "IETF-Roughtime": 70 | // Unspecified version preference defaults to IETF. 71 | return []protocol.Version{}, nil 72 | case "Google-Roughtime", "": 73 | return []protocol.Version{protocol.VersionGoogle}, nil 74 | default: 75 | return nil, fmt.Errorf("unrecognized version for server %s: %s", server.Name, server.Version) 76 | } 77 | } 78 | 79 | // Roughtime stores the result of a successful Roughtime query. 80 | type Roughtime struct { 81 | // The request and the blind used to generate the nonce of the request. 82 | Req, Blind []byte 83 | // The bytes of the response. 84 | Resp []byte 85 | // The time reported by the server. 86 | Midpoint time.Time 87 | // The "uncertainty radius" of the server's reported time. It indicates 88 | // that the server is "reasonably sure" that the real is within this number 89 | // of microseconds of the real time. 90 | Radius time.Duration 91 | } 92 | 93 | // Get sends a request to a server and verifies the response. It makes 94 | // at most as many attempts as specified, waiting for the given amount of time 95 | // for each reply. It uses prev to generate the nonce of the request. This 96 | // may be nil, in which case the request is the first in the chain. 97 | func Get(server *config.Server, attempts int, timeout time.Duration, prev *Roughtime) (*Roughtime, error) { 98 | versionPreference, err := getVersionPreferenceForServer(server) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | var reply, prevReply []byte 104 | if prev != nil { 105 | prevReply = prev.Resp 106 | } 107 | 108 | // Create the request. 109 | nonce, blind, request, err := protocol.CreateRequest(versionPreference, rand.Reader, prevReply, server.PublicKey) 110 | if err != nil { 111 | panic(fmt.Sprintf("internal error: %s", err)) 112 | } 113 | if len(request) < protocol.MinRequestSize { 114 | panic("internal error: bad request length") 115 | } 116 | 117 | udpAddr, err := serverUDPAddr(server) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | // Attempt to send request to the server. 123 | for i := 0; i < attempts; i++ { 124 | conn, err := net.DialUDP("udp", nil, udpAddr) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | err = conn.SetReadDeadline(time.Now().Add(timeout)) 130 | if err != nil { 131 | return nil, err 132 | } 133 | _, err = conn.Write(request) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | var replyBytes [1024]byte 139 | 140 | n, err := conn.Read(replyBytes[:]) 141 | if err == nil { 142 | reply = replyBytes[:n] 143 | break 144 | } 145 | 146 | if netErr, ok := err.(net.Error); ok { 147 | if !netErr.Timeout() { 148 | return nil, fmt.Errorf("reading from socket: %s", err) 149 | } 150 | } 151 | } 152 | 153 | if reply == nil { 154 | return nil, errors.New("no reply") 155 | } 156 | 157 | // Verify the response. 158 | midpoint, radius, err := protocol.VerifyReply(versionPreference, reply, server.PublicKey, nonce) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | return &Roughtime{ 164 | Req: request, 165 | Blind: blind[:], 166 | Resp: reply, 167 | Midpoint: midpoint, 168 | Radius: radius, 169 | }, nil 170 | } 171 | 172 | // Now returns the time provided by a Roughtime response. 173 | func (rt *Roughtime) Now() (time.Time, time.Duration) { 174 | return rt.Midpoint, rt.Radius 175 | } 176 | 177 | func (rt *Roughtime) String() string { 178 | t, d := rt.Now() 179 | return fmt.Sprintf("%s ±%s", t, d) 180 | } 181 | 182 | // ParseConfig loads parses a JSON-encoded Roughtime-server configuration, 183 | // skipping those servers that the client doesn't support. It returns the 184 | // sequenc of servers with valid configurations, as well as the number of 185 | // configurations it skipped. 186 | // 187 | // If the server's address is a domain name, the client will attempt to resolve 188 | // it. At the moment, the client only supports servers with an Ed25519 root 189 | // public key and who are listening on UDP. 190 | func ParseConfig(jsonData []byte) (servers []config.Server, skipped int, err error) { 191 | var serversJSON config.ServersJSON 192 | if err := json.Unmarshal(jsonData, &serversJSON); err != nil { 193 | return nil, len(servers), err 194 | } 195 | 196 | seenNames := make(map[string]bool) 197 | for _, srv := range serversJSON.Servers { 198 | // Ensure that no two server configurations have the same name. 199 | if _, ok := seenNames[srv.Name]; ok { 200 | return nil, len(servers), fmt.Errorf("duplicate server name: %q", srv.Name) 201 | } 202 | seenNames[srv.Name] = true 203 | 204 | if srv.PublicKeyType != "ed25519" { 205 | skipped++ 206 | continue 207 | } 208 | 209 | udpAddr, err := serverUDPAddr(&srv) 210 | if err != nil { 211 | return nil, len(servers), fmt.Errorf("%q lists invalid address: %s", srv.Name, err) 212 | } 213 | 214 | if udpAddr == nil { 215 | skipped++ 216 | continue 217 | } 218 | 219 | servers = append(servers, srv) 220 | } 221 | 222 | return servers, skipped, nil 223 | } 224 | 225 | // serverUDPAddr attempts to resolve the UDP address specified by the server 226 | // configution. 227 | func serverUDPAddr(server *config.Server) (*net.UDPAddr, error) { 228 | for _, addr := range server.Addresses { 229 | if addr.Protocol != "udp" && addr.Protocol != "udp4" && addr.Protocol != "udp6" { 230 | continue 231 | } 232 | 233 | return net.ResolveUDPAddr("udp", addr.Address) 234 | } 235 | 236 | return nil, nil 237 | } 238 | 239 | // LoadConfig reads and parses a JSON-encoded string from configFile. 240 | func LoadConfig(configFile string) (servers []config.Server, skipped int, err error) { 241 | jsonBytes, err := os.ReadFile(configFile) 242 | if err != nil { 243 | return nil, 0, err 244 | } 245 | 246 | return ParseConfig(jsonBytes) 247 | } 248 | 249 | // Result stores the request and response of a Roughtime query to a server. It 250 | // is either a server's time or an error. 251 | type Result struct { 252 | *Roughtime 253 | // The configuration of the server used for the query. 254 | Server *config.Server 255 | // The network delay incurred by the query. 256 | Delay time.Duration 257 | // The error recorded on an unsuccessful query. 258 | err error 259 | } 260 | 261 | // Error returns the error resulting from the query, if any. 262 | func (r *Result) Error() error { 263 | return r.err 264 | } 265 | 266 | // Do requests Roughtime from a sequence of servers in order. If the request 267 | // fails, then the error is recorded. The nonce of each request is computed from 268 | // the response of the last, skipping requests that fail. 269 | func Do(servers []config.Server, attempts int, timeout time.Duration, prev *Roughtime) []Result { 270 | results := make([]Result, 0, len(servers)) 271 | var delay time.Duration 272 | for i := range servers { 273 | srv := &servers[i] 274 | start := time.Now() 275 | rt, err := Get(srv, attempts, timeout, prev) 276 | delay = time.Since(start) 277 | if err == nil { // Request succeeded 278 | logger.Printf("%s: %s (in %v)", srv.Name, rt, delay.Truncate(time.Millisecond)) 279 | prev = rt 280 | } else { // Request failed 281 | logger.Printf("skipped %s: %s\n", srv.Name, err) 282 | } 283 | results = append(results, Result{rt, srv, delay, err}) 284 | } 285 | return results 286 | } 287 | 288 | // DoFromFile loads a sequence of server configurations from configFile and requests 289 | // Roughtime from them in order. 290 | func DoFromFile(configFile string, attempts int, timeout time.Duration, prev *Roughtime) ([]Result, error) { 291 | servers, skipped, err := LoadConfig(configFile) 292 | if err != nil { 293 | return nil, err 294 | } else if len(servers) == 0 { 295 | return nil, fmt.Errorf("%s has no suitable servers", configFile) 296 | } else if skipped > 0 { 297 | return nil, fmt.Errorf("would skip %d servers", skipped) 298 | } 299 | 300 | return Do(servers, attempts, timeout, prev), nil 301 | } 302 | 303 | // MedianDeltaWithRadiusThresh computes the median difference between t0 304 | // and the time reported by each server, rejecting responses whose uncertainty 305 | // radii aren't within the accepted limit. 306 | func MedianDeltaWithRadiusThresh(results []Result, t0 time.Time, thresh time.Duration) (time.Duration, error) { 307 | if len(results) == 0 { 308 | return 0, errors.New("no results") 309 | } 310 | 311 | var deltas []time.Duration 312 | var delay time.Duration 313 | for _, res := range results { 314 | delay += res.Delay 315 | if res.Error() == nil { 316 | rt := res.Roughtime 317 | t1, radius := rt.Now() 318 | 319 | // Decide whether to reject this result. 320 | if radius > thresh { 321 | continue 322 | } 323 | 324 | // Add the delta between this time and t0, accounting for the 325 | // network delay accumulated so far. 326 | deltas = append(deltas, t1.Sub(t0)-delay) 327 | } 328 | } 329 | 330 | if len(deltas) == 0 { 331 | return 0, errors.New("no valid responses") 332 | } 333 | // Compute median delta 334 | slices.Sort(deltas) 335 | if len(deltas)%2 == 0 { 336 | return (deltas[(len(deltas)-1)/2] + deltas[len(deltas)/2]) / 2, nil 337 | } else { 338 | return deltas[(len(deltas)-1)/2], nil 339 | } 340 | } 341 | 342 | // Chain represents a sequence of ordered Roughtime queries. 343 | type Chain struct { 344 | *Roughtime 345 | // The server who signed the response. 346 | Server *config.Server 347 | // The next query in the chain. 348 | Next *Chain 349 | } 350 | 351 | // NewChain returns a Roughtime chain comprised of the successful queries in a 352 | // sequence of results. 353 | func NewChain(results []Result) *Chain { 354 | var next *Chain 355 | for i := len(results) - 1; i >= 0; i-- { 356 | if results[i].Error() == nil { 357 | link := &Chain{ 358 | results[i].Roughtime, 359 | results[i].Server, 360 | next, 361 | } 362 | next = link 363 | } 364 | } 365 | return next 366 | } 367 | 368 | // Verify returns true if the chain is valid. A chain is valid if for each link 369 | // in the chain, (1) the signature in the server's response is valid, and (2) the 370 | // response was used to generate the nonce in the next link's request. 371 | // 372 | // If prev != nil, then prev.Resp is used to compute the nonce for the first 373 | // request in the chain. 374 | func (chain *Chain) Verify(prev *Roughtime) (bool, error) { 375 | var prevReply []byte 376 | if prev != nil { 377 | prevReply = prev.Resp 378 | } 379 | for link := chain; link != nil; link = link.Next { 380 | versionPreference, err := getVersionPreferenceForServer(chain.Server) 381 | if err != nil { 382 | return false, err 383 | } 384 | 385 | nonce := make([]byte, len(link.Roughtime.Blind)) 386 | protocol.CalculateChainNonce(nonce, prevReply, link.Roughtime.Blind) 387 | m, r, err := protocol.VerifyReply(versionPreference, link.Roughtime.Resp, link.Server.PublicKey, nonce) 388 | if err != nil { 389 | return false, err 390 | } 391 | 392 | if m != link.Roughtime.Midpoint || r != link.Roughtime.Radius { 393 | return false, errors.New("timestamp mismatch") 394 | } 395 | 396 | prevReply = link.Roughtime.Resp 397 | } 398 | return true, nil 399 | } 400 | -------------------------------------------------------------------------------- /cmd/getroughtime/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Cloudflare, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // A simple Roughtime client. 16 | package main 17 | 18 | import ( 19 | "encoding/base64" 20 | "flag" 21 | "log" 22 | "os" 23 | "runtime" 24 | "time" 25 | 26 | "github.com/cloudflare/roughtime/client" 27 | "github.com/cloudflare/roughtime/config" 28 | ) 29 | 30 | const ( 31 | // Build info. 32 | Version = "dev" 33 | BuildTime = "" 34 | ) 35 | 36 | func main() { 37 | // Command-line arguments. 38 | getVersion := flag.Bool("version", false, "Print the version and exit.") 39 | configFile := flag.String("config", "", "A list of Roughtime servers.") 40 | pingAddr := flag.String("ping", "", "Send a UDP request, e.g., localhost:2002.") 41 | pingPubKey := flag.String("pubkey", "", "The Ed25519 public key of the address to ping.") 42 | pingVersion := flag.String("ping-version", "Google-Roughtime", "The Roughtime version to use in ping.") 43 | attempts := flag.Int("attempts", client.DefaultQueryAttempts, "Number of times to try each server.") 44 | timeout := flag.Duration("timeout", client.DefaultQueryTimeout, "Amount of time to wait for each request.") 45 | 46 | flag.Parse() 47 | logger := log.New(os.Stdout, "", 0) 48 | client.SetLogger(logger) 49 | 50 | if *getVersion { 51 | logger.Printf("getroughtime %s (%s) built %s\n", Version, runtime.Version(), BuildTime) 52 | os.Exit(0) 53 | } 54 | 55 | if *configFile != "" { 56 | t0 := time.Now() 57 | res, err := client.DoFromFile(*configFile, *attempts, *timeout, nil) 58 | if err != nil { 59 | logger.Fatal(err) 60 | } 61 | delta, err := client.MedianDeltaWithRadiusThresh(res, t0, 10*time.Second) 62 | if err != nil { 63 | logger.Fatal(err) 64 | } 65 | logger.Printf("Delta: %v", delta.Truncate(time.Millisecond)) 66 | os.Exit(0) 67 | } 68 | 69 | if *pingAddr != "" { 70 | if *pingPubKey == "" { 71 | logger.Fatal("Ping: missing -pubkey") 72 | } 73 | pk, err := base64.StdEncoding.DecodeString(*pingPubKey) 74 | if err != nil { 75 | logger.Fatalf("Public key decode error: %s\n", err) 76 | } else if len(pk) != 32 { 77 | logger.Fatalf("Public key decode error: incorrect length") 78 | } 79 | switch *pingVersion { 80 | case "Google-Roughtime", "IETF-Roughtime": 81 | default: 82 | logger.Fatalf("Invalid ping version: %s\n", *pingVersion) 83 | } 84 | 85 | server := &config.Server{ 86 | Name: "", 87 | Version: *pingVersion, 88 | PublicKeyType: "ed25519", 89 | PublicKey: pk, 90 | Addresses: []config.ServerAddress{ 91 | { 92 | Protocol: "udp", 93 | Address: *pingAddr, 94 | }, 95 | }, 96 | } 97 | 98 | start := time.Now() 99 | rt, err := client.Get(server, *attempts, *timeout, nil) 100 | delay := time.Since(start).Truncate(time.Millisecond) 101 | if err != nil { 102 | logger.Fatalf("Ping error: %s\n", err) 103 | } 104 | logger.Printf("Ping response: %s (in %s)\n", rt, delay) 105 | os.Exit(0) 106 | } 107 | 108 | logger.Fatal("Either provide a configuration via -config or an address via -ping") 109 | } 110 | -------------------------------------------------------------------------------- /cmd/keygen/keygen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "flag" 8 | "log" 9 | "os" 10 | ) 11 | 12 | func main() { 13 | pubFile := flag.String("pub", "", "File to put the public key in") 14 | privFile := flag.String("priv", "", "File to put the private key in") 15 | 16 | flag.Parse() 17 | pub, priv, err := ed25519.GenerateKey(rand.Reader) 18 | if err != nil { 19 | log.Fatalf("Error generating key: %v", err) 20 | } 21 | 22 | pubEnc := base64.StdEncoding.EncodeToString(pub) 23 | 24 | privEnc := base64.StdEncoding.EncodeToString(priv) 25 | 26 | if err := os.WriteFile(*pubFile, []byte(pubEnc), 0o644); err != nil { 27 | log.Fatal("Can't write public key") 28 | } 29 | 30 | if err := os.WriteFile(*privFile, []byte(privEnc), 0o600); err != nil { 31 | log.Fatal("Can't write private key") 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /cmd/testserver/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloudflare, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // A simple Roughtime server, intended for testing. 16 | package main 17 | 18 | import ( 19 | "crypto/ed25519" 20 | "crypto/rand" 21 | "encoding/base64" 22 | "encoding/hex" 23 | "errors" 24 | "flag" 25 | "log" 26 | "net" 27 | "time" 28 | 29 | "github.com/cloudflare/roughtime/protocol" 30 | ) 31 | 32 | // Roughtime radius to use for responses 33 | const radius = time.Second 34 | 35 | func main() { 36 | var ( 37 | // Command line parameters 38 | addr = flag.String("addr", "127.0.0.1:2002", "address to listen on") 39 | rootKeySeedHex = flag.String("root-key", "", "hex-encoded root key seed (random 32 bytes)") 40 | 41 | err error 42 | rootKeySeed []byte 43 | ) 44 | 45 | log.SetFlags(log.Lshortfile &^ (log.Ldate | log.Ltime)) 46 | flag.Parse() 47 | 48 | // Set up root key 49 | if *rootKeySeedHex != "" { 50 | rootKeySeed, err = hex.DecodeString(*rootKeySeedHex) 51 | if err != nil { 52 | log.Fatalf("Failed to parse root key seed: %v", err) 53 | } 54 | if len(rootKeySeed) != 32 { 55 | log.Fatalf("Unexpected root key seed length: got %d; want 32", len(rootKeySeed)) 56 | } 57 | } else { 58 | rootKeySeed = make([]byte, 32) 59 | if _, err = rand.Read(rootKeySeed); err != nil { 60 | log.Fatalf("rand.Read() failed: %v", err) 61 | } 62 | } 63 | rootSK := ed25519.NewKeyFromSeed(rootKeySeed) 64 | log.Printf("Root public key: %s", base64.StdEncoding.EncodeToString(rootSK.Public().(ed25519.PublicKey))) 65 | 66 | netAddr, err := net.ResolveUDPAddr("udp", *addr) 67 | if err != nil { 68 | log.Fatalf("Could not resolve %s: %v", netAddr, err) 69 | } 70 | conn, err := net.ListenUDP("udp", netAddr) 71 | if err != nil { 72 | log.Fatalf("Could not listen on %s: %v", *addr, err) 73 | } 74 | 75 | _, onlineSK, err := ed25519.GenerateKey(rand.Reader) 76 | if err != nil { 77 | log.Fatalf("Could not generate key: %v", err) 78 | } 79 | 80 | now := time.Now() 81 | yesterday := now.Add(-24 * time.Hour) 82 | tomorrow := now.Add(24 * time.Hour) 83 | onlineCert, err := protocol.NewCertificate(yesterday, tomorrow, onlineSK, rootSK) 84 | if err != nil { 85 | log.Fatalf("Could not generate certificate: %v", err) 86 | } 87 | 88 | buf := make([]byte, 1280) 89 | for { 90 | reqLen, peer, err := conn.ReadFrom(buf) 91 | if err != nil { 92 | log.Fatalf("Failed to read request: %v", err) 93 | } 94 | 95 | resp, err := handleRequest(buf[:reqLen], onlineCert, onlineSK) 96 | if err != nil { 97 | log.Fatalf("Error while handling request: %v", err) 98 | } 99 | 100 | if _, err = conn.WriteTo(resp, peer); err != nil { 101 | log.Fatalf("Failed to write response: %v", err) 102 | } 103 | } 104 | } 105 | 106 | func handleRequest(requestBytes []byte, cert *protocol.Certificate, onlineSK ed25519.PrivateKey) (resp []byte, err error) { 107 | req, err := protocol.ParseRequest(requestBytes) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | responseVer, err := protocol.ResponseVersionFromSupported(req.Versions) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | // Parse the request and create the response. 118 | replies, err := protocol.CreateReplies(responseVer, []protocol.Request{*req}, time.Now(), radius, cert) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | if len(replies) != 1 { 124 | return nil, errors.New("internal error: unexpected number of replies were computed") 125 | } 126 | 127 | return replies[0], nil 128 | } 129 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Roughtime Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. */ 14 | 15 | // Modifications copyright 2023 Cloudflare, Inc. This file has been modified to 16 | // allow the server config to indicate whether it supports IETF-Roughtime. 17 | 18 | // Package config contains JSON structs for encoding information about 19 | // Roughtime servers. 20 | package config 21 | 22 | // ServersJSON represents a JSON format for distributing information about 23 | // Roughtime servers. 24 | type ServersJSON struct { 25 | Servers []Server `json:"servers"` 26 | } 27 | 28 | // Server represents a Roughtime server in a JSON configuration. 29 | type Server struct { 30 | Name string `json:"name"` 31 | // Version indicates whether the server is known to support the IETF 32 | // version ("IETF-Roughtime") or only the legacy version 33 | // ("Google-Roughtime"). If unspecified, then legacy should be assumed. 34 | Version string `json:"version"` 35 | // PublicKeyType specifies the type of the public key contained in 36 | // |PublicKey|. Normally this will be "ed25519" but implementations 37 | // should ignore entries with unknown key types. 38 | PublicKeyType string `json:"publicKeyType"` 39 | PublicKey []byte `json:"publicKey"` 40 | Addresses []ServerAddress `json:"addresses"` 41 | } 42 | 43 | // ServerAddress represents the address of a Roughtime server in a JSON 44 | // configuration. 45 | type ServerAddress struct { 46 | Protocol string `json:"protocol"` 47 | // Address contains a protocol specific address. For the protocol 48 | // "udp", the address has the form "host:port" where host is either a 49 | // DNS name, an IPv4 literal, or an IPv6 literal in square brackets. 50 | Address string `json:"address"` 51 | } 52 | 53 | // Chain represents a history of Roughtime queries where each provably follows 54 | // the previous one. 55 | type Chain struct { 56 | Links []Link `json:"links"` 57 | } 58 | 59 | // Link represents an entry in a Chain. 60 | type Link struct { 61 | // PublicKeyType specifies the type of public key contained in 62 | // |PublicKey|. See the same field in |Server| for details. 63 | PublicKeyType string `json:"publicKeyType"` 64 | PublicKey []byte `json:"serverPublicKey"` 65 | // NonceOrBlind contains either the full nonce (only for the first 66 | // |Link| in a |Chain|) or else contains a blind value that is combined 67 | // with the previous reply to make the next nonce. In either case, the 68 | // value is 64 bytes long. 69 | NonceOrBlind []byte `json:"nonceOrBlind"` 70 | // Reply contains the reply from the server. 71 | Reply []byte `json:"reply"` 72 | } 73 | -------------------------------------------------------------------------------- /ecosystem.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | { 4 | "name": "Cloudflare-Roughtime-2", 5 | "version": "IETF-Roughtime", 6 | "publicKeyType": "ed25519", 7 | "publicKey": "0GD7c3yP8xEc4Zl2zeuN2SlLvDVVocjsPSL8/Rl/7zg=", 8 | "addresses": [ 9 | { 10 | "protocol": "udp", 11 | "address": "roughtime.cloudflare.com:2003" 12 | } 13 | ] 14 | }, 15 | { 16 | "name": "int08h-Roughtime", 17 | "version": "IETF-Roughtime", 18 | "publicKeyType": "ed25519", 19 | "publicKey": "AW5uAoTSTDfG5NfY1bTh08GUnOqlRb+HVhbJ3ODJvsE=", 20 | "addresses": [ 21 | { 22 | "protocol": "udp", 23 | "address": "roughtime.int08h.com:2002" 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "roughtime.se", 29 | "version": "IETF-Roughtime", 30 | "publicKeyType": "ed25519", 31 | "publicKey": "S3AzfZJ5CjSdkJ21ZJGbxqdYP/SoE8fXKY0+aicsehI=", 32 | "addresses": [ 33 | { 34 | "protocol": "udp", 35 | "address": "roughtime.se:2002" 36 | } 37 | ] 38 | }, 39 | { 40 | "name": "time.txryan.com", 41 | "version": "Google-Roughtime", 42 | "publicKeyType": "ed25519", 43 | "publicKey": "iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=", 44 | "addresses": [ 45 | { 46 | "protocol": "udp", 47 | "address": "time.txryan.com:2002" 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /ecosystem.json.go: -------------------------------------------------------------------------------- 1 | // Code generated by "ecosystem_json_go_builder". DO NOT EDIT. 2 | // source: ecosystem.json 3 | package roughtime 4 | 5 | import "github.com/cloudflare/roughtime/config" 6 | 7 | var Ecosystem = []config.Server{ 8 | { 9 | Name: "Cloudflare-Roughtime-2", 10 | Version: "IETF-Roughtime", 11 | PublicKeyType: "ed25519", 12 | PublicKey: []byte{208, 96, 251, 115, 124, 143, 243, 17, 28, 225, 153, 118, 205, 235, 141, 217, 41, 75, 188, 53, 85, 161, 200, 236, 61, 34, 252, 253, 25, 127, 239, 56}, 13 | Addresses: []config.ServerAddress{ 14 | { 15 | Protocol: "udp", 16 | Address: "roughtime.cloudflare.com:2003", 17 | }, 18 | }, 19 | }, 20 | { 21 | Name: "int08h-Roughtime", 22 | Version: "IETF-Roughtime", 23 | PublicKeyType: "ed25519", 24 | PublicKey: []byte{1, 110, 110, 2, 132, 210, 76, 55, 198, 228, 215, 216, 213, 180, 225, 211, 193, 148, 156, 234, 165, 69, 191, 135, 86, 22, 201, 220, 224, 201, 190, 193}, 25 | Addresses: []config.ServerAddress{ 26 | { 27 | Protocol: "udp", 28 | Address: "roughtime.int08h.com:2002", 29 | }, 30 | }, 31 | }, 32 | { 33 | Name: "roughtime.se", 34 | Version: "IETF-Roughtime", 35 | PublicKeyType: "ed25519", 36 | PublicKey: []byte{75, 112, 51, 125, 146, 121, 10, 52, 157, 144, 157, 181, 100, 145, 155, 198, 167, 88, 63, 244, 168, 19, 199, 215, 41, 141, 62, 106, 39, 44, 122, 18}, 37 | Addresses: []config.ServerAddress{ 38 | { 39 | Protocol: "udp", 40 | Address: "roughtime.se:2002", 41 | }, 42 | }, 43 | }, 44 | { 45 | Name: "time.txryan.com", 46 | Version: "Google-Roughtime", 47 | PublicKeyType: "ed25519", 48 | PublicKey: []byte{136, 21, 99, 198, 15, 245, 143, 188, 181, 250, 68, 20, 76, 22, 29, 77, 166, 241, 10, 154, 94, 177, 79, 244, 236, 62, 15, 48, 50, 100, 217, 96}, 49 | Addresses: []config.ServerAddress{ 50 | { 51 | Protocol: "udp", 52 | Address: "time.txryan.com:2002", 53 | }, 54 | }, 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /ecosystem.md: -------------------------------------------------------------------------------- 1 | # The Roughtime ecosystem 2 | 3 | File `ecosystem.json` contains the configurations of a growing list of Roughtime 4 | servers. This file contains a brief description of how each server is 5 | provisioned. Refer to `README.md` for information about adding your server to 6 | the list. 7 | 8 | 9 | ## Cloudflare-Roughtime-2 10 | 11 | Cloudflare's Roughtime service aims for high availability and low latency. The 12 | [announcement](https://blog.cloudflare.com/roughtime/) provides details about 13 | how we set up the service. Briefly, the domain for Roughtime resolves to an 14 | address in Cloudflare's anycast IP range (both IPv4 and IPv6 are supported), so 15 | the response may come from any one of their points of presence. The 16 | implementation is based on Google's [Go 17 | code](https://roughtime.googlesource.com/roughtime), but has been updated to 18 | support both Google-Roughtime and IETF Roughtime (draft08). This service is 19 | currently in beta. As such the root key is subject to change. It will be 20 | updated here and in the [developer 21 | docs](https://developers.cloudflare.com/time-services/roughtime/recipes/). You 22 | can also obtain it over DNS; see the docs for details. 23 | 24 | 25 | ## int08h-Roughtime 26 | 27 | A public Roughtime server operated by the author of the [Rust](https://github.com/int08h/roughenough) 28 | and [Java](https://github.com/int08h/nearenough) implementations of Roughtime. 29 | 30 | The server runs the latest release of [roughenough](https://github.com/int08h/roughenough) 31 | on Digital Ocean droplets in their US NYC datacenter. The server supports both the 32 | Google-Roughtime and IETF-Roughtime protocols. Time is sourced from Google's 33 | [public NTP servers](https://developers.google.com/time/smear), 34 | Amazon's [public NTP servers](https://aws.amazon.com/about-aws/whats-new/2022/11/amazon-time-sync-internet-public-ntp-service/), 35 | and NIST's [public NTP servers](https://www.nist.gov/pml/time-and-frequency-division/time-distribution/internet-time-service-its). 36 | 37 | Available at `roughtime.int08h.com:2002` its public key is stable and the service 38 | is available 24/7, modulo a few seconds downtime for maintenance. 39 | 40 | The int08h instance keeps the "rough" in Roughtime: it smears leapseconds 41 | and always reports a 'radius' (RADI tag) of 2 seconds to account for the resulting 42 | uncertainty. The int08h Roughtime instance will **never** set the DUT1, 43 | DTAI, or LEAP tags as this level of precision is unnecessary. 44 | 45 | The public key is available in a [blog post at int08h](https://int08h.com/post/public-roughtime-server/), 46 | and the DNS `TXT` record of `roughtime.int08h.com`: 47 | 48 | ``` 49 | $ dig -t txt roughtime.int08h.com 50 | ``` 51 | 52 | ## roughtime.se 53 | 54 | [roughtime.se](https://roughtime.se) provides a stratum 1 Roughtime service. It 55 | runs the [roughtimed](https://github.com/dansarie/roughtimed) implementation. 56 | Hosting is provided by STUPI AB. The server is located in Stockholm, Sweden and 57 | is directly connected to atomic clocks that track the UTC timescale. The aim is 58 | for the server to be compatible with the 59 | [latest published IETF Roughtime draft](https://datatracker.ietf.org/doc/draft-ietf-ntp-roughtime/). 60 | The server is connected to high-availability power and network infrastructure in 61 | a datacenter, however no availability is guaranteed. The public key is available 62 | on the server's web site, and as a DNS TXT record: 63 | 64 | ``` 65 | dig TXT roughtime.se 66 | ``` 67 | 68 | ## time.txryan.com 69 | 70 | [time.txryan.com](https://time.txryan.com) runs on a stratum 2 NTP server. 71 | 72 | The clock is synchronized with authenticated NTP connections to NIST (National 73 | Institute of Standards and Technology), and the Canadian equivalent, NRC 74 | (National Research Council Canada), which are both directly connected to atomic 75 | sources (caesium fountains and/or hydrogen masers). There are also multiple 76 | unauthenticated stratum 1 upstreams, maintained by GNSS (GPS + Galileo + 77 | GLONASS). The accuracy is typically within +/- 50 microseconds. 78 | 79 | The Roughtime service is accessible at `time.txryan.com:2002`. The public key is 80 | available on time.txryan.com's [website](https://time.txryan.com), or through a 81 | DNS TXT lookup. 82 | 83 | ``` 84 | dig TXT time.txryan.com +short 85 | ``` 86 | 87 | The Roughtime service is powered by Google's [Go reference 88 | implementation](https://roughtime.googlesource.com/roughtime/). 89 | 90 | No uptime is guaranteed, but the server is constantly monitored for accuracy and 91 | availability. From time to time, there may be a few minutes of downtime for 92 | server maintenance. 93 | 94 | 95 | ## Inactive servers 96 | 97 | 98 | ## Chainpoint-Roughtime 99 | 100 | **This service is unreachable as of 2024-07-01.** 101 | 102 | The [Chainpoint](https://chainpoint.org) Roughtime service is hosted 103 | at `roughtime.chainpoint.org:2002`. The public key, and information about 104 | running the Docker container we've created for [roughenough](https://github.com/int08h/roughenough), 105 | is provided at [https://github.com/chainpoint/chainpoint-roughtime](https://github.com/chainpoint/chainpoint-roughtime). 106 | 107 | In addition to the Github repository `README.md`, the long-term public key in 108 | Hexadecimal form is also provided as a DNS `TXT` record accessible with: 109 | 110 | ``` 111 | $ dig -t txt roughtime.chainpoint.org 112 | ``` 113 | 114 | The Chainpoint Roughtime service is in open beta, but aims to operate with 115 | high-availability. The [roughenough](https://github.com/int08h/roughenough) 116 | Rust implementation of Roughtime is currently running on two servers in the 117 | Google Compute Engine cloud (US-EAST4), both synced to Google's internal 118 | high accuracy NTP service. These servers exist behind a public UDP 119 | load-balancer and a Cloudflare DNS `A` record. 120 | 121 | 122 | 123 | ### Cloudflare-Roughtime 124 | 125 | **Deprecation notice**: The Cloudflare-Roughtime server will be shut down on 126 | 2024-07-01. Please update your client to use Cloudflare-Roughtime-2 instead. 127 | 128 | 129 | ## Google-Sandbox-Roughtime 130 | 131 | **This service is unreachable as of 2024-07-01.** 132 | 133 | This is Google's [proof-of-concept 134 | server](https://roughtime.googlesource.com/roughtime/#current-state-of-the-project). 135 | It is experimental and does not, as of yet, provide uptime guarantees. The root 136 | public key is published 137 | [here](https://roughtime.googlesource.com/roughtime/+/master/roughtime-servers.json). 138 | 139 | 140 | ## Mixmin Roughtime 141 | 142 | **This service is unreachable as of 2024-07-01.** 143 | 144 | Mixmin's Roughtime service resides on a dedicated Raspberry Pi running Arch 145 | Linux. The Pi has an Adafruit GPS module fitted and uses it to sync the system 146 | clock via NTP. It uses Adam Langley's reference implementation of Roughtime, 147 | written in Go and is compiled locally on the Raspberry Pi. The Roughtime 148 | server was announced on the mailing list, archived 149 | [here](https://groups.google.com/a/chromium.org/forum/#!topic/proto-roughtime/7PApRXJ-x0Y). 150 | The announcement includes the server details. 151 | 152 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudflare/roughtime 2 | 3 | go 1.21.1 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/roughtime/8b34bf166fa60d69ce45fff751d4384bafe4cee0/go.sum -------------------------------------------------------------------------------- /internal/ecosystem_json_go_builder/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Roughtime Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Modifications copyright 2018 Cloudflare, Inc. 16 | 17 | // This package defines some functionalities useful for building Roughtime 18 | // clients. It's based on Google's original Go implementation of Roughtime. For 19 | // more information, visit https://roughtime.googlesource.com/roughtime/. 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "log" 25 | "os" 26 | 27 | "github.com/cloudflare/roughtime/client" 28 | ) 29 | 30 | const ( 31 | inputJSONFile string = "ecosystem.json" 32 | outputGoFile string = inputJSONFile + ".go" 33 | ) 34 | 35 | func main() { 36 | client.SetLogger(log.New(os.Stderr, "", log.LstdFlags)) 37 | servers, skipped, err := client.LoadConfig(inputJSONFile) 38 | if err != nil { 39 | panic(err) 40 | } 41 | if skipped > 0 { 42 | fmt.Printf("skipped %v servers", skipped) 43 | } 44 | 45 | file, err := os.Create(outputGoFile) 46 | if err != nil { 47 | panic(err) 48 | } 49 | defer file.Close() 50 | 51 | fmt.Fprint(file, filePrefix) 52 | for _, server := range servers { 53 | fmt.Fprintf(file, "\t{\n") 54 | fmt.Fprintf(file, "\t\tName: \"%v\",\n", server.Name) 55 | fmt.Fprintf(file, "\t\tVersion: \"%v\",\n", server.Version) 56 | fmt.Fprintf(file, "\t\tPublicKeyType: \"%v\",\n", server.PublicKeyType) 57 | 58 | fmt.Fprint(file, "\t\tPublicKey: []byte{") 59 | for i, b := range server.PublicKey { 60 | fmt.Fprint(file, b) 61 | if i < len(server.PublicKey)-1 { 62 | fmt.Fprint(file, ", ") 63 | } 64 | } 65 | fmt.Fprintln(file, "},") 66 | 67 | fmt.Fprintln(file, "\t\tAddresses: []config.ServerAddress{") 68 | for _, address := range server.Addresses { 69 | fmt.Fprintln(file, "\t\t\t{") 70 | fmt.Fprintf(file, "\t\t\t\tProtocol: \"%v\",\n", address.Protocol) 71 | fmt.Fprintf(file, "\t\t\t\tAddress: \"%v\",\n", address.Address) 72 | fmt.Fprintln(file, "\t\t\t},") 73 | } 74 | fmt.Fprintln(file, "\t\t},") 75 | fmt.Fprintln(file, "\t},") 76 | } 77 | fmt.Fprintln(file, "}") 78 | } 79 | 80 | const ( 81 | filePrefix string = `// Code generated by "ecosystem_json_go_builder". DO NOT EDIT. 82 | // source: ecosystem.json 83 | package roughtime 84 | 85 | import "github.com/cloudflare/roughtime/config" 86 | 87 | var Ecosystem = []config.Server{ 88 | ` 89 | ) 90 | -------------------------------------------------------------------------------- /internal/ecosystem_json_go_builder/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Roughtime Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Modifications copyright 2018 Cloudflare, Inc. 16 | 17 | // This package defines some functionalities useful for building Roughtime 18 | // clients. It's based on Google's original Go implementation of Roughtime. For 19 | // more information, visit https://roughtime.googlesource.com/roughtime/. 20 | package main 21 | 22 | import ( 23 | "reflect" 24 | "testing" 25 | 26 | "github.com/cloudflare/roughtime" 27 | "github.com/cloudflare/roughtime/client" 28 | ) 29 | 30 | func TestMain(t *testing.T) { 31 | fromJSON, _, err := client.LoadConfig("../../" + inputJSONFile) 32 | if err != nil { 33 | t.Errorf("load config error: %v", err) 34 | } else { 35 | if !reflect.DeepEqual(fromJSON, roughtime.Ecosystem) { 36 | t.Error("ecosystem.json not equal ecosystem.json.go") 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /protocol/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloudflare, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package protocol 16 | 17 | import "fmt" 18 | 19 | // ErrorType is an error type. 20 | type ErrorType uint16 21 | 22 | const ( 23 | ErrorDecode ErrorType = iota 24 | ErrorNonceLen 25 | ErrorRequestLen 26 | ErrorUnsupportedVersion 27 | ErrorMissingVersion 28 | ) 29 | 30 | // Error represents a protocol error. 31 | type Error struct { 32 | // Type is the error type. 33 | Type ErrorType 34 | 35 | // Info includes optional info. 36 | Info string 37 | } 38 | 39 | func (e Error) Error() string { 40 | s := "" 41 | switch e.Type { 42 | case ErrorDecode: 43 | s += "decode" 44 | case ErrorNonceLen: 45 | s += "nonce length" 46 | case ErrorRequestLen: 47 | s += "request length" 48 | case ErrorUnsupportedVersion: 49 | s += "no version in common" 50 | case ErrorMissingVersion: 51 | s += "missing VER tag" 52 | default: 53 | s += "unknown" 54 | } 55 | if len(e.Info) > 0 { 56 | s += ": " + e.Info 57 | } 58 | return s 59 | } 60 | 61 | func errDecode(info string) Error { 62 | return Error{ 63 | Type: ErrorDecode, 64 | Info: info, 65 | } 66 | } 67 | 68 | func errUnsupportedVersion(vers []Version) Error { 69 | return Error{ 70 | Type: ErrorUnsupportedVersion, 71 | Info: fmt.Sprintf("%q", vers), 72 | } 73 | } 74 | 75 | var ( 76 | errNonceLen = Error{ 77 | Type: ErrorNonceLen, 78 | Info: "", 79 | } 80 | errRequestLen = Error{ 81 | Type: ErrorRequestLen, 82 | Info: "", 83 | } 84 | errMissingVersion = Error{ 85 | Type: ErrorMissingVersion, 86 | Info: "", 87 | } 88 | ) 89 | -------------------------------------------------------------------------------- /protocol/internal/cmd/gen_test_vectors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloudflare, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Generate test vectors consumed by the unit tests for the protocol package. 16 | package main 17 | 18 | import ( 19 | "crypto/ed25519" 20 | "encoding/hex" 21 | "encoding/json" 22 | "errors" 23 | "fmt" 24 | "os" 25 | "time" 26 | 27 | "github.com/cloudflare/roughtime/protocol" 28 | "github.com/cloudflare/roughtime/protocol/internal/testing" 29 | ) 30 | 31 | const ( 32 | ROOT_KEY_HEX = "d102b712f341204711daaf20e0d13557a37073e9c25325c1c6bda876eb2d6a2d" 33 | ONLINE_KEY_HEX = "613bbf61d362d6474041486a9440feeb7cc71b48951a30e7b0190be42bc7a5ab" 34 | ) 35 | 36 | var ( 37 | rootPrivateKey ed25519.PrivateKey 38 | rootPublicKey ed25519.PublicKey 39 | onlinePrivateKey ed25519.PrivateKey 40 | onlineCert *protocol.Certificate 41 | 42 | testMinTime = time.Unix(0, 0) 43 | testMaxTime = time.Unix(100, 0) 44 | testMidpoint = time.Unix(50, 0) 45 | testRadius = time.Duration(5) * time.Second 46 | ) 47 | 48 | func init() { 49 | rootKeySeed, err := hex.DecodeString(ROOT_KEY_HEX) 50 | if err != nil { 51 | panic(err) 52 | } 53 | rootPrivateKey = ed25519.NewKeyFromSeed(rootKeySeed) 54 | rootPublicKey = rootPrivateKey.Public().(ed25519.PublicKey) 55 | 56 | onlineKeySeed, err := hex.DecodeString(ONLINE_KEY_HEX) 57 | if err != nil { 58 | panic(err) 59 | } 60 | onlinePrivateKey = ed25519.NewKeyFromSeed(onlineKeySeed) 61 | onlineCert, err = protocol.NewCertificate(testMinTime, testMaxTime, onlinePrivateKey, rootPrivateKey) 62 | if err != nil { 63 | panic(err) 64 | } 65 | } 66 | 67 | func ensureDir(dirName string) error { 68 | err := os.Mkdir(dirName, 0777) 69 | if err == nil { 70 | return nil 71 | } 72 | if os.IsExist(err) { 73 | // Check that the existing path is a directory. 74 | info, err := os.Stat(dirName) 75 | if err != nil { 76 | return err 77 | } 78 | if !info.IsDir() { 79 | return errors.New("path exists but is not a directory") 80 | } 81 | return nil 82 | } 83 | return err 84 | } 85 | 86 | func fileNmameFor(ver protocol.Version) string { 87 | switch ver { 88 | case protocol.VersionGoogle: 89 | return "roughtime_google" 90 | case protocol.VersionDraft08: 91 | return "roughtime_ietf_draft08" 92 | case protocol.VersionDraft11: 93 | return "roughtime_ietf_draft11" 94 | default: 95 | panic("unhandled version") 96 | } 97 | } 98 | 99 | func main() { 100 | if err := ensureDir("testdata"); err != nil { 101 | panic(err) 102 | } 103 | 104 | for _, ver := range []protocol.Version{protocol.VersionDraft11, protocol.VersionDraft08, protocol.VersionGoogle} { 105 | r := testing.NewTestRand() 106 | clientVersionPref := []protocol.Version{ver} 107 | 108 | for _, numRequestsPerBatch := range []int{1, 10, 100} { 109 | var testVec testing.TestVector 110 | testVec.Info = fmt.Sprintf("%s %d", ver, numRequestsPerBatch) 111 | testVec.RootKey = ROOT_KEY_HEX 112 | testVec.OnlineKey = ONLINE_KEY_HEX 113 | 114 | // Set the requests and replies. 115 | requests := make([]protocol.Request, 0, numRequestsPerBatch) 116 | for i := 0; i < numRequestsPerBatch; i++ { 117 | _, _, reqBytes, err := protocol.CreateRequest(clientVersionPref, r, nil, rootPublicKey) 118 | if err != nil { 119 | panic(err) 120 | } 121 | req, err := protocol.ParseRequest(reqBytes) 122 | if err != nil { 123 | panic(err) 124 | } 125 | testVec.Requests = append(testVec.Requests, hex.EncodeToString(reqBytes)) 126 | requests = append(requests, *req) 127 | } 128 | 129 | replies, err := protocol.CreateReplies(ver, requests, testMidpoint, testRadius, onlineCert) 130 | if err != nil { 131 | panic(err) 132 | } 133 | 134 | for i := 0; i < numRequestsPerBatch; i++ { 135 | _, _, err = protocol.VerifyReply(clientVersionPref, replies[i], rootPublicKey, requests[i].Nonce) 136 | if err != nil { 137 | panic(err) 138 | } 139 | 140 | testVec.Replies = append(testVec.Replies, hex.EncodeToString(replies[i])) 141 | } 142 | 143 | testVecBytes, err := json.Marshal(&testVec) 144 | if err != nil { 145 | panic(err) 146 | } 147 | 148 | f, err := os.Create(fmt.Sprintf("testdata/%s_%03d.json", fileNmameFor(ver), numRequestsPerBatch)) 149 | if err != nil { 150 | panic(err) 151 | } 152 | if _, err = f.Write(testVecBytes); err != nil { 153 | panic(err) 154 | } 155 | f.Close() 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /protocol/internal/testing/testing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloudflare, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package testing 16 | 17 | // TestVector encodes test vectors. 18 | type TestVector struct { 19 | // Info encodes some high level information about the test vector. 20 | Info string `json:"info"` 21 | 22 | // RootKey is the hex-encoded ed25519 key seed used to generate the root 23 | // key pair. 24 | RootKey string `json:"root_key"` 25 | 26 | // OnlineKey is the hex-encoded ed25519 key seed used to generate the 27 | // online key pair. 28 | OnlineKey string `json:"online_key"` 29 | 30 | // Requests is a sequence of hex-encoded requests. 31 | Requests []string `json:"request"` 32 | 33 | // Replies is a sequence of hex-encoded replies corresponding to 34 | // `Requests`. The requests are handled as a batch. 35 | Replies []string `json:"replies"` 36 | } 37 | 38 | // TestRand implements io/Reader using a fixed sequence of bytes. It is 39 | // intended to be used in place of crypto/Rand for tests that we want to be 40 | // deterministic. 41 | type TestRand struct { 42 | nextByte uint 43 | byteWrap uint 44 | } 45 | 46 | func (r *TestRand) Read(b []byte) (n int, err error) { 47 | for i := range b { 48 | b[i] = byte(r.nextByte) 49 | r.nextByte = (r.nextByte + 1) % r.byteWrap 50 | } 51 | return len(b), nil 52 | } 53 | 54 | // NewTestRand returns an instance of TestRand. 55 | func NewTestRand() *TestRand { 56 | return &TestRand{ 57 | nextByte: 0, 58 | // Pick a modulus that is not a multiple of the Roughtime nonce size so 59 | // that when generating many nonces at once we're likley to not have a 60 | // repeat. 61 | byteWrap: 253, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /protocol/protocol.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Roughtime Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. */ 14 | 15 | // Modifications copyright 2023 Cloudflare, Inc. 16 | // 17 | // The code has been extended to support IETF-Roughtime. 18 | 19 | // Package protocol implements the core of the Roughtime protocol. 20 | package protocol 21 | 22 | import ( 23 | "bytes" 24 | "crypto/ed25519" 25 | "crypto/sha512" 26 | "encoding/binary" 27 | "errors" 28 | "fmt" 29 | "io" 30 | "math" 31 | "sort" 32 | "time" 33 | ) 34 | 35 | const ( 36 | ietfRoughtimeFrame = "ROUGHTIM" 37 | maxNonceSize = sha512.Size 38 | 39 | // MinRequestSize is the minimum number of bytes in a request. 40 | MinRequestSize = 1024 41 | 42 | certificateContext = "RoughTime v1 delegation signature--\x00" 43 | signedResponseContext = "RoughTime v1 response signature\x00" 44 | ) 45 | 46 | // makeTag converts a four character string into a Roughtime tag value. 47 | func makeTag(tag string) uint32 { 48 | if len(tag) != 4 { 49 | panic("makeTag: len(tag) != 4: " + tag) 50 | } 51 | 52 | return uint32(tag[0]) | uint32(tag[1])<<8 | uint32(tag[2])<<16 | uint32(tag[3])<<24 53 | } 54 | 55 | var ( 56 | // Various tags used in the Roughtime protocol. 57 | tagCERT = makeTag("CERT") 58 | tagDELE = makeTag("DELE") 59 | tagINDX = makeTag("INDX") 60 | tagMAXT = makeTag("MAXT") 61 | tagMIDP = makeTag("MIDP") 62 | tagMINT = makeTag("MINT") 63 | tagNONC = makeTag("NONC") 64 | tagPAD = makeTag("PAD\xff") 65 | tagPATH = makeTag("PATH") 66 | tagPUBK = makeTag("PUBK") 67 | tagRADI = makeTag("RADI") 68 | tagROOT = makeTag("ROOT") 69 | tagSIG = makeTag("SIG\x00") 70 | tagSREP = makeTag("SREP") 71 | tagSRV = makeTag("SRV\x00") 72 | tagVER = makeTag("VER\x00") 73 | tagZZZZ = makeTag("ZZZZ") 74 | ) 75 | 76 | // tagsSlice is the type of an array of tags. It provides utility functions so 77 | // that they can be sorted. 78 | type tagsSlice []uint32 79 | 80 | func (t tagsSlice) Len() int { return len(t) } 81 | func (t tagsSlice) Less(i, j int) bool { return t[i] < t[j] } 82 | func (t tagsSlice) Swap(i, j int) { t[i], t[j] = t[j], t[i] } 83 | 84 | // Encode converts a map of tags to bytestrings into an encoded message. The 85 | // number of elements in msg and the sum of the lengths of all the bytestrings 86 | // must be ≤ 2**32. 87 | func Encode(msg map[uint32][]byte) ([]byte, error) { 88 | if len(msg) == 0 { 89 | return make([]byte, 4), nil 90 | } 91 | 92 | if len(msg) >= math.MaxInt32 { 93 | return nil, errors.New("encode: too many tags") 94 | } 95 | 96 | var payloadSum uint64 97 | for _, payload := range msg { 98 | if len(payload)%4 != 0 { 99 | return nil, errors.New("encode: length of value is not a multiple of four") 100 | } 101 | payloadSum += uint64(len(payload)) 102 | } 103 | if payloadSum >= 1<<32 { 104 | return nil, errors.New("encode: payloads too large") 105 | } 106 | 107 | tags := tagsSlice(make([]uint32, 0, len(msg))) 108 | for tag := range msg { 109 | tags = append(tags, tag) 110 | } 111 | sort.Sort(tags) 112 | 113 | numTags := uint64(len(tags)) 114 | 115 | encoded := make([]byte, 4*(1+numTags-1+numTags)+payloadSum) 116 | binary.LittleEndian.PutUint32(encoded, uint32(len(tags))) 117 | offsets := encoded[4:] 118 | tagBytes := encoded[4*(1+(numTags-1)):] 119 | payloads := encoded[4*(1+(numTags-1)+numTags):] 120 | 121 | currentOffset := uint32(0) 122 | 123 | for i, tag := range tags { 124 | payload := msg[tag] 125 | if i > 0 { 126 | binary.LittleEndian.PutUint32(offsets, currentOffset) 127 | offsets = offsets[4:] 128 | } 129 | 130 | binary.LittleEndian.PutUint32(tagBytes, tag) 131 | tagBytes = tagBytes[4:] 132 | 133 | if len(payload) > 0 { 134 | copy(payloads, payload) 135 | payloads = payloads[len(payload):] 136 | currentOffset += uint32(len(payload)) 137 | } 138 | } 139 | 140 | return encoded, nil 141 | } 142 | 143 | // Decode parses the output of encode back into a map of tags to bytestrings. 144 | func Decode(bytes []byte) (map[uint32][]byte, error) { 145 | if len(bytes) < 4 { 146 | return nil, errDecode("message too short to be valid") 147 | } 148 | if len(bytes)%4 != 0 { 149 | return nil, errDecode("message is not a multiple of four bytes") 150 | } 151 | 152 | numTags := uint64(binary.LittleEndian.Uint32(bytes)) 153 | 154 | if numTags == 0 { 155 | return make(map[uint32][]byte), nil 156 | } 157 | 158 | minLen := 4 * (1 + (numTags - 1) + numTags) 159 | 160 | if uint64(len(bytes)) < minLen { 161 | return nil, errDecode("message too short to be valid") 162 | } 163 | 164 | offsets := bytes[4:] 165 | tags := bytes[4*(1+numTags-1):] 166 | payloads := bytes[minLen:] 167 | 168 | if len(payloads) > math.MaxInt32 { 169 | return nil, errDecode("message too large") 170 | } 171 | payloadLength := uint32(len(payloads)) 172 | 173 | currentOffset := uint32(0) 174 | var lastTag uint32 175 | ret := make(map[uint32][]byte) 176 | 177 | for i := uint64(0); i < numTags; i++ { 178 | tag := binary.LittleEndian.Uint32(tags) 179 | tags = tags[4:] 180 | 181 | if i > 0 && lastTag >= tag { 182 | return nil, errDecode("tags out of order") 183 | } 184 | 185 | var nextOffset uint32 186 | if i < numTags-1 { 187 | nextOffset = binary.LittleEndian.Uint32(offsets) 188 | offsets = offsets[4:] 189 | } else { 190 | nextOffset = payloadLength 191 | } 192 | 193 | if nextOffset%4 != 0 { 194 | return nil, errDecode("payload length is not a multiple of four bytes") 195 | } 196 | 197 | if nextOffset < currentOffset { 198 | return nil, errDecode("offsets out of order") 199 | } 200 | 201 | length := nextOffset - currentOffset 202 | if uint32(len(payloads)) < length { 203 | return nil, errDecode("message truncated") 204 | } 205 | 206 | payload := payloads[:length] 207 | payloads = payloads[length:] 208 | ret[tag] = payload 209 | currentOffset = nextOffset 210 | lastTag = tag 211 | } 212 | 213 | return ret, nil 214 | } 215 | 216 | // messageOverhead returns the number of bytes needed for Encode to encode the 217 | // given number of tags. 218 | func messageOverhead(versionIETF bool, numTags int) int { 219 | framing := 0 220 | if versionIETF { 221 | framing = 12 // "ROUGHTIM" + message length (4 bytes) 222 | } 223 | return framing + 4*2*numTags 224 | } 225 | 226 | // nonceSize returns the nonce length. 227 | func nonceSize(versionIETF bool) int { 228 | if !versionIETF { 229 | return 64 230 | } 231 | return 32 232 | } 233 | 234 | // CalculateChainNonce fills the `nonce` buffer with the nonce used in the next 235 | // request in a chain given a reply and a blinding factor. The length of the 236 | // buffer is expected to match the nonce length for the protocol version. 237 | func CalculateChainNonce(nonce, prevReply, blind []byte) { 238 | var out [maxNonceSize]byte 239 | h := sha512.New() 240 | h.Write(prevReply) 241 | h.Sum(out[:0]) 242 | 243 | h.Reset() 244 | h.Write(out[:]) 245 | h.Write(blind) 246 | h.Sum(out[:0]) 247 | copy(nonce, out[:]) 248 | } 249 | 250 | // encodeFramed adds IETF message framing to a message. 251 | func encodeFramed(versionIETF bool, msg []byte) []byte { 252 | if versionIETF { 253 | framedMsg := make([]byte, 0, 12+len(msg)) 254 | framedMsg = append(framedMsg, ietfRoughtimeFrame...) 255 | framedMsg = binary.LittleEndian.AppendUint32(framedMsg, uint32(len(msg))) 256 | framedMsg = append(framedMsg, msg...) 257 | msg = framedMsg 258 | } 259 | 260 | return msg 261 | } 262 | 263 | // decodeFramed determines if the requester is a legacy client 264 | // (Google-Roughtime) or supports the IETF version. In the later case, it 265 | // removes the IETF message framing. 266 | func decodeFramed(req []byte) ([]byte, bool, error) { 267 | // In the IETF version of Roughtime, the first eight bytes of the datagram are 268 | // equal to "ROUGHTIM". In Google-Roughtime, the first four bytes encode the 269 | // number of tags. This is therefore a good distinguisher as long as "ROUG", 270 | // interpreted as a little-endian uint32, is not a valid number of tags. 271 | versionIETF := len(req) >= 8 && bytes.Equal(req[:8], []byte(ietfRoughtimeFrame)) 272 | 273 | if versionIETF { 274 | if len(req) < 8 { 275 | return nil, false, errDecode("request is too short to encode message frame") 276 | } 277 | req = req[8:] 278 | 279 | if len(req) < 4 { 280 | return nil, false, errDecode("request is too short to encode the message length") 281 | } 282 | roughtimeMessageLen := binary.LittleEndian.Uint32(req[:4]) 283 | req = req[4:] 284 | 285 | if len(req) != int(roughtimeMessageLen) { 286 | return nil, false, errDecode("message has unexpected length") 287 | } 288 | } 289 | 290 | return req, versionIETF, nil 291 | } 292 | 293 | func handleSRVTag(advertisedPreference []Version) bool { 294 | for _, version := range advertisedPreference { 295 | // The SRV tag is first defined in draft-ietf-ntp-roughtime-11 296 | if version == VersionDraft11 { 297 | return true 298 | } 299 | } 300 | return false 301 | } 302 | 303 | // CreateRequest creates a Roughtime request given an entropy source and the 304 | // contents of a previous reply for chaining. If this request is the first of a 305 | // chain, prevReply can be empty. It returns the nonce (needed to verify the 306 | // reply), the blind (needed to prove correct chaining to an external party) 307 | // and the request itself. 308 | func CreateRequest(versionPreference []Version, rand io.Reader, prevReply []byte, rootPublicKey ed25519.PublicKey) (nonce, blind []byte, request []byte, err error) { 309 | advertisedVersions, versionIETF, err := advertisedVersionsFromPreference(versionPreference) 310 | if err != nil { 311 | return nil, nil, nil, err 312 | } 313 | nonceSize := nonceSize(versionIETF) 314 | nonce = make([]byte, nonceSize) 315 | blind = make([]byte, nonceSize) 316 | if _, err := io.ReadFull(rand, blind); err != nil { 317 | return nil, nil, nil, err 318 | } 319 | 320 | CalculateChainNonce(nonce, prevReply, blind) 321 | 322 | // Construct the packet. 323 | packet := make(map[uint32][]byte) 324 | valuesLen := 0 325 | numTags := 0 326 | 327 | // NONC 328 | packet[tagNONC] = nonce 329 | valuesLen += len(nonce) 330 | numTags += 1 331 | 332 | // VER 333 | if versionIETF { 334 | encoded := make([]byte, 0, len(advertisedVersions)*4) 335 | for _, ver := range advertisedVersions { 336 | encoded = binary.LittleEndian.AppendUint32(encoded, uint32(ver)) 337 | } 338 | packet[tagVER] = encoded 339 | valuesLen += len(encoded) 340 | numTags += 1 341 | } 342 | 343 | // SRV 344 | if handleSRVTag(advertisedVersions) { 345 | srv := make([]byte, 0, 64) 346 | h := sha512.New() 347 | h.Write([]byte{0xff}) 348 | h.Write(rootPublicKey) 349 | h.Sum(srv) 350 | srv = srv[:32] 351 | packet[tagSRV] = srv 352 | valuesLen += len(nonce) 353 | numTags += 1 354 | } 355 | 356 | // Padding (PAD in Google-Roughtime or ZZZZ in the IETF version) 357 | var paddingTag uint32 358 | if versionIETF { 359 | paddingTag = tagZZZZ 360 | } else { 361 | paddingTag = tagPAD 362 | } 363 | padding := make([]byte, MinRequestSize-messageOverhead(versionIETF, numTags+1)-valuesLen) 364 | packet[paddingTag] = padding 365 | 366 | msg, err := Encode(packet) 367 | if err != nil { 368 | return nil, nil, nil, err 369 | } 370 | 371 | return nonce, blind, encodeFramed(versionIETF, msg), nil 372 | } 373 | 374 | // tree represents a Merkle tree of nonces. Each element of values is a layer 375 | // in the tree, with the widest layer first. 376 | type tree struct { 377 | values [][][maxNonceSize]byte 378 | } 379 | 380 | var ( 381 | hashLeafTweak = []byte{0} 382 | hashNodeTweak = []byte{1} 383 | ) 384 | 385 | // hashLeaf hashes an nonce to form the leaf of the Merkle tree. 386 | func hashLeaf(out *[maxNonceSize]byte, in []byte) { 387 | h := sha512.New() 388 | h.Write(hashLeafTweak) 389 | h.Write(in) 390 | h.Sum(out[:0]) 391 | } 392 | 393 | // hashNode hashes two child elements of the Merkle tree to produce an interior 394 | // node. 395 | func hashNode(out *[maxNonceSize]byte, left, right []byte) { 396 | h := sha512.New() 397 | h.Write(hashNodeTweak) 398 | h.Write(left) 399 | h.Write(right) 400 | h.Sum(out[:0]) 401 | } 402 | 403 | // newTree creates a Merkle tree given one or more nonces. 404 | func newTree(nonceSize int, requests []Request) *tree { 405 | if len(requests) == 0 { 406 | panic("newTree: passed empty slice") 407 | } 408 | 409 | levels := 1 410 | width := len(requests) 411 | for width > 1 { 412 | width = (width + 1) / 2 413 | levels++ 414 | } 415 | 416 | ret := &tree{ 417 | values: make([][][maxNonceSize]byte, 0, levels), 418 | } 419 | 420 | leaves := make([][maxNonceSize]byte, ((len(requests)+1)/2)*2) 421 | for i, req := range requests { 422 | var leaf [maxNonceSize]byte 423 | hashLeaf(&leaf, req.Nonce) 424 | leaves[i] = leaf 425 | } 426 | // Fill any extra leaves with an existing leaf, to simplify analysis 427 | // that we are not inadvertently signing other messages. 428 | for i := len(requests); i < len(leaves); i++ { 429 | leaves[i] = leaves[0] 430 | } 431 | ret.values = append(ret.values, leaves) 432 | 433 | for i := 1; i < levels; i++ { 434 | lastLevel := ret.values[i-1] 435 | width := len(lastLevel) / 2 436 | if width%2 == 1 { 437 | width++ 438 | } 439 | level := make([][maxNonceSize]byte, width) 440 | for j := 0; j < len(lastLevel)/2; j++ { 441 | hashNode(&level[j], lastLevel[j*2][:nonceSize], lastLevel[j*2+1][:nonceSize]) 442 | } 443 | // Fill the extra node with an existing node, to simplify 444 | // analysis that we are not inadvertently signing other 445 | // messages. 446 | if len(lastLevel)/2 < len(level) { 447 | level[len(lastLevel)/2] = level[0] 448 | } 449 | ret.values = append(ret.values, level) 450 | } 451 | 452 | return ret 453 | } 454 | 455 | // Root returns the root value of t. 456 | func (t *tree) Root() *[maxNonceSize]byte { 457 | return &t.values[len(t.values)-1][0] 458 | } 459 | 460 | // Levels returns the number of levels in t. 461 | func (t *tree) Levels() int { 462 | return len(t.values) 463 | } 464 | 465 | // Path returns elements from t needed to prove, given the root, that the leaf 466 | // at the given index is in the tree. 467 | func (t *tree) Path(index int) (path [][]byte) { 468 | path = make([][]byte, 0, len(t.values)) 469 | 470 | for level := 0; level < len(t.values)-1; level++ { 471 | if index%2 == 1 { 472 | path = append(path, t.values[level][index-1][:]) 473 | } else { 474 | path = append(path, t.values[level][index+1][:]) 475 | } 476 | 477 | index /= 2 478 | } 479 | 480 | return path 481 | } 482 | 483 | // Request is a request sent by a client. 484 | type Request struct { 485 | // Nonce is the request nonce. 486 | Nonce []byte 487 | // Nonce is the sequence of versions advertised by the client, ordered from 488 | // most to least preferred. 489 | Versions []Version 490 | // srv is the SRV tag indicating which root public key the client is 491 | // expecting to verify the response with. 492 | srv []byte 493 | } 494 | 495 | // ParseRequest resolves the supported versions indicated by the client and 496 | // parses the values required to produce a response. 497 | func ParseRequest(bytes []byte) (req *Request, err error) { 498 | if len(bytes) < MinRequestSize { 499 | return nil, errRequestLen 500 | } 501 | 502 | msg, versionIETF, err := decodeFramed(bytes) 503 | if err != nil { 504 | return nil, err 505 | } 506 | 507 | packet, err := Decode(msg) 508 | if err != nil { 509 | return nil, err 510 | } 511 | 512 | nonce, ok := packet[tagNONC] 513 | if !ok || len(nonce) != nonceSize(versionIETF) { 514 | return nil, errNonceLen 515 | } 516 | 517 | var versions []Version 518 | if versionIETF { 519 | encoded, ok := packet[tagVER] 520 | if !ok { 521 | return nil, errMissingVersion 522 | } 523 | for len(encoded) > 0 { 524 | if len(encoded) < 4 { 525 | return nil, errDecode("malformed version list") 526 | } 527 | 528 | ver := Version(binary.LittleEndian.Uint32(encoded[:4])) 529 | if ver.isSupported() { 530 | // De-duplicate any repeated version. 531 | ok := true 532 | for i := range versions { 533 | if versions[i] == ver { 534 | ok = false 535 | break 536 | } 537 | } 538 | if ok { 539 | versions = append(versions, ver) 540 | } 541 | } 542 | encoded = encoded[4:] 543 | } 544 | } else { 545 | versions = []Version{VersionGoogle} 546 | } 547 | 548 | var srv []byte 549 | if handleSRVTag(versions) { 550 | srv = packet[tagSRV] 551 | } 552 | 553 | return &Request{nonce, versions, srv}, nil 554 | } 555 | 556 | // CreateReplies signs, using privateKey, a batch of nonces along with the 557 | // given time and radius. It returns one reply for each nonce using that 558 | // signature and includes cert in each. 559 | // 560 | // The same version is indicated in each reply. It's the callers responsibility 561 | // to ensure that each client supports this version. Likewise, the server 562 | // indicated by each request, if any, must match the certificate. 563 | func CreateReplies(ver Version, requests []Request, midpoint time.Time, radius time.Duration, cert *Certificate) ([][]byte, error) { 564 | versionIETF := ver != VersionGoogle 565 | nonceSize := nonceSize(versionIETF) 566 | 567 | for i := range requests { 568 | if len(requests[i].srv) > 0 && !bytes.Equal(requests[i].srv, cert.srv) { 569 | return nil, fmt.Errorf("request %d indicates the wrong server", i) 570 | } 571 | } 572 | 573 | if len(requests) == 0 { 574 | return nil, nil 575 | } 576 | 577 | tree := newTree(nonceSize, requests) 578 | 579 | // Convert the midpoint and radius to their Roughtime representation. 580 | var midPointUint64 uint64 581 | var radiusUint32 uint32 582 | if versionIETF { 583 | midPointUint64 = uint64(midpoint.Unix()) 584 | radiusUint32 = uint32(radius.Seconds()) 585 | } else { 586 | midPointUint64 = uint64(midpoint.UnixMicro()) 587 | radiusUint32 = uint32(radius.Microseconds()) 588 | } 589 | 590 | var midpointBytes [8]byte 591 | binary.LittleEndian.PutUint64(midpointBytes[:], midPointUint64) 592 | var radiusBytes [4]byte 593 | binary.LittleEndian.PutUint32(radiusBytes[:], radiusUint32) 594 | 595 | signedReply := map[uint32][]byte{ 596 | tagMIDP: midpointBytes[:], 597 | tagRADI: radiusBytes[:], 598 | tagROOT: tree.Root()[:nonceSize], 599 | } 600 | 601 | signedReplyBytes, err := Encode(signedReply) 602 | if err != nil { 603 | return nil, err 604 | } 605 | 606 | toBeSigned := signedResponseContext + string(signedReplyBytes) 607 | sig := ed25519.Sign(cert.onlinePrivateKey, []byte(toBeSigned)) 608 | 609 | reply := map[uint32][]byte{ 610 | tagSREP: signedReplyBytes, 611 | tagSIG: sig, 612 | tagCERT: cert.BytesForVersion(ver), 613 | } 614 | 615 | if versionIETF { 616 | encoded := make([]byte, 0, 4) 617 | encoded = binary.LittleEndian.AppendUint32(encoded, uint32(ver)) 618 | reply[tagVER] = encoded 619 | } 620 | 621 | replies := make([][]byte, 0, len(requests)) 622 | 623 | for i := range requests { 624 | var indexBytes [4]byte 625 | binary.LittleEndian.PutUint32(indexBytes[:], uint32(i)) 626 | reply[tagINDX] = indexBytes[:] 627 | 628 | reply[tagNONC] = requests[i].Nonce 629 | 630 | path := tree.Path(i) 631 | pathBytes := make([]byte, 0, nonceSize*len(path)) 632 | for _, pathStep := range path { 633 | pathBytes = append(pathBytes, pathStep[:nonceSize]...) 634 | } 635 | reply[tagPATH] = pathBytes 636 | 637 | replyBytes, err := Encode(reply) 638 | if err != nil { 639 | return nil, err 640 | } 641 | 642 | replies = append(replies, encodeFramed(versionIETF, replyBytes)) 643 | } 644 | 645 | return replies, nil 646 | } 647 | 648 | type Certificate struct { 649 | // googleBytes is the certificate we send to legacy clients 650 | // (Google-Roughtime). The MINT and MAXT fields encode timestamps in 651 | // microseconds. 652 | googleBytes []byte 653 | bytes []byte 654 | 655 | // onlinePrivateKey is the online private key. 656 | onlinePrivateKey ed25519.PrivateKey 657 | 658 | // srv is the payload of the SRV tag that the client would send to indicate 659 | // the root public key delegated by this certificate. 660 | srv []byte 661 | } 662 | 663 | // BytesForVersion returns a serialized certificate compatible with the given 664 | // version. Legacy clients (Google-Roughtime) expect a non-standard encoding of 665 | // the MINT and MAXT fields. 666 | func (cert *Certificate) BytesForVersion(ver Version) []byte { 667 | switch ver { 668 | case VersionGoogle: 669 | return cert.googleBytes 670 | default: 671 | return cert.bytes 672 | } 673 | } 674 | 675 | // Select a certificate suitable for responding to the request. 676 | func SelectCertificateForRequest(req *Request, certs []Certificate) *Certificate { 677 | // Return the first certificate for which the root public key was indicated 678 | // by the client. 679 | for _, cert := range certs { 680 | if bytes.Equal(req.srv, cert.srv) { 681 | return &cert 682 | } 683 | } 684 | 685 | // If no SRV tag was sent, then guess the first certificate. 686 | if len(req.srv) == 0 && len(certs) > 0 { 687 | return &certs[0] 688 | } 689 | 690 | // The SRV tag indicates an unknown root public key, or the certificate 691 | // list is empty. 692 | return nil 693 | } 694 | 695 | // NewCertificate returns a signed certificate, using rootPrivateKey, 696 | // delegating authority for the given timestamp to onlinePrivateKey. 697 | func NewCertificate(minTime, maxTime time.Time, onlinePrivateKey, rootPrivateKey ed25519.PrivateKey) (cert *Certificate, err error) { 698 | if maxTime.Before(minTime) { 699 | return nil, errors.New("protocol: maxTime < minTime") 700 | } 701 | 702 | certs := make([][]byte, 2) 703 | for i, t := range []struct { 704 | minTime uint64 705 | maxTime uint64 706 | }{ 707 | // Legacy (Google-Roughtime) 708 | { 709 | minTime: uint64(minTime.UnixMicro()), 710 | maxTime: uint64(maxTime.UnixMicro()), 711 | }, 712 | // IETF 713 | { 714 | minTime: uint64(minTime.Unix()), 715 | maxTime: uint64(maxTime.Unix()), 716 | }, 717 | } { 718 | var minTimeBytes, maxTimeBytes [8]byte 719 | binary.LittleEndian.PutUint64(minTimeBytes[:], t.minTime) 720 | binary.LittleEndian.PutUint64(maxTimeBytes[:], t.maxTime) 721 | 722 | signed := map[uint32][]byte{ 723 | tagPUBK: ed25519.PrivateKey(onlinePrivateKey).Public().(ed25519.PublicKey), 724 | tagMINT: minTimeBytes[:], 725 | tagMAXT: maxTimeBytes[:], 726 | } 727 | 728 | signedBytes, err := Encode(signed) 729 | if err != nil { 730 | return nil, err 731 | } 732 | 733 | toBeSigned := certificateContext + string(signedBytes) 734 | sig := ed25519.Sign(rootPrivateKey, []byte(toBeSigned)) 735 | 736 | cert := map[uint32][]byte{ 737 | tagSIG: sig, 738 | tagDELE: signedBytes, 739 | } 740 | 741 | certs[i], err = Encode(cert) 742 | if err != nil { 743 | return nil, err 744 | } 745 | } 746 | 747 | // SRV 748 | srv := make([]byte, 0, 64) 749 | h := sha512.New() 750 | h.Write([]byte{0xff}) 751 | h.Write(ed25519.PrivateKey(rootPrivateKey).Public().(ed25519.PublicKey)) 752 | h.Sum(srv) 753 | srv = srv[:32] 754 | 755 | return &Certificate{ 756 | googleBytes: certs[0], 757 | bytes: certs[1], 758 | onlinePrivateKey: onlinePrivateKey, 759 | srv: srv, 760 | }, nil 761 | } 762 | 763 | func getValue(msg map[uint32][]byte, tag uint32, name string) (value []byte, err error) { 764 | value, ok := msg[tag] 765 | if !ok { 766 | return nil, errors.New("protocol: missing " + name) 767 | } 768 | return value, nil 769 | } 770 | 771 | func getFixedLength(msg map[uint32][]byte, tag uint32, name string, length int) (value []byte, err error) { 772 | value, err = getValue(msg, tag, name) 773 | if err != nil { 774 | return nil, err 775 | } 776 | if len(value) != length { 777 | return nil, errors.New("protocol: incorrect length for " + name) 778 | } 779 | return value, nil 780 | } 781 | 782 | func getUint32(msg map[uint32][]byte, tag uint32, name string) (result uint32, err error) { 783 | valueBytes, err := getFixedLength(msg, tag, name, 4) 784 | if err != nil { 785 | return 0, err 786 | } 787 | return binary.LittleEndian.Uint32(valueBytes), nil 788 | } 789 | 790 | func getUint64(msg map[uint32][]byte, tag uint32, name string) (result uint64, err error) { 791 | valueBytes, err := getFixedLength(msg, tag, name, 8) 792 | if err != nil { 793 | return 0, err 794 | } 795 | return binary.LittleEndian.Uint64(valueBytes), nil 796 | } 797 | 798 | func getSubmessage(msg map[uint32][]byte, tag uint32, name string) (result map[uint32][]byte, err error) { 799 | valueBytes, err := getValue(msg, tag, name) 800 | if err != nil { 801 | return nil, err 802 | } 803 | 804 | result, err = Decode(valueBytes) 805 | if err != nil { 806 | return nil, errors.New("protocol: failed to parse " + name + ": " + err.Error()) 807 | } 808 | 809 | return result, nil 810 | } 811 | 812 | // VerifyReply parses the Roughtime reply in replyBytes, authenticates it using 813 | // publicKey and verifies that nonce is included in it. It returns the included 814 | // timestamp and radius. 815 | func VerifyReply(versionPreference []Version, replyBytes, publicKey []byte, nonce []byte) (midp time.Time, radi time.Duration, err error) { 816 | advertisedVersions, versionIETF, err := advertisedVersionsFromPreference(versionPreference) 817 | if err != nil { 818 | return midp, radi, err 819 | } 820 | nonceSize := nonceSize(versionIETF) 821 | 822 | unframedReply, _, err := decodeFramed(replyBytes) 823 | if err != nil { 824 | return midp, radi, err 825 | } 826 | 827 | reply, err := Decode(unframedReply) 828 | if err != nil { 829 | return midp, radi, errors.New("protocol: failed to parse top-level reply: " + err.Error()) 830 | } 831 | 832 | // Make sure the version selected by the server matches one that we advertised. 833 | var responseVer Version 834 | if versionIETF { 835 | encoded, ok := reply[tagVER] 836 | if !ok { 837 | return midp, radi, errMissingVersion 838 | } 839 | if len(encoded) != 4 { 840 | return midp, radi, errDecode("malformed version") 841 | } 842 | 843 | responseVer = Version(binary.LittleEndian.Uint32(encoded[:])) 844 | } else { 845 | responseVer = VersionGoogle 846 | } 847 | versionOK := false 848 | for _, ver := range advertisedVersions { 849 | if responseVer == ver { 850 | versionOK = true 851 | break 852 | } 853 | } 854 | if !versionOK { 855 | return midp, radi, errUnsupportedVersion([]Version{responseVer}) 856 | } 857 | 858 | // Make sure the NONC tag is present and indicates the same nonce as sent in 859 | // request. 860 | if versionIETF { 861 | responseNONC, ok := reply[tagNONC] 862 | if !ok { 863 | return midp, radi, errors.New("protocol: response is missing NONC tag") 864 | } 865 | if !bytes.Equal(nonce, responseNONC) { 866 | return midp, radi, errors.New("protocol: nonce in responce is different from nonce in request") 867 | } 868 | } 869 | 870 | cert, err := getSubmessage(reply, tagCERT, "certificate") 871 | if err != nil { 872 | return midp, radi, err 873 | } 874 | 875 | signatureBytes, err := getFixedLength(cert, tagSIG, "signature", ed25519.SignatureSize) 876 | if err != nil { 877 | return midp, radi, err 878 | } 879 | 880 | delegationBytes, err := getValue(cert, tagDELE, "delegation") 881 | if err != nil { 882 | return midp, radi, err 883 | } 884 | 885 | if !ed25519.Verify(publicKey, []byte(certificateContext+string(delegationBytes)), signatureBytes) { 886 | return midp, radi, errors.New("protocol: invalid delegation signature") 887 | } 888 | 889 | delegation, err := Decode(delegationBytes) 890 | if err != nil { 891 | return midp, radi, errors.New("protocol: failed to parse delegation: " + err.Error()) 892 | } 893 | 894 | minTime, err := getUint64(delegation, tagMINT, "minimum time") 895 | if err != nil { 896 | return midp, radi, err 897 | } 898 | 899 | maxTime, err := getUint64(delegation, tagMAXT, "maximum time") 900 | if err != nil { 901 | return midp, radi, err 902 | } 903 | 904 | delegatedPublicKey, err := getFixedLength(delegation, tagPUBK, "public key", ed25519.PublicKeySize) 905 | if err != nil { 906 | return midp, radi, err 907 | } 908 | 909 | responseSigBytes, err := getFixedLength(reply, tagSIG, "signature", ed25519.SignatureSize) 910 | if err != nil { 911 | return midp, radi, err 912 | } 913 | 914 | signedResponseBytes, ok := reply[tagSREP] 915 | if !ok { 916 | return midp, radi, errors.New("protocol: response is missing signed portion") 917 | } 918 | 919 | if !ed25519.Verify(delegatedPublicKey, []byte(signedResponseContext+string(signedResponseBytes)), responseSigBytes) { 920 | return midp, radi, errors.New("protocol: invalid response signature") 921 | } 922 | 923 | signedResponse, err := Decode(signedResponseBytes) 924 | if err != nil { 925 | return midp, radi, errors.New("protocol: failed to parse signed response: " + err.Error()) 926 | } 927 | 928 | root, err := getFixedLength(signedResponse, tagROOT, "root", nonceSize) 929 | if err != nil { 930 | return midp, radi, err 931 | } 932 | 933 | midpoint, err := getUint64(signedResponse, tagMIDP, "midpoint") 934 | if err != nil { 935 | return midp, radi, err 936 | } 937 | 938 | radius, err := getUint32(signedResponse, tagRADI, "radius") 939 | if err != nil { 940 | return midp, radi, err 941 | } 942 | 943 | if maxTime < minTime { 944 | return midp, radi, errors.New("protocol: invalid delegation range") 945 | } 946 | 947 | if midpoint < minTime || maxTime < midpoint { 948 | return midp, radi, errors.New("protocol: timestamp out of range for delegation") 949 | } 950 | 951 | index, err := getUint32(reply, tagINDX, "index") 952 | if err != nil { 953 | return midp, radi, err 954 | } 955 | 956 | path, err := getValue(reply, tagPATH, "path") 957 | if err != nil { 958 | return midp, radi, err 959 | } 960 | if len(path)%nonceSize != 0 { 961 | return midp, radi, errors.New("protocol: path is not a multiple of the hash size") 962 | } 963 | 964 | var hash [maxNonceSize]byte 965 | hashLeaf(&hash, nonce) 966 | 967 | for len(path) > 0 { 968 | pathElementIsRight := index&1 == 0 969 | if pathElementIsRight { 970 | hashNode(&hash, hash[:nonceSize], path[:nonceSize]) 971 | } else { 972 | hashNode(&hash, path[:nonceSize], hash[:nonceSize]) 973 | } 974 | 975 | index >>= 1 976 | path = path[nonceSize:] 977 | } 978 | 979 | if !bytes.Equal(hash[:nonceSize], root) { 980 | return midp, radi, errors.New("protocol: calculated tree root doesn't match signed root") 981 | } 982 | 983 | if versionIETF { 984 | midp = time.Unix(int64(midpoint), 0) 985 | radi = time.Duration(radius) * time.Second 986 | } else { 987 | midp = time.UnixMicro(int64(midpoint)) 988 | radi = time.Duration(radius) * time.Microsecond 989 | } 990 | return 991 | } 992 | -------------------------------------------------------------------------------- /protocol/protocol_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Roughtime Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. */ 14 | 15 | // Modifications copyright 2023 Cloudflare, Inc. 16 | // 17 | // The code has been extended to support IETF-Roughtime. 18 | 19 | //go:generate go run ./internal/cmd/gen_test_vectors.go 20 | 21 | package protocol 22 | 23 | import ( 24 | "bytes" 25 | "crypto/ed25519" 26 | "crypto/rand" 27 | "encoding/hex" 28 | "encoding/json" 29 | "fmt" 30 | "io" 31 | "os" 32 | "testing" 33 | "testing/quick" 34 | "time" 35 | 36 | protocolTesting "github.com/cloudflare/roughtime/protocol/internal/testing" 37 | ) 38 | 39 | var ( 40 | testMinTime = time.Unix(0, 0) 41 | testMaxTime = time.Unix(100, 0) 42 | testMidpoint = time.Unix(50, 0) 43 | testRadius = time.Duration(5) * time.Second 44 | ) 45 | 46 | func testEncodeDecodeRoundtrip(msg map[uint32][]byte) bool { 47 | encoded, err := Encode(msg) 48 | if err != nil { 49 | return true 50 | } 51 | 52 | decoded, err := Decode(encoded) 53 | if err != nil { 54 | return false 55 | } 56 | 57 | if len(msg) != len(decoded) { 58 | return false 59 | } 60 | 61 | for tag, payload := range msg { 62 | otherPayload, ok := decoded[tag] 63 | if !ok { 64 | return false 65 | } 66 | if !bytes.Equal(payload, otherPayload) { 67 | return false 68 | } 69 | } 70 | 71 | return true 72 | } 73 | 74 | func TestEncodeDecode(t *testing.T) { 75 | if err := quick.Check(testEncodeDecodeRoundtrip, &quick.Config{ 76 | MaxCountScale: 10, 77 | }); err != nil { 78 | t.Fatal(err) 79 | } 80 | } 81 | 82 | func TestRequestSize(t *testing.T) { 83 | rootPublicKey := make([]byte, 32) 84 | for _, ver := range allVersions { 85 | t.Run(ver.String(), func(t *testing.T) { 86 | _, _, request, err := CreateRequest([]Version{ver}, rand.Reader, nil, rootPublicKey) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | if len(request) != MinRequestSize { 91 | t.Errorf("got %d byte request, want %d bytes", len(request), MinRequestSize) 92 | } 93 | }) 94 | } 95 | } 96 | 97 | func createServerIdentity(t *testing.T) (cert *Certificate, rootPublicKey []byte) { 98 | rootPublicKey, rootPrivateKey, err := ed25519.GenerateKey(rand.Reader) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | 103 | _, onlinePrivateKey, err := ed25519.GenerateKey(rand.Reader) 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | 108 | if cert, err = NewCertificate(testMinTime, testMaxTime, onlinePrivateKey, rootPrivateKey); err != nil { 109 | t.Fatal(err) 110 | } 111 | 112 | return cert, rootPublicKey 113 | } 114 | 115 | func TestRunTestVectors(t *testing.T) { 116 | for _, fileName := range []string{ 117 | "testdata/roughtime_ietf_draft08_001.json", 118 | "testdata/roughtime_ietf_draft08_010.json", 119 | "testdata/roughtime_ietf_draft08_100.json", 120 | "testdata/roughtime_ietf_draft11_001.json", 121 | "testdata/roughtime_ietf_draft11_010.json", 122 | "testdata/roughtime_ietf_draft11_100.json", 123 | "testdata/roughtime_google_001.json", 124 | "testdata/roughtime_google_010.json", 125 | "testdata/roughtime_google_100.json", 126 | } { 127 | f, err := os.Open(fileName) 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | 132 | testVecBytes, err := io.ReadAll(f) 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | 137 | var testVec protocolTesting.TestVector 138 | if err = json.Unmarshal(testVecBytes, &testVec); err != nil { 139 | t.Fatal(err) 140 | } 141 | 142 | t.Run(testVec.Info, func(t *testing.T) { 143 | rootKeySeed, err := hex.DecodeString(testVec.RootKey) 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | rootPrivateKey := ed25519.NewKeyFromSeed(rootKeySeed) 148 | rootPublicKey := rootPrivateKey.Public().(ed25519.PublicKey) 149 | 150 | onlineKeySeed, err := hex.DecodeString(testVec.OnlineKey) 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | onlinePrivateKey := ed25519.NewKeyFromSeed(onlineKeySeed) 155 | onlineCert, err := NewCertificate(testMinTime, testMaxTime, onlinePrivateKey, rootPrivateKey) 156 | if err != nil { 157 | panic(err) 158 | } 159 | 160 | requests := make([]Request, 0) 161 | advertisedVersions := make(map[Version]uint) 162 | for _, ver := range allVersions { 163 | advertisedVersions[ver] = 0 164 | } 165 | for _, request := range testVec.Requests { 166 | requestBytes, err := hex.DecodeString(request) 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | 171 | req, err := ParseRequest(requestBytes) 172 | if err != nil { 173 | t.Fatal(err) 174 | } 175 | 176 | for _, ver := range req.Versions { 177 | advertisedVersions[ver] += 1 178 | } 179 | 180 | requests = append(requests, *req) 181 | } 182 | 183 | supportedVersions := make([]Version, 0, len(allVersions)) 184 | for ver, advertisementCount := range advertisedVersions { 185 | if advertisementCount > 0 { 186 | supportedVersions = append(supportedVersions, ver) 187 | } 188 | } 189 | responseVer, err := ResponseVersionFromSupported(supportedVersions) 190 | if err != nil { 191 | t.Fatal(err) 192 | } 193 | 194 | replies, err := CreateReplies(responseVer, requests, testMidpoint, testRadius, onlineCert) 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | 199 | // Infer the client's configuration. (The client must have supported the response version.) 200 | for i := range replies { 201 | expectedReply, err := hex.DecodeString(testVec.Replies[i]) 202 | if err != nil { 203 | t.Fatal(err) 204 | } 205 | 206 | // Check that the replies match the test vector. 207 | if !bytes.Equal(replies[i], expectedReply) { 208 | t.Error("unexpected reply") 209 | } 210 | 211 | // Make sure the responses verify properly. 212 | _, _, err = VerifyReply([]Version{responseVer}, replies[i], rootPublicKey, requests[i].Nonce) 213 | if err != nil { 214 | t.Error(err) 215 | } 216 | } 217 | }) 218 | } 219 | } 220 | 221 | func TestRoundtrip(t *testing.T) { 222 | cert, rootPublicKey := createServerIdentity(t) 223 | 224 | for _, ver := range allVersions { 225 | t.Run(ver.String(), func(t *testing.T) { 226 | for _, numRequests := range []int{1, 2, 3, 4, 5, 15, 16, 17} { 227 | advertisedVersions := make(map[Version]uint) 228 | for _, ver := range allVersions { 229 | advertisedVersions[ver] = 0 230 | } 231 | 232 | requests := make([]Request, 0, numRequests) 233 | for i := 0; i < numRequests; i++ { 234 | nonceSent, _, request, err := CreateRequest([]Version{ver}, rand.Reader, nil, rootPublicKey) 235 | if err != nil { 236 | panic(err) 237 | } 238 | 239 | req, err := ParseRequest(request) 240 | if err != nil { 241 | t.Fatal(err) 242 | } 243 | 244 | if !bytes.Equal(nonceSent, req.Nonce) { 245 | t.Fatal("received nonce does not match sent") 246 | } 247 | 248 | for _, ver := range req.Versions { 249 | advertisedVersions[ver] += 1 250 | } 251 | 252 | requests = append(requests, *req) 253 | } 254 | 255 | supportedVersions := make([]Version, 0, len(allVersions)) 256 | for ver, advertisementCount := range advertisedVersions { 257 | if advertisementCount > 0 { 258 | supportedVersions = append(supportedVersions, ver) 259 | } 260 | } 261 | responseVer, err := ResponseVersionFromSupported(supportedVersions) 262 | if err != nil { 263 | t.Fatal(err) 264 | } 265 | 266 | replies, err := CreateReplies(responseVer, requests, testMidpoint, testRadius, cert) 267 | if err != nil { 268 | t.Fatal(err) 269 | } 270 | 271 | if len(replies) != len(requests) { 272 | t.Fatalf("received %d replies for %d nonces", len(replies), len(requests)) 273 | } 274 | 275 | for i, reply := range replies { 276 | midpoint, radius, err := VerifyReply([]Version{responseVer}, reply, rootPublicKey, requests[i].Nonce) 277 | if err != nil { 278 | t.Errorf("error parsing reply #%d: %s", i, err) 279 | continue 280 | } 281 | 282 | if midpoint != testMidpoint { 283 | t.Errorf("reply #%d gave a midpoint of %v, want %v", i, midpoint, testMidpoint) 284 | } 285 | if radius != testRadius { 286 | t.Errorf("reply #%d gave a radius of %d, want %d", i, radius, testRadius) 287 | } 288 | } 289 | } 290 | }) 291 | } 292 | } 293 | 294 | func TestChaining(t *testing.T) { 295 | // This test demonstrates how a claim of misbehaviour from a client 296 | // would be checked. The client creates a two element chain in this 297 | // example where the first server says that the time is 10 and the 298 | // second says that it's 5. 299 | certA, rootPublicKeyA := createServerIdentity(t) 300 | certB, rootPublicKeyB := createServerIdentity(t) 301 | 302 | for _, ver := range allVersions { 303 | t.Run(ver.String(), func(t *testing.T) { 304 | _, _, req1Bytes, err := CreateRequest([]Version{ver}, rand.Reader, nil, rootPublicKeyA) 305 | if err != nil { 306 | t.Fatal(err) 307 | } 308 | 309 | req1, err := ParseRequest(req1Bytes) 310 | if err != nil { 311 | t.Fatal(err) 312 | } 313 | 314 | replies1, err := CreateReplies(ver, []Request{*req1}, testMidpoint, testRadius, certA) 315 | if err != nil { 316 | t.Fatal(err) 317 | } 318 | 319 | _, blind2, req2Bytes, err := CreateRequest([]Version{ver}, rand.Reader, replies1[0], rootPublicKeyB) 320 | if err != nil { 321 | t.Fatal(err) 322 | } 323 | 324 | req2, err := ParseRequest(req2Bytes) 325 | if err != nil { 326 | t.Fatal(err) 327 | } 328 | 329 | replies2, err := CreateReplies(ver, []Request{*req2}, testMidpoint.Add(time.Duration(-10)*time.Second), testRadius, certB) 330 | if err != nil { 331 | t.Fatal(err) 332 | } 333 | 334 | // The client would present a series of tuples of (server identity, 335 | // nonce/blind, reply) as its claim of misbehaviour. The first element 336 | // contains a nonce where as all other elements contain just the 337 | // blinding value, as the nonce used for that request is calculated 338 | // from that and the previous reply. 339 | type claimStep struct { 340 | serverPublicKey []byte 341 | nonceOrBlind []byte 342 | reply []byte 343 | } 344 | 345 | claim := []claimStep{ 346 | {rootPublicKeyA, req1.Nonce, replies1[0]}, 347 | {rootPublicKeyB, blind2, replies2[0]}, 348 | } 349 | 350 | // In order to verify a claim, one would check each of the replies 351 | // based on the calculated nonce. 352 | var lastMidpoint time.Time 353 | var misbehaviourFound bool 354 | for i, step := range claim { 355 | nonce := make([]byte, len(step.nonceOrBlind)) 356 | if i == 0 { 357 | copy(nonce[:], step.nonceOrBlind[:]) 358 | } else { 359 | CalculateChainNonce(nonce, claim[i-1].reply, step.nonceOrBlind) 360 | } 361 | midpoint, _, err := VerifyReply([]Version{ver}, step.reply, step.serverPublicKey, nonce) 362 | if err != nil { 363 | t.Fatal(err) 364 | } 365 | 366 | // This example doesn't take the radius into account. 367 | if i > 0 && midpoint.Before(lastMidpoint) { 368 | misbehaviourFound = true 369 | } 370 | lastMidpoint = midpoint 371 | } 372 | 373 | if !misbehaviourFound { 374 | t.Error("did not find expected misbehaviour") 375 | } 376 | }) 377 | } 378 | } 379 | 380 | // Test that tag constants match values in the tag registry (draft-ietf-ntp-roughtime-08, Section 12.3) 381 | func TestIETFTags(t *testing.T) { 382 | for _, tc := range []struct { 383 | name string 384 | got uint32 385 | want uint32 386 | }{ 387 | { 388 | name: "SRV", 389 | got: tagSRV, 390 | want: 0x00565253, 391 | }, 392 | { 393 | name: "VER", 394 | got: tagVER, 395 | want: 0x00524556, 396 | }, 397 | { 398 | name: "ZZZZ", 399 | got: tagZZZZ, 400 | want: 0x5a5a5a5a, 401 | }, 402 | } { 403 | if tc.got != tc.want { 404 | t.Errorf("%s mismatch: got %04x; want %04x", tc.name, tc.got, tc.want) 405 | } 406 | } 407 | } 408 | 409 | func TestServerIgnoresUnrecognizedVersions(t *testing.T) { 410 | rootPublicKey := make([]byte, 32) 411 | for _, ver := range ietfVersions { 412 | t.Run(ver.String(), func(t *testing.T) { 413 | _, _, request, err := CreateRequest([]Version{0x1234578, ver, 0xffffffff, ver}, rand.Reader, nil, rootPublicKey) 414 | if err != nil { 415 | t.Fatal(err) 416 | } 417 | 418 | req, err := ParseRequest(request) 419 | if err != nil { 420 | t.Fatal(err) 421 | } 422 | 423 | if len(req.Versions) != 1 || req.Versions[0] != ver { 424 | t.Fatal("unexpected version") 425 | } 426 | }) 427 | } 428 | } 429 | 430 | // Test that if no version preference is specified, then the client defaults to 431 | // IETF-Roughtime. 432 | func TestEmptyVersionPreference(t *testing.T) { 433 | advertisedVersions, versionIETF, err := advertisedVersionsFromPreference(nil) 434 | if err != nil { 435 | t.Fatal(err) 436 | } 437 | 438 | if versionIETF != true { 439 | t.Fatal("versionIETF: got false; want true") 440 | } 441 | 442 | if len(advertisedVersions) != len(ietfVersions) { 443 | t.Fatalf("len(advertisedVersions): got %d; want %d", len(advertisedVersions), len(ietfVersions)) 444 | } 445 | 446 | for i := range advertisedVersions { 447 | if advertisedVersions[i] != ietfVersions[i] { 448 | t.Fatalf("advertisedVersions: got %q; want %q", advertisedVersions, ietfVersions) 449 | } 450 | } 451 | } 452 | 453 | func TestSelectCertificateForRequest(t *testing.T) { 454 | certs := make([]Certificate, 0) 455 | rootPublicKeys := make([]ed25519.PublicKey, 0) 456 | for i := 0; i < 4; i++ { 457 | cert, rootPublicKey := createServerIdentity(t) 458 | certs = append(certs, *cert) 459 | rootPublicKeys = append(rootPublicKeys, rootPublicKey) 460 | 461 | } 462 | 463 | for i := range certs { 464 | t.Run(fmt.Sprintf("select_server_%v", i), func(t *testing.T) { 465 | _, _, reqBytes, err := CreateRequest([]Version{}, rand.Reader, nil, rootPublicKeys[i]) 466 | if err != nil { 467 | t.Fatal(err) 468 | } 469 | 470 | req, err := ParseRequest(reqBytes) 471 | if err != nil { 472 | t.Fatal(err) 473 | } 474 | 475 | selectedCert := SelectCertificateForRequest(req, certs) 476 | if selectedCert == nil { 477 | t.Fatal("no certificate selected") 478 | } 479 | 480 | if !bytes.Equal(selectedCert.srv, certs[i].srv) { 481 | t.Fatalf("selected the wrong certificate") 482 | } 483 | }) 484 | } 485 | 486 | t.Run("unknown_server", func(t *testing.T) { 487 | _, unknownRootPublicKey := createServerIdentity(t) 488 | 489 | _, _, reqBytes, err := CreateRequest([]Version{}, rand.Reader, nil, unknownRootPublicKey) 490 | if err != nil { 491 | t.Fatal(err) 492 | } 493 | 494 | req, err := ParseRequest(reqBytes) 495 | if err != nil { 496 | t.Fatal(err) 497 | } 498 | 499 | selectedCert := SelectCertificateForRequest(req, certs) 500 | if selectedCert != nil { 501 | t.Fatal("certificate selected for unknown server") 502 | } 503 | }) 504 | 505 | t.Run("empty_cert_list", func(t *testing.T) { 506 | _, someRootPublicKey := createServerIdentity(t) 507 | 508 | _, _, reqBytes, err := CreateRequest([]Version{}, rand.Reader, nil, someRootPublicKey) 509 | if err != nil { 510 | t.Fatal(err) 511 | } 512 | 513 | req, err := ParseRequest(reqBytes) 514 | if err != nil { 515 | t.Fatal(err) 516 | } 517 | 518 | selectedCert := SelectCertificateForRequest(req, []Certificate{}) 519 | if selectedCert != nil { 520 | t.Fatal("certificate selected from empty list") 521 | } 522 | 523 | }) 524 | 525 | for i := range certs { 526 | t.Run(fmt.Sprintf("tag_not_sent_server_%v", i), func(t *testing.T) { 527 | _, _, reqBytes, err := CreateRequest([]Version{VersionDraft08}, rand.Reader, nil, rootPublicKeys[i]) 528 | if err != nil { 529 | t.Fatal(err) 530 | } 531 | 532 | req, err := ParseRequest(reqBytes) 533 | if err != nil { 534 | t.Fatal(err) 535 | } 536 | 537 | selectedCert := SelectCertificateForRequest(req, certs) 538 | if selectedCert == nil { 539 | t.Fatal("no certificate selected") 540 | } 541 | 542 | // No SRV tag was sent, so expect the server to select the first. 543 | if !bytes.Equal(selectedCert.srv, certs[0].srv) { 544 | t.Fatalf("selected the wrong certificate") 545 | } 546 | }) 547 | } 548 | } 549 | 550 | func TestCreateReplyForIncorrectCertificate(t *testing.T) { 551 | _, unknownRootPublicKey := createServerIdentity(t) 552 | cert, _ := createServerIdentity(t) 553 | 554 | _, _, reqBytes, err := CreateRequest(nil, rand.Reader, nil, unknownRootPublicKey) 555 | if err != nil { 556 | t.Fatal(err) 557 | } 558 | 559 | req, err := ParseRequest(reqBytes) 560 | if err != nil { 561 | t.Fatal(err) 562 | } 563 | 564 | _, err = CreateReplies(VersionDraft11, []Request{*req}, time.Now(), 0, cert) 565 | if err == nil { 566 | t.Error("expected failure") 567 | } 568 | 569 | } 570 | 571 | func FuzzParseRequest(f *testing.F) { 572 | for _, fileName := range []string{ 573 | "testdata/roughtime_ietf_draft08_001.json", 574 | "testdata/roughtime_ietf_draft08_010.json", 575 | "testdata/roughtime_ietf_draft08_100.json", 576 | "testdata/roughtime_ietf_draft11_001.json", 577 | "testdata/roughtime_ietf_draft11_010.json", 578 | "testdata/roughtime_ietf_draft11_100.json", 579 | "testdata/roughtime_google_001.json", 580 | "testdata/roughtime_google_010.json", 581 | "testdata/roughtime_google_100.json", 582 | } { 583 | fp, err := os.Open(fileName) 584 | if err != nil { 585 | continue 586 | } 587 | 588 | testVecBytes, err := io.ReadAll(fp) 589 | if err != nil { 590 | continue 591 | } 592 | 593 | var testVec protocolTesting.TestVector 594 | if err = json.Unmarshal(testVecBytes, &testVec); err != nil { 595 | continue 596 | } 597 | 598 | for _, request := range testVec.Requests { 599 | requestBytes, err := hex.DecodeString(request) 600 | if err == nil { 601 | f.Add(requestBytes) 602 | } 603 | } 604 | } 605 | 606 | f.Fuzz(func(t *testing.T, data []byte) { 607 | _, _ = ParseRequest(data) 608 | }) 609 | } 610 | 611 | func FuzzVerifyReply(f *testing.F) { 612 | f.Fuzz(func(t *testing.T, replyBytes []byte, publicKey []byte, nonce []byte) { 613 | for _, ver := range allVersions { 614 | _, _, _ = VerifyReply([]Version{ver}, replyBytes, publicKey, nonce) 615 | } 616 | }) 617 | } 618 | -------------------------------------------------------------------------------- /protocol/testdata/roughtime_google_001.json: -------------------------------------------------------------------------------- 1 | {"info":"Google-Roughtime 1","root_key":"d102b712f341204711daaf20e0d13557a37073e9c25325c1c6bda876eb2d6a2d","online_key":"613bbf61d362d6474041486a9440feeb7cc71b48951a30e7b0190be42bc7a5ab","request":["02000000400000004e4f4e43504144fff89428abb78e85915ed2ec40007b9294082879882bb38ac1794b2ce99356b32da8f84f3574ad023f6eea9e6cdd0945de911f58fdf5f913aff723a49f03bdf43e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"],"replies":["06000000400000008000000080000000e40000007c010000534947004e4f4e43504154485352455043455254494e4458a81e4c68ccee770c1a1b14e7edaad55490f02312e915680a5035ea250697a4c1735fc5157f5f266a92a95f2f0b928de32a04df0d51000bfa935c60ff38f6bf01f89428abb78e85915ed2ec40007b9294082879882bb38ac1794b2ce99356b32da8f84f3574ad023f6eea9e6cdd0945de911f58fdf5f913aff723a49f03bdf43e03000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000f618e3040b8a0e3d33093b11aeb99e99c345de0c2ad20be2f583758a0d0d2f68fdfb7a17e192e9dae6f3daca34446a5c02b0a076250c45f4046d4dc326a7859202000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000000000000"]} -------------------------------------------------------------------------------- /protocol/testdata/roughtime_google_010.json: -------------------------------------------------------------------------------- 1 | {"info":"Google-Roughtime 10","root_key":"d102b712f341204711daaf20e0d13557a37073e9c25325c1c6bda876eb2d6a2d","online_key":"613bbf61d362d6474041486a9440feeb7cc71b48951a30e7b0190be42bc7a5ab","request":["02000000400000004e4f4e43504144ff196e721cd714a9b2cab3d58626534f817a3e6a624a4e4ef7817022b54c0e57a1d80120764297724f440b95ec028c28816e289e9b9a33133617585e2717fcc2c80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","02000000400000004e4f4e43504144ff249febcd658968787f3004ad8281b0f5b15339043bdd930bf98585aadffb4b97162e3bcfbe05ecb409f18d40c70f2bb214ec503a41afdb8ecfc5efe38de95f980000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","02000000400000004e4f4e43504144ff42120ff9a18d8f771ee11d2e2de100cbb8a11ed86906a9f8c7521dedd3245556ff6da92a77783b83d6f587960c60821bc40614b101b29689127b3ca6e8e4b45d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","02000000400000004e4f4e43504144ffd5fcd7cdbec36d43ebe8db2e0132fdbea3c564c1f9e477504f443acb422cb9239e449eb7905e4cf099bcfbaa63d284c1046bc39afd76b0359f01cbd7b530ab720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","02000000400000004e4f4e43504144ff1ad63e251edec7ed0de8f33c4d519604f6dcbfaa80c1f414122375b19257ea1788f0cc58128ef0ee90dca1018dfb9c55993ed95106d7736285b75206ea1694cc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","02000000400000004e4f4e43504144ff892c24e9be18e76050a385797b24836e37979f8322c5d0c6356be7a367f72b73d63c9b84b79e622f47047ccff8fb0d2184c8ff6499c746d953effe6af19b9d1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","02000000400000004e4f4e43504144fff6bc4a78bc943741d1eab31079385ba78aed0603cd5a9b30977b158316b0762362361461a35412f8ab55a962f3512a6df6bd8d24d37dcaa9dd9852456c5a17580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","02000000400000004e4f4e43504144ffaeb6d68f53ffc7f0378a27af17137bf0a637d5ff2b8f51c1488bba9a57aaf4d1f82f7f13b3bc7ce91e8140f15e2a67b46fba15e4c919606de84eda982f6e94cc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","02000000400000004e4f4e43504144ffad1f519984ad953c76272503712d98e9eaef7a4b0919d528bd16e6e8beaa1c8f9e53aa9ca3a6dc09b27daf61372908e69d2716179dd3aaf2bfe6ebc3dccf7ccc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","02000000400000004e4f4e43504144ff2703856b6aad48159871af407bbcc92746f33b5f8a67f0efbd780bd5bf6c0729a1d194664227cc06d4b16d137496306ae5bf9be1f52c0b94b98c821b21cdda360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"],"replies":["06000000400000008000000080010000e40100007c020000534947004e4f4e43504154485352455043455254494e44581b5c4ddb2c250192bf02b32e98afe8620c62fa72e979b99e4dbde5dcd76fe73d7995bb750aec9bca6aeafeaf17bd296696a32392572c5788695c4852db707405196e721cd714a9b2cab3d58626534f817a3e6a624a4e4ef7817022b54c0e57a1d80120764297724f440b95ec028c28816e289e9b9a33133617585e2717fcc2c84ca01047cabd99f719393bad436c4ec204324657d6bc69b0ec7dff360a79177c5980f2f3385f70697ae6a87cd3e0d8cd17135554bd4414a714df28894c910fd774f2479b82858204bf8360645fcf8a7a6c8a1225d71cb4642c156ec881533cffe3b79b6c541bd489138fe00fc994f7979a05d83bddf8c081b378253417667df7fe87d07fc129c69a5ee781178426b984361950db3a7391d9ef27d78eb5811f525d1c18f5778391ede04339e52c23735451b42a330e116e4191bb4c168606df94be55219df43bd4aa6d47bbe10410679ba923dc096dda6d80dea72b62213578393bc12389ba25c820747829909253f38653d51e5fbb0847505b4d290d294fb06703000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000cd73f31f9c608b45257422e37ecdf7f22a79d50b7d46924a1c49e6a164dad7c32af2c731836381af052c5784b2bf07b48b90c3da564a413e890ef93d5aa5ff1502000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000000000000","06000000400000008000000080010000e40100007c020000534947004e4f4e43504154485352455043455254494e44581b5c4ddb2c250192bf02b32e98afe8620c62fa72e979b99e4dbde5dcd76fe73d7995bb750aec9bca6aeafeaf17bd296696a32392572c5788695c4852db707405249febcd658968787f3004ad8281b0f5b15339043bdd930bf98585aadffb4b97162e3bcfbe05ecb409f18d40c70f2bb214ec503a41afdb8ecfc5efe38de95f98923fd42e7adc8eb34980566782b44d7302bc685b8a79d148266ff098ad44cd245a0e8ee61f2b6133189ddafa6328dbf9fd26acd0d5bba64f891e879990f039c874f2479b82858204bf8360645fcf8a7a6c8a1225d71cb4642c156ec881533cffe3b79b6c541bd489138fe00fc994f7979a05d83bddf8c081b378253417667df7fe87d07fc129c69a5ee781178426b984361950db3a7391d9ef27d78eb5811f525d1c18f5778391ede04339e52c23735451b42a330e116e4191bb4c168606df94be55219df43bd4aa6d47bbe10410679ba923dc096dda6d80dea72b62213578393bc12389ba25c820747829909253f38653d51e5fbb0847505b4d290d294fb06703000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000cd73f31f9c608b45257422e37ecdf7f22a79d50b7d46924a1c49e6a164dad7c32af2c731836381af052c5784b2bf07b48b90c3da564a413e890ef93d5aa5ff1502000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000001000000","06000000400000008000000080010000e40100007c020000534947004e4f4e43504154485352455043455254494e44581b5c4ddb2c250192bf02b32e98afe8620c62fa72e979b99e4dbde5dcd76fe73d7995bb750aec9bca6aeafeaf17bd296696a32392572c5788695c4852db70740542120ff9a18d8f771ee11d2e2de100cbb8a11ed86906a9f8c7521dedd3245556ff6da92a77783b83d6f587960c60821bc40614b101b29689127b3ca6e8e4b45d3864f2edd525e6f3ffd0591923bb521e2bb47b5bf14d8f24c5ec42ea5b86d4ac8125851c99d229a5e42973f72a494b77e968e496623e7ed9fa870b0c3303dc4585f62e3b2706795819b5740ed7760d2b195e23f8db90f795fb7abbdf4e93475f11775832532c3ae7420fb7aca9da2612d717f7db42466f46c8adf7026e9ee955fe87d07fc129c69a5ee781178426b984361950db3a7391d9ef27d78eb5811f525d1c18f5778391ede04339e52c23735451b42a330e116e4191bb4c168606df94be55219df43bd4aa6d47bbe10410679ba923dc096dda6d80dea72b62213578393bc12389ba25c820747829909253f38653d51e5fbb0847505b4d290d294fb06703000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000cd73f31f9c608b45257422e37ecdf7f22a79d50b7d46924a1c49e6a164dad7c32af2c731836381af052c5784b2bf07b48b90c3da564a413e890ef93d5aa5ff1502000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000002000000","06000000400000008000000080010000e40100007c020000534947004e4f4e43504154485352455043455254494e44581b5c4ddb2c250192bf02b32e98afe8620c62fa72e979b99e4dbde5dcd76fe73d7995bb750aec9bca6aeafeaf17bd296696a32392572c5788695c4852db707405d5fcd7cdbec36d43ebe8db2e0132fdbea3c564c1f9e477504f443acb422cb9239e449eb7905e4cf099bcfbaa63d284c1046bc39afd76b0359f01cbd7b530ab72b0aeb4fb4d99d657a67174305a9fd89abec17846536626011a839e41193560ac014d65b7b4bc8d8a7ce959842be45151c0a65e60a14c5a74cf77355075d4a1c885f62e3b2706795819b5740ed7760d2b195e23f8db90f795fb7abbdf4e93475f11775832532c3ae7420fb7aca9da2612d717f7db42466f46c8adf7026e9ee955fe87d07fc129c69a5ee781178426b984361950db3a7391d9ef27d78eb5811f525d1c18f5778391ede04339e52c23735451b42a330e116e4191bb4c168606df94be55219df43bd4aa6d47bbe10410679ba923dc096dda6d80dea72b62213578393bc12389ba25c820747829909253f38653d51e5fbb0847505b4d290d294fb06703000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000cd73f31f9c608b45257422e37ecdf7f22a79d50b7d46924a1c49e6a164dad7c32af2c731836381af052c5784b2bf07b48b90c3da564a413e890ef93d5aa5ff1502000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000003000000","06000000400000008000000080010000e40100007c020000534947004e4f4e43504154485352455043455254494e44581b5c4ddb2c250192bf02b32e98afe8620c62fa72e979b99e4dbde5dcd76fe73d7995bb750aec9bca6aeafeaf17bd296696a32392572c5788695c4852db7074051ad63e251edec7ed0de8f33c4d519604f6dcbfaa80c1f414122375b19257ea1788f0cc58128ef0ee90dca1018dfb9c55993ed95106d7736285b75206ea1694cc2e7eb130d1599f0cb5f6833a2ddf6c767b0ef243ae241564ffe10bae650f3bdbad15cf4eb7351aeebb09b28f918621eaf8107369c864dd736dd83a24231c1d0cf32ca6285f1cca466470e7a990cfb379b44f7db4d8d8377b392dee477f95d949c38858c80c4be6c65b6115fec1a02d7937aeabd1a0ab814558d632df51f74a5ebe5778e2ad8efc56ef9a52d16cd444316ff70c909acdf3dbb28429dfd7951cbec1531c22ca32abbe533a6f1f439717a02579de740509a84a0bd45f12f73d4c34be55219df43bd4aa6d47bbe10410679ba923dc096dda6d80dea72b62213578393bc12389ba25c820747829909253f38653d51e5fbb0847505b4d290d294fb06703000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000cd73f31f9c608b45257422e37ecdf7f22a79d50b7d46924a1c49e6a164dad7c32af2c731836381af052c5784b2bf07b48b90c3da564a413e890ef93d5aa5ff1502000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000004000000","06000000400000008000000080010000e40100007c020000534947004e4f4e43504154485352455043455254494e44581b5c4ddb2c250192bf02b32e98afe8620c62fa72e979b99e4dbde5dcd76fe73d7995bb750aec9bca6aeafeaf17bd296696a32392572c5788695c4852db707405892c24e9be18e76050a385797b24836e37979f8322c5d0c6356be7a367f72b73d63c9b84b79e622f47047ccff8fb0d2184c8ff6499c746d953effe6af19b9d1b265214ce7dc13d96ef929d7d24513ae893ad4dac8e7d300c09c6e5cea8ec8f54f506c17dd5629407e85b549ff768b861a628d719a22c05c28c14fd05d20fac8ef32ca6285f1cca466470e7a990cfb379b44f7db4d8d8377b392dee477f95d949c38858c80c4be6c65b6115fec1a02d7937aeabd1a0ab814558d632df51f74a5ebe5778e2ad8efc56ef9a52d16cd444316ff70c909acdf3dbb28429dfd7951cbec1531c22ca32abbe533a6f1f439717a02579de740509a84a0bd45f12f73d4c34be55219df43bd4aa6d47bbe10410679ba923dc096dda6d80dea72b62213578393bc12389ba25c820747829909253f38653d51e5fbb0847505b4d290d294fb06703000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000cd73f31f9c608b45257422e37ecdf7f22a79d50b7d46924a1c49e6a164dad7c32af2c731836381af052c5784b2bf07b48b90c3da564a413e890ef93d5aa5ff1502000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000005000000","06000000400000008000000080010000e40100007c020000534947004e4f4e43504154485352455043455254494e44581b5c4ddb2c250192bf02b32e98afe8620c62fa72e979b99e4dbde5dcd76fe73d7995bb750aec9bca6aeafeaf17bd296696a32392572c5788695c4852db707405f6bc4a78bc943741d1eab31079385ba78aed0603cd5a9b30977b158316b0762362361461a35412f8ab55a962f3512a6df6bd8d24d37dcaa9dd9852456c5a1758731317217a0505e524aeea678a811babdb2f5fc5134f5fcc11341293dd0a0d118e397c690b3375f1ca1da470d9997e2c090f11e90ed936aa658062186cc53983d62572fc7fac7ac1977e7854fecf0594dd26dcad99740c1d442143e4f4c1ea44d2dc6bddeaf55e5f7e336c810fe95b0bbd177e3834acea12c4b8a63456a83367be5778e2ad8efc56ef9a52d16cd444316ff70c909acdf3dbb28429dfd7951cbec1531c22ca32abbe533a6f1f439717a02579de740509a84a0bd45f12f73d4c34be55219df43bd4aa6d47bbe10410679ba923dc096dda6d80dea72b62213578393bc12389ba25c820747829909253f38653d51e5fbb0847505b4d290d294fb06703000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000cd73f31f9c608b45257422e37ecdf7f22a79d50b7d46924a1c49e6a164dad7c32af2c731836381af052c5784b2bf07b48b90c3da564a413e890ef93d5aa5ff1502000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000006000000","06000000400000008000000080010000e40100007c020000534947004e4f4e43504154485352455043455254494e44581b5c4ddb2c250192bf02b32e98afe8620c62fa72e979b99e4dbde5dcd76fe73d7995bb750aec9bca6aeafeaf17bd296696a32392572c5788695c4852db707405aeb6d68f53ffc7f0378a27af17137bf0a637d5ff2b8f51c1488bba9a57aaf4d1f82f7f13b3bc7ce91e8140f15e2a67b46fba15e4c919606de84eda982f6e94cc15c17f4262a8bc8a6f9284ed0994066950e0ee049b18187ae13c44a98b4ba5c690b5dfd9f7beb18732daff31583ef363c231bdd6836ad8f2117766df4d2cc98ad62572fc7fac7ac1977e7854fecf0594dd26dcad99740c1d442143e4f4c1ea44d2dc6bddeaf55e5f7e336c810fe95b0bbd177e3834acea12c4b8a63456a83367be5778e2ad8efc56ef9a52d16cd444316ff70c909acdf3dbb28429dfd7951cbec1531c22ca32abbe533a6f1f439717a02579de740509a84a0bd45f12f73d4c34be55219df43bd4aa6d47bbe10410679ba923dc096dda6d80dea72b62213578393bc12389ba25c820747829909253f38653d51e5fbb0847505b4d290d294fb06703000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000cd73f31f9c608b45257422e37ecdf7f22a79d50b7d46924a1c49e6a164dad7c32af2c731836381af052c5784b2bf07b48b90c3da564a413e890ef93d5aa5ff1502000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000007000000","06000000400000008000000080010000e40100007c020000534947004e4f4e43504154485352455043455254494e44581b5c4ddb2c250192bf02b32e98afe8620c62fa72e979b99e4dbde5dcd76fe73d7995bb750aec9bca6aeafeaf17bd296696a32392572c5788695c4852db707405ad1f519984ad953c76272503712d98e9eaef7a4b0919d528bd16e6e8beaa1c8f9e53aa9ca3a6dc09b27daf61372908e69d2716179dd3aaf2bfe6ebc3dccf7ccc1efad7ac66201054ae66b68fee3a1d59fe9a4fc35e8435dd96ff635786ff91a141619be2cf268f8f978466929bf28b0d737a223a61d34c061e87e0d43bdf519b85f62e3b2706795819b5740ed7760d2b195e23f8db90f795fb7abbdf4e93475f11775832532c3ae7420fb7aca9da2612d717f7db42466f46c8adf7026e9ee955be5778e2ad8efc56ef9a52d16cd444316ff70c909acdf3dbb28429dfd7951cbec1531c22ca32abbe533a6f1f439717a02579de740509a84a0bd45f12f73d4c342c81935bc282d75d1fdd1b81f00fda2afa7e4c6a89406915351042c4666d980ec5d56cc602f0b402b502073c17ae49a944e0bc9f8197386942258a58d56c259503000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000cd73f31f9c608b45257422e37ecdf7f22a79d50b7d46924a1c49e6a164dad7c32af2c731836381af052c5784b2bf07b48b90c3da564a413e890ef93d5aa5ff1502000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000008000000","06000000400000008000000080010000e40100007c020000534947004e4f4e43504154485352455043455254494e44581b5c4ddb2c250192bf02b32e98afe8620c62fa72e979b99e4dbde5dcd76fe73d7995bb750aec9bca6aeafeaf17bd296696a32392572c5788695c4852db7074052703856b6aad48159871af407bbcc92746f33b5f8a67f0efbd780bd5bf6c0729a1d194664227cc06d4b16d137496306ae5bf9be1f52c0b94b98c821b21cdda36d9c161953ec166549bf9a246fed0d5beee9dea1f1a232297516ce066bda13d714d2a76682dc769d1d48e2f277bec35fa9db7737c9ccef11e33834107737e458e85f62e3b2706795819b5740ed7760d2b195e23f8db90f795fb7abbdf4e93475f11775832532c3ae7420fb7aca9da2612d717f7db42466f46c8adf7026e9ee955be5778e2ad8efc56ef9a52d16cd444316ff70c909acdf3dbb28429dfd7951cbec1531c22ca32abbe533a6f1f439717a02579de740509a84a0bd45f12f73d4c342c81935bc282d75d1fdd1b81f00fda2afa7e4c6a89406915351042c4666d980ec5d56cc602f0b402b502073c17ae49a944e0bc9f8197386942258a58d56c259503000000040000000c000000524144494d494450524f4f54404b4c0080f0fa0200000000cd73f31f9c608b45257422e37ecdf7f22a79d50b7d46924a1c49e6a164dad7c32af2c731836381af052c5784b2bf07b48b90c3da564a413e890ef93d5aa5ff1502000000400000005349470044454c450edc41a4fdd9250f9bd7ccf4e48f5fb0ad77c44363d41c59ce9fafeaeb979096548e90828fb329a491a9083b2ccf43c9de24b3575b00a454075b35e523f8dd020300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de3779000000000000000000e1f5050000000009000000"]} -------------------------------------------------------------------------------- /protocol/testdata/roughtime_ietf_draft08_001.json: -------------------------------------------------------------------------------- 1 | {"info":"draft-ietf-ntp-roughtime-08 1","root_key":"d102b712f341204711daaf20e0d13557a37073e9c25325c1c6bda876eb2d6a2d","online_key":"613bbf61d362d6474041486a9440feeb7cc71b48951a30e7b0190be42bc7a5ab","request":["524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a08000080cbc848bc48dc3e129525ff673435ee04bb08976d51c84c650e1cf3785cce0f4a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"],"replies":["524f55474854494d7c0100000700000040000000440000006400000064000000a80000004001000053494700564552004e4f4e43504154485352455043455254494e44584c274e9d3b48a6e4f4ddcb10f52d9bf6264c28136717c99568e9a807aaf800572c7c34c89267fea1b9ff29ce9a986c5a8eb43664612e09ec8e2aaee21bed040608000080cbc848bc48dc3e129525ff673435ee04bb08976d51c84c650e1cf3785cce0f4a03000000040000000c000000524144494d494450524f4f54050000003200000000000000af1d404574c9b4464e2a601bf47ae118080b007b96c1c2d1c8d7576cef40e36002000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000000000000"]} -------------------------------------------------------------------------------- /protocol/testdata/roughtime_ietf_draft08_010.json: -------------------------------------------------------------------------------- 1 | {"info":"draft-ietf-ntp-roughtime-08 10","root_key":"d102b712f341204711daaf20e0d13557a37073e9c25325c1c6bda876eb2d6a2d","online_key":"613bbf61d362d6474041486a9440feeb7cc71b48951a30e7b0190be42bc7a5ab","request":["524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a08000080714c361dd11cc906b6c5790afe89c4dbcbc668bac1e733b4191e701930e45ab000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a0800008033d5f4be7099d06a49a3f627649a3298182f9b925ccc9eada68be265752b2f2900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a080000808f2b7a56089960389566b9059757386aa3f8fbbfeba7aca1186e92d993a8151300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a080000802cfb82d050cdfe8735060b5385f918e19001b88c60d824e10b1837a9ba2c520d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a0800008005ebaedb9b8081e84148bc7b6a52bbc14261fa12dfe49f09d14fe3e5ad12148a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a0800008091238ed385ac9052b16c43edbc36b55f8eff55207c72fdfe6984515cb5c696bf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a08000080575776c802426bb7a4c9733563babed21ce8aa5348d6a37c72cc67c8dcd2a3f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a080000803655d96bfbfb991ad60747199e7d4cb9d7bd8528bb4183688e19037f005fcb7d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a080000807c355ddbe7cb1f24a49bbdbb3b92288f60d4129525ccb3551af75ab2c3614f3700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df4030000030000000400000024000000564552004e4f4e435a5a5a5a0800008033b774a87d97fed31b800141aeaa4524f67df89b48c674558de1f47f9aa2301c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"],"replies":["524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d80208000080714c361dd11cc906b6c5790afe89c4dbcbc668bac1e733b4191e701930e45ab083ddcd6280e78b912424474f85921da9b6a5c39ddb46e4e6ab4e092c9ef9db8ff4540a284391cce470335335d184d61ef276cd46149cc707fede692144a4a8a84007a77893878b56f4c63e0b3d470b24e2f35fad2850885503f7a79444d927e4998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000000000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020800008033d5f4be7099d06a49a3f627649a3298182f9b925ccc9eada68be265752b2f29a499d257d55115634f86e67f6ade5741003b0fb3ee778a7a403d7745fdfc70e9f4540a284391cce470335335d184d61ef276cd46149cc707fede692144a4a8a84007a77893878b56f4c63e0b3d470b24e2f35fad2850885503f7a79444d927e4998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000001000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d802080000808f2b7a56089960389566b9059757386aa3f8fbbfeba7aca1186e92d993a81513c72d6708469de8e9e8f354b65b60b0f94780d530c4336aa1f69e2afba14efdb4d19fcf2b281989a92b965279e95b26e58abb7394e87f84de10b4ce76322af72f4007a77893878b56f4c63e0b3d470b24e2f35fad2850885503f7a79444d927e4998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000002000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d802080000802cfb82d050cdfe8735060b5385f918e19001b88c60d824e10b1837a9ba2c520ddd039c7d46dc2eaea00912e95a11258173a63709ed899f74798d400666525470d19fcf2b281989a92b965279e95b26e58abb7394e87f84de10b4ce76322af72f4007a77893878b56f4c63e0b3d470b24e2f35fad2850885503f7a79444d927e4998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000003000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020800008005ebaedb9b8081e84148bc7b6a52bbc14261fa12dfe49f09d14fe3e5ad12148a3250700b2948d597d6b6213a78ac41535bacbbe46d3163272ad74ff582bd66b96914ea5992d7b07488504f3fa20bafe4ede0c2c22a53b885cea509e09d3c6c1f191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f86998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000004000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020800008091238ed385ac9052b16c43edbc36b55f8eff55207c72fdfe6984515cb5c696bfa58520bfd4f568df8d9ffdf4edeb3e90d429cce40ce944c72ff42c9a280c39b36914ea5992d7b07488504f3fa20bafe4ede0c2c22a53b885cea509e09d3c6c1f191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f86998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000005000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d80208000080575776c802426bb7a4c9733563babed21ce8aa5348d6a37c72cc67c8dcd2a3f5079d1a3fcd6449b5723e0484a8922ab7e79535a478e1f3041489bae195667ea64b1aabe653c725a14c62278ed6c8957861cea2f4fecb0733011090b33bb0aa71191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f86998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000006000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d802080000803655d96bfbfb991ad60747199e7d4cb9d7bd8528bb4183688e19037f005fcb7d970ae2fc11ca2959d969c18fe54efcae5b5dc53eb8d2beb5b2be3fc10baee5484b1aabe653c725a14c62278ed6c8957861cea2f4fecb0733011090b33bb0aa71191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f86998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000007000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d802080000807c355ddbe7cb1f24a49bbdbb3b92288f60d4129525ccb3551af75ab2c3614f37db9a0b42b92049c8cb90ece4476570fe9e49fa50619d8e41dfc4f8529014ab17d19fcf2b281989a92b965279e95b26e58abb7394e87f84de10b4ce76322af72f191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f865fb3fe4264ca2aa3503abb6ab27ca452fa3ae2ee50a05a9667382f9391728a8603000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000008000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020800008033b774a87d97fed31b800141aeaa4524f67df89b48c674558de1f47f9aa2301c2f4ee8946b64b69474b54a99c12b7cb0ff65fb0c14d991873f99cad918ad1bebd19fcf2b281989a92b965279e95b26e58abb7394e87f84de10b4ce76322af72f191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f865fb3fe4264ca2aa3503abb6ab27ca452fa3ae2ee50a05a9667382f9391728a8603000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000009000000"]} -------------------------------------------------------------------------------- /protocol/testdata/roughtime_ietf_draft11_001.json: -------------------------------------------------------------------------------- 1 | {"info":"draft-ietf-ntp-roughtime-11 1","root_key":"d102b712f341204711daaf20e0d13557a37073e9c25325c1c6bda876eb2d6a2d","online_key":"613bbf61d362d6474041486a9440feeb7cc71b48951a30e7b0190be42bc7a5ab","request":["524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b34cbc848bc48dc3e129525ff673435ee04bb08976d51c84c650e1cf3785cce0f4a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"],"replies":["524f55474854494d7c0100000700000040000000440000006400000064000000a80000004001000053494700564552004e4f4e43504154485352455043455254494e44584c274e9d3b48a6e4f4ddcb10f52d9bf6264c28136717c99568e9a807aaf800572c7c34c89267fea1b9ff29ce9a986c5a8eb43664612e09ec8e2aaee21bed04060b000080cbc848bc48dc3e129525ff673435ee04bb08976d51c84c650e1cf3785cce0f4a03000000040000000c000000524144494d494450524f4f54050000003200000000000000af1d404574c9b4464e2a601bf47ae118080b007b96c1c2d1c8d7576cef40e36002000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000000000000"]} -------------------------------------------------------------------------------- /protocol/testdata/roughtime_ietf_draft11_010.json: -------------------------------------------------------------------------------- 1 | {"info":"draft-ietf-ntp-roughtime-11 10","root_key":"d102b712f341204711daaf20e0d13557a37073e9c25325c1c6bda876eb2d6a2d","online_key":"613bbf61d362d6474041486a9440feeb7cc71b48951a30e7b0190be42bc7a5ab","request":["524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b34714c361dd11cc906b6c5790afe89c4dbcbc668bac1e733b4191e701930e45ab0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b3433d5f4be7099d06a49a3f627649a3298182f9b925ccc9eada68be265752b2f29000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b348f2b7a56089960389566b9059757386aa3f8fbbfeba7aca1186e92d993a81513000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b342cfb82d050cdfe8735060b5385f918e19001b88c60d824e10b1837a9ba2c520d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b3405ebaedb9b8081e84148bc7b6a52bbc14261fa12dfe49f09d14fe3e5ad12148a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b3491238ed385ac9052b16c43edbc36b55f8eff55207c72fdfe6984515cb5c696bf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b34575776c802426bb7a4c9733563babed21ce8aa5348d6a37c72cc67c8dcd2a3f5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b343655d96bfbfb991ad60747199e7d4cb9d7bd8528bb4183688e19037f005fcb7d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b347c355ddbe7cb1f24a49bbdbb3b92288f60d4129525ccb3551af75ab2c3614f37000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","524f55474854494df40300000400000004000000240000004400000056455200535256004e4f4e435a5a5a5a0b00008053ca8a87ea8b39253a9bef995703eb3c07a34bc138d5971c15c45a89b6364b3433b774a87d97fed31b800141aeaa4524f67df89b48c674558de1f47f9aa2301c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"],"replies":["524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020b000080714c361dd11cc906b6c5790afe89c4dbcbc668bac1e733b4191e701930e45ab083ddcd6280e78b912424474f85921da9b6a5c39ddb46e4e6ab4e092c9ef9db8ff4540a284391cce470335335d184d61ef276cd46149cc707fede692144a4a8a84007a77893878b56f4c63e0b3d470b24e2f35fad2850885503f7a79444d927e4998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000000000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020b00008033d5f4be7099d06a49a3f627649a3298182f9b925ccc9eada68be265752b2f29a499d257d55115634f86e67f6ade5741003b0fb3ee778a7a403d7745fdfc70e9f4540a284391cce470335335d184d61ef276cd46149cc707fede692144a4a8a84007a77893878b56f4c63e0b3d470b24e2f35fad2850885503f7a79444d927e4998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000001000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020b0000808f2b7a56089960389566b9059757386aa3f8fbbfeba7aca1186e92d993a81513c72d6708469de8e9e8f354b65b60b0f94780d530c4336aa1f69e2afba14efdb4d19fcf2b281989a92b965279e95b26e58abb7394e87f84de10b4ce76322af72f4007a77893878b56f4c63e0b3d470b24e2f35fad2850885503f7a79444d927e4998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000002000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020b0000802cfb82d050cdfe8735060b5385f918e19001b88c60d824e10b1837a9ba2c520ddd039c7d46dc2eaea00912e95a11258173a63709ed899f74798d400666525470d19fcf2b281989a92b965279e95b26e58abb7394e87f84de10b4ce76322af72f4007a77893878b56f4c63e0b3d470b24e2f35fad2850885503f7a79444d927e4998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000003000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020b00008005ebaedb9b8081e84148bc7b6a52bbc14261fa12dfe49f09d14fe3e5ad12148a3250700b2948d597d6b6213a78ac41535bacbbe46d3163272ad74ff582bd66b96914ea5992d7b07488504f3fa20bafe4ede0c2c22a53b885cea509e09d3c6c1f191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f86998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000004000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020b00008091238ed385ac9052b16c43edbc36b55f8eff55207c72fdfe6984515cb5c696bfa58520bfd4f568df8d9ffdf4edeb3e90d429cce40ce944c72ff42c9a280c39b36914ea5992d7b07488504f3fa20bafe4ede0c2c22a53b885cea509e09d3c6c1f191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f86998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000005000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020b000080575776c802426bb7a4c9733563babed21ce8aa5348d6a37c72cc67c8dcd2a3f5079d1a3fcd6449b5723e0484a8922ab7e79535a478e1f3041489bae195667ea64b1aabe653c725a14c62278ed6c8957861cea2f4fecb0733011090b33bb0aa71191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f86998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000006000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020b0000803655d96bfbfb991ad60747199e7d4cb9d7bd8528bb4183688e19037f005fcb7d970ae2fc11ca2959d969c18fe54efcae5b5dc53eb8d2beb5b2be3fc10baee5484b1aabe653c725a14c62278ed6c8957861cea2f4fecb0733011090b33bb0aa71191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f86998414bf0d56b4950612d936300acfe0d7a38dd0aa59c92a7af6e4d3ac2b870703000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000007000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020b0000807c355ddbe7cb1f24a49bbdbb3b92288f60d4129525ccb3551af75ab2c3614f37db9a0b42b92049c8cb90ece4476570fe9e49fa50619d8e41dfc4f8529014ab17d19fcf2b281989a92b965279e95b26e58abb7394e87f84de10b4ce76322af72f191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f865fb3fe4264ca2aa3503abb6ab27ca452fa3ae2ee50a05a9667382f9391728a8603000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000008000000","524f55474854494dfc01000007000000400000004400000064000000e400000028010000c001000053494700564552004e4f4e43504154485352455043455254494e4458d454743b3b939e0bb1d969f676cdbc8cfd3ca80f05bd128f84f38b5a143ef9f280978731f4ac5485abf821ce05ccb51b2d6b087658c636be42c015726373d8020b00008033b774a87d97fed31b800141aeaa4524f67df89b48c674558de1f47f9aa2301c2f4ee8946b64b69474b54a99c12b7cb0ff65fb0c14d991873f99cad918ad1bebd19fcf2b281989a92b965279e95b26e58abb7394e87f84de10b4ce76322af72f191b91a4bcdfd949aec4437c01e4e748908a6c56addef3a4c71ddc7b08985f865fb3fe4264ca2aa3503abb6ab27ca452fa3ae2ee50a05a9667382f9391728a8603000000040000000c000000524144494d494450524f4f54050000003200000000000000c8142bb32b76a218a945f027769e141bf0c349a0d915e28a2208d44f230a814b02000000400000005349470044454c45723fdab135deedb4d1e2ee7e8254881463fda216bd901421ef26b3046eae88937b4346cba4d5d2c33917be636e2e4c883db231ccdbb87d902f8f86f7c216ea000300000020000000280000005055424b4d494e544d41585481d19b7ff58d408302a83f24da533dde16b71f80f8c1b8ce2798ae1571de37790000000000000000640000000000000009000000"]} -------------------------------------------------------------------------------- /protocol/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloudflare, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package protocol 16 | 17 | import "fmt" 18 | 19 | // Version indicates the version of the Roughtime protocol in use. 20 | type Version uint32 21 | 22 | const ( 23 | // VersionGoogle is Google-Roughtime as described here: 24 | // https://roughtime.googlesource.com/roughtime/+/HEAD/PROTOCOL.md 25 | VersionGoogle Version = 0 26 | 27 | // VersionDraft08 is draft-ietf-ntp-roughtime-08. 28 | VersionDraft08 Version = 0x80000008 29 | 30 | // VersionDraft11 is draft-ietf-ntp-roughtime-11. 31 | VersionDraft11 Version = 0x8000000b 32 | ) 33 | 34 | // allVersions is a list of all supported versions in order from newest to oldest. 35 | var allVersions = []Version{ 36 | VersionDraft11, 37 | VersionDraft08, 38 | VersionGoogle, 39 | } 40 | 41 | // ietfVersions is a list of all IETF drafts in order from newest to oldest. 42 | var ietfVersions = []Version{ 43 | VersionDraft11, 44 | VersionDraft08, 45 | } 46 | 47 | func (ver Version) isSupported() bool { 48 | for i := range ietfVersions { 49 | if ver == ietfVersions[i] { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | 56 | func (ver Version) String() string { 57 | switch ver { 58 | case VersionGoogle: 59 | return "Google-Roughtime" 60 | case VersionDraft08: 61 | return "draft-ietf-ntp-roughtime-08" 62 | case VersionDraft11: 63 | return "draft-ietf-ntp-roughtime-11" 64 | default: 65 | return fmt.Sprintf("%d", uint32(ver)) 66 | } 67 | } 68 | 69 | // advertisedVersionsFromPreference derives the list of versions advertised in 70 | // its request by a client with the given preferences. 71 | // 72 | // If len(versionPreference) == 0, then a safe default is used. 73 | // 74 | // If versionPreference includes Google-Roughtime, then it must be the only 75 | // version that is supported. If not, an error is returned. 76 | // 77 | // This function also returns a boolean indicating whether to use 78 | // IETF-Roughtime. 79 | func advertisedVersionsFromPreference(versionPreference []Version) ([]Version, bool, error) { 80 | if len(versionPreference) == 0 { 81 | return []Version{VersionDraft11, VersionDraft08}, true, nil 82 | } 83 | 84 | versionIETF := true 85 | for _, vers := range versionPreference { 86 | if vers == VersionGoogle { 87 | versionIETF = false 88 | break 89 | } 90 | } 91 | if !versionIETF && len(versionPreference) != 1 { 92 | return nil, false, fmt.Errorf("cannot support %s simultaneously with other versions", VersionGoogle) 93 | } 94 | 95 | return versionPreference, versionIETF, nil 96 | } 97 | 98 | // ResponseVersionFromSupported selects a version to use from the list of 99 | // versions supported by the clients. Returns an error if the input slice is 100 | // zero-length. 101 | func ResponseVersionFromSupported(supportedVersions []Version) (Version, error) { 102 | for _, ver := range allVersions { 103 | for _, supportedVer := range supportedVersions { 104 | if ver == supportedVer { 105 | return ver, nil 106 | } 107 | } 108 | } 109 | return 0, errUnsupportedVersion(supportedVersions) 110 | } 111 | -------------------------------------------------------------------------------- /recipes/alerter.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | // This program performs a sequence of Roughtime queries and creates an alert 5 | // via `notify-send` if the system's clock is skewed beyond an acceptable 6 | // threshold. 7 | // 8 | // This has been tested with go>=1.10 on Ubuntu 18.04. 9 | package main 10 | 11 | import ( 12 | "flag" 13 | "fmt" 14 | "log" 15 | "math" 16 | "os" 17 | "os/exec" 18 | "time" 19 | 20 | "github.com/cloudflare/roughtime/client" 21 | ) 22 | 23 | const ( 24 | summary = `Check your clock!` 25 | template = `Your clock is off Roughtime by %v. See %s for details.` 26 | ) 27 | 28 | func main() { 29 | //caFile := flag.String("ca", "recipes/testdata/ca.pem", "File with the server's CA certificate") 30 | rtConfig := flag.String("rt", "ecosystem.json", "File with the Roughtime configuration") 31 | dialAttempts := flag.Int("a", client.DefaultQueryAttempts, "Number of times to try dialing each Roughtime server") 32 | dialTimeout := flag.Duration("d", client.DefaultQueryTimeout, "Time to wait for each dial attempt") 33 | rtMaxRadius := flag.Duration("r", time.Second*10, "Maximum uncertainty radius permitted from Roughtime server") 34 | alertThreshold := flag.Duration("thresh", time.Second*10, "Minimum clock skew for triggering an alert") 35 | logFile := flag.String("log", "/dev/stdout", "File to which to write the log") 36 | 37 | flag.Parse() 38 | 39 | // Logging 40 | f, err := os.OpenFile(*logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | defer f.Close() 45 | logger := log.New(f, "", log.Ldate|log.Lshortfile) 46 | client.SetLogger(logger) 47 | 48 | // Query the Roughtime servers. 49 | t0 := time.Now() 50 | res, err := client.DoFromFile(*rtConfig, *dialAttempts, *dialTimeout, nil) 51 | if err != nil { 52 | logger.Fatal(err) 53 | } 54 | 55 | // Compute the median difference between t0 and the time reported by the 56 | // each server, excluding those servers whose radii are too large. 57 | delta, err := client.MedianDeltaWithRadiusThresh(res, t0, *rtMaxRadius) 58 | if err != nil { 59 | logger.Fatal(err) 60 | } 61 | logger.Printf("delta: %v", delta.Truncate(time.Millisecond)) 62 | 63 | // Check if the skew exceeds the alert threshold. If so, then use 64 | // `notify-send` to emit an alert. 65 | skew := time.Duration(math.Abs(float64(delta))) 66 | if skew > *alertThreshold { 67 | body := fmt.Sprintf(template, skew.Truncate(time.Millisecond), *logFile) 68 | cmd := exec.Command("notify-send", "-u", "critical", "-i", "clock", summary, body) 69 | if err := cmd.Run(); err != nil { 70 | logger.Fatal(err) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /recipes/testdata/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j 5 | ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL 6 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 7 | LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug 8 | RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm 9 | +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW 10 | PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM 11 | xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB 12 | Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 13 | hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg 14 | EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF 15 | MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA 16 | FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec 17 | nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z 18 | eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF 19 | hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 20 | Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe 21 | vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep 22 | +OkuE6N36B9K 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /recipes/tls.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | // This example configures a TLS client to use Roughtime to synchronize its 5 | // clock and sends a GET request to https://example.com. 6 | package main 7 | 8 | import ( 9 | "crypto/tls" 10 | "crypto/x509" 11 | "flag" 12 | "log" 13 | "net/http" 14 | "os" 15 | "time" 16 | 17 | "github.com/cloudflare/roughtime/client" 18 | ) 19 | 20 | func main() { 21 | caFile := flag.String("ca", "recipes/testdata/ca.pem", "File with the server's CA certificate") 22 | rtConfig := flag.String("rt", "ecosystem.json", "File with the Roughtime configuration") 23 | dialAttempts := flag.Int("a", client.DefaultQueryAttempts, "Number of times to try dialing each Roughtime server") 24 | dialTimeout := flag.Duration("d", client.DefaultQueryTimeout, "Time to wait for each dial attempt") 25 | rtMaxRadius := flag.Duration("r", time.Second*10, "Maximum uncertainty radius permitted from Roughtime server") 26 | 27 | flag.Parse() 28 | logger := log.New(os.Stdout, "", 0) 29 | client.SetLogger(logger) 30 | 31 | // Load the list of Roughtime-server configurations. 32 | rtServers, skipped, err := client.LoadConfig(*rtConfig) 33 | if err != nil { 34 | logger.Fatal(err) 35 | } 36 | if len(rtServers) == 0 { 37 | logger.Fatalf("No valid servers (skipped %d)", skipped) 38 | } 39 | 40 | // Get the system clock's current time, then immediately query the Roughtime 41 | // servers. 42 | t0 := time.Now() 43 | res := client.Do(rtServers, *dialAttempts, *dialTimeout, nil) 44 | 45 | // Compute the median difference between t0 and the time reported by each 46 | // server, rejecting those responses whose radii are too large. (Note that 47 | // this accounts for network delay.) 48 | delta, err := client.MedianDeltaWithRadiusThresh(res, t0, *rtMaxRadius) 49 | if err != nil { 50 | logger.Fatal(err) 51 | } 52 | logger.Printf("delta: %v\n", delta.Truncate(time.Millisecond)) 53 | 54 | ca, err := os.ReadFile(*caFile) 55 | if err != nil { 56 | logger.Fatal(err) 57 | } 58 | certPool := x509.NewCertPool() 59 | certPool.AppendCertsFromPEM(ca) 60 | 61 | httpClient := &http.Client{ 62 | Transport: &http.Transport{ 63 | TLSClientConfig: &tls.Config{ 64 | RootCAs: certPool, 65 | Time: func() time.Time { 66 | return time.Now().Add(delta) 67 | }, 68 | }, 69 | }, 70 | } 71 | 72 | resp, err := httpClient.Get("https://example.com") 73 | if err != nil { 74 | logger.Fatal(err) 75 | } 76 | logger.Print(resp.Status) 77 | resp.Body.Close() 78 | } 79 | -------------------------------------------------------------------------------- /roughtime.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloudflare, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:generate go run ./internal/ecosystem_json_go_builder 16 | 17 | package roughtime 18 | --------------------------------------------------------------------------------